$30 off During Our Annual Pro Sale. View Details »

SymfonyCon Vienna 2025: Twig, still relevant in...

SymfonyCon Vienna 2025: Twig, still relevant in 2025?

Fabien Potencier

December 09, 2024
Tweet

More Decks by Fabien Potencier

Other Decks in Programming

Transcript

  1. </> htmx <button hx-post="/clicked" hx-swap="outerHTML"> Click Me </button> 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
  2. </> 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 %} <ul id="messages"> {% for message in messages %} <li>{{ message.content }}</li> {% endfor %} </ul> <a href="{{ path('messages') }}">Refresh</a> {% endblock %}
  3. #[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 %} <ul id="messages"> {% block messages %} ... {% endblock %} </ul> <button hx-post="{{ path('refresh_messages') }}" hx-target="#messages"> Refresh </button> {% endblock %} "block" attribute added in Symfony 7.2 https://github.com/symfony/symfony/pull/58028 Twig ❤ </> htmx
  4. 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!)
  5. 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 * ...
  6. 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([]);
  7. Operators Precedence {{ '42' ~ 1 + 41 }} ?

    {{ '42' ~ (1 + 41) }} {{ ('42' ~ 1) + 41 }} Probably not what you would expect Probably what you would expect
  8. 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
  9. 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.
  10. 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)]
  11. 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
  12. 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,
  13. 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") }}
  14. 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
  15. 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 %}
  16. 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
  17. 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
  18. 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 %}
  19. 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
  20. for improvements (4.x only) loop.changed {% for entry in entries

    %} {% if loop.changed(entry.category) %} <h2>{{ entry.category }}</h2> {% endif %} <p>{{ entry.message }}</p> {% endfor %} https://github.com/twigphp/Twig/pull/4135 New
  21. for improvements (4.x only) loop.cycle {% for row in rows

    %} <li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li> {% endfor %} https://github.com/twigphp/Twig/pull/4135 New {{ cycle(['odd', 'even'], loop.index0) }}
  22. for improvements (4.x only) recursive loops via loop() <ul class="sitemap">

    {%- for item in sitemap %} <li>{{ item.title }} {%- if item.children -%} <ul class="submenu depth-{{ loop.depth }}"> {{ loop(item.children) }} </ul> {%- endif %} </li> {%- endfor %} </ul> New https://github.com/twigphp/Twig/pull/4153 New
  23. 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 %}