Slide 1

Slide 1 text

Twig: Still relevant in 2025? Fabien Potencier

Slide 2

Slide 2 text

Twig turned 15!

Slide 3

Slide 3 text

Twig is "still" widely used

Slide 4

Slide 4 text

The SPA Pendulum Swing The return of server-side rendering

Slide 5

Slide 5 text

> htmx Click Me Supported in Symfony since 2.0 preview release 5 - Dec 8, 2010 https://github.com/symfony/symfony/commit/e8672740 https://jolicode.com/blog/making-a-single-page-application-with-htmx-and-symfony

Slide 6

Slide 6 text

> htmx #[Route('/messages', name: 'messages')] public function messages(MessageRepository $messageRepo): Response { return $this->render('messages.html.twig', [ 'messages' => $messageRepo->findAll(), // many more variables ]); } {% extends 'base.html.twig' %} {% block body %}
    {% for message in messages %}
  • {{ message.content }}
  • {% endfor %}
Refresh {% endblock %}

Slide 7

Slide 7 text

#[Route('/messages/refresh', name: 'refresh_messages')] #[Template('messages/messages.html.twig', block: 'messages')] public function messages(MessageRepository $messageRepository): array { return ['messages' => $messageRepository->findAll()]; } {% extends 'base.html.twig' %} {% block body %}
    {% block messages %} ... {% endblock %}
Refresh {% endblock %} "block" attribute added in Symfony 7.2 https://github.com/symfony/symfony/pull/58028 Twig ❤ > htmx

Slide 8

Slide 8 text

Twig Sandbox User-contributed templates with security https://twig.symfony.com/doc/3.x/sandbox.html {{ include('not_trusted.html.twig', sandboxed: true) }}

Slide 9

Slide 9 text

Twig 3.9+ Twig evolved a lot in the last 6 months Template cache hot reload Operator precedence fixes Arrows everywhere Named arguments (macros, ...) Twig callable arguments as snake-case or camelCase Colon as argument separator Yielding instead of echo-ing Inline comments ChainCache Argument unpacking Static Analyzer support{% types %} Compile time checks {% guard %} Functions: enum, html_cva, enum_cases Filters: find, singular, plural, shuffle Tests: sequence, mapping Tons of internal changes/refactoring to make the code more robust (no more global functions!)

Slide 10

Slide 10 text

Template cache hot reload https://github.com/twigphp/Twig/pull/4338 Use cases: * Long-running processes - Messenger worker * Modern PHP app servers - FrankenPHP * User templates - stored in a DB * ...

Slide 11

Slide 11 text

Template cache hot reload https://github.com/twigphp/Twig/pull/4338 $twig = new Twig\Environment($loader, [ 'auto_reload' => false, 'cache' => __DIR__.'/cache', ]); // prime the cache echo $twig->load('index.twig')->render([]); // update the template file_put_contents(__DIR__.'/index.twig', file_get_contents(__DIR__.'/index.twig').'foo'); // still use the cached version echo $twig->load('index.twig')->render([]); // remove the cache for this template $twig->removeCache('index.twig'); // refresh the cache with the updated template echo $twig->load('index.twig')->render([]);

Slide 12

Slide 12 text

Template cache hot reload https://github.com/twigphp/Twig/pull/4338 In memory cache only (read-only cache)

Slide 13

Slide 13 text

ChainCache ReadOnlyFilesystemCache https://github.com/twigphp/Twig/pull/4171 https://github.com/symfony/symfony/pull/54384

Slide 14

Slide 14 text

Operator Precedence 2015 2019 2020 2022 2013 2014 2017

Slide 15

Slide 15 text

Operators Precedence {{ '42' ~ 1 + 41 }} ? {{ '42' ~ (1 + 41) }} {{ ('42' ~ 1) + 41 }} Probably not what you would expect Probably what you would expect

Slide 16

Slide 16 text

4242 462 How to NOT break BC? Twig 3 Twig 4 {{ '42' ~ (1 + 41) }} {{ ('42' ~ 1) + 41 }} {{ '42' ~ 1 + 41 }} https://github.com/twigphp/Twig/pull/4363

Slide 17

Slide 17 text

A framework to automate these changes Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4.

Slide 18

Slide 18 text

Other operator precedence changes 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange(70)] '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange(5)] '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange(27)]

Slide 19

Slide 19 text

PHP 8 has a similar change concatenation vs addition/substraction https://wiki.php.net/rfc/concatenation_precedence

Slide 20

Slide 20 text

Inline comments {{ # this is an inline comment "Hello World"|upper # this is an inline comment }} {{ { # this is an inline comment fruit: 'apple', # this is an inline comment color: 'red', # this is an inline comment }|join(', ') }} {{ "Hello World"|upper # this is an inline comment }} {{ "Hello World"|upper # this is an inline comment }} Nope Yep Yep Yep

Slide 21

Slide 21 text

Inline comments https://github.com/twigphp/Twig/pull/4349

Slide 22

Slide 22 text

Twig callables A Twig Filter is a Twig callable A Twig Function is a Twig callable A Twig Test is a Twig callable A Twig Tag is NOT a Twig callable 'needs_environment' => false, 'needs_context' => false, 'needs_charset' => false, 'is_variadic' => false, 'deprecation_info' => null, 'deprecated' => false, 'deprecating_package' => '', 'alternative' => null,

Slide 23

Slide 23 text

: vs = for named arguments https://github.com/twigphp/Twig/pull/4209 https://twig.symfony.com/doc/3.x/coding_standards.html

Slide 24

Slide 24 text

Named arguments everywhere https://github.com/twigphp/Twig/pull/4300 {# dot operator#} {{ html.generate_input(name: 'pwd', type: 'password') }} {# macro #} {{ html.generate(name: 'pwd', type: 'password') }} {# block function #} {{ block(name: "title", template: "common_blocks.twig") }}

Slide 25

Slide 25 text

Static analysis with {% types %} https://github.com/twigphp/Twig/pull/4235 https://twig.symfony.com/types {% types { is_correct: 'boolean', score?: 'number', } %} https://github.com/alisqi/TwigQI https://github.com/twigstan/twigstan

Slide 26

Slide 26 text

Compile-time checks with {% guard %} https://github.com/twigphp/Twig/pull/4304 https://twig.symfony.com/guard {% guard function importmap %} {{ importmap('app') }} {% else %} {# the importmap function doesn't exist, generate fallback code #} {% endguard %}

Slide 27

Slide 27 text

Arguments unpacking https://github.com/twigphp/Twig/pull/4300 {% set numbers = [1, 2, ...moreNumbers] %} {% set ratings = {'foo': 10, 'bar': 5, ...moreRatings} %} {{ 'Hello %s %s!'|format(...['Fabien', 'Potencier']) }} New

Slide 28

Slide 28 text

Twig callable args as camel or snake https://github.com/twigphp/Twig/pull/4318 public static function htmlCva(... $compoundVariants = [], ...): Cva {% set alert = html_cva( // ... compound_variants=[{ color: ['red'], size: ['md', 'lg'], class: 'font-bold' }] ) %} PHP likes camelCase Twig likes snake_case

Slide 29

Slide 29 text

Arrows everywhere https://github.com/twigphp/Twig/pull/4377 https://github.com/twigphp/Twig/pull/4378 {% set first_name_fn = (p) => p.first %} {{ _self.display_people(people, first_name_fn) }} {% macro display_people(people, fn) %} {{ people|map(fn)|join(', ') }} {% endmacro %}

Slide 30

Slide 30 text

Yielding output instead of ob_* output https://github.com/twigphp/Twig/pull/3950

Slide 31

Slide 31 text

for tag new implementation 4.x only

Slide 32

Slide 32 text

for improvements (4.x only) loop.last always available non-countable generators https://github.com/twigphp/Twig/pull/4134

Slide 33

Slide 33 text

for improvements (4.x only) loop.previous / loop.next {% for value in values %} {% if not loop.first and value > loop.previous %} The value just increased! {% endif %} {{ value }} {% if not loop.last and loop.next > value %} The value will increase even more! {% endif %} {% endfor %} https://github.com/twigphp/Twig/pull/4135 New

Slide 34

Slide 34 text

for improvements (4.x only) loop.changed {% for entry in entries %} {% if loop.changed(entry.category) %}

{{ entry.category }}

{% endif %}

{{ entry.message }}

{% endfor %} https://github.com/twigphp/Twig/pull/4135 New

Slide 35

Slide 35 text

for improvements (4.x only) loop.cycle {% for row in rows %}
  • {{ row }}
  • {% endfor %} https://github.com/twigphp/Twig/pull/4135 New {{ cycle(['odd', 'even'], loop.index0) }}

    Slide 36

    Slide 36 text

    for improvements (4.x only) recursive loops via loop()
      {%- for item in sitemap %}
    • {{ item.title }} {%- if item.children -%} {%- endif %}
    • {%- endfor %}
    New https://github.com/twigphp/Twig/pull/4153 New

    Slide 37

    Slide 37 text

    for improvements (4.x only) if support is back! {% set stopOnFabien = false %} {% for user in users if not stopOnFabien %} - {{ user }} {% set stopOnFabien = user == 'Fabien' %} {% endfor %} https://github.com/twigphp/Twig/pull/4251 {% for user in users if loop.length != 5 %} - {{ user }} {% endfor %}

    Slide 38

    Slide 38 text

    Welcome to the new world! 💪 + +

    Slide 39

    Slide 39 text

    https://symfony.com/sponsor Sponsor Symfony Thank you!