Fabien Potencier has published the details on a rewrite of the macro system landing in Twig 4.0. Macros now follow the same argument rules as functions and filters, so a missing argument or a mistyped named argument raises an error instead of rendering broken HTML.
As Fabien puts it, macros have been the odd one out for a long time:
Macros are as old as Twig itself. They look like functions, you call them like functions, and for more than fifteen years they have quietly refused to behave like functions: every argument is optional, extra arguments vanish into a magic variable and a typo in a named argument is silently ignored.
Arguments Are Required Unless You Give Them a Default
In Twig 4.0, a macro argument is required unless the signature gives it a default value:
{% macro input(name, value = '', type = 'text', size = 20) %} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}"/> {% endmacro %} {# Error: Value for argument "name" is required for macro "input". #} {{ forms.input() }}
This says that name has to be passed, and the other three fall back to their defaults. If you want to keep an argument optional, give it a default. Using = null reproduces the old behavior.
The errors are specific about what went wrong:
Value for argument "name" is required for macro "input".Unknown argument "sise" for macro "input".Too many arguments for macro "input".Argument "name" is defined twice for macro "input".
Variadic Arguments Are Now Explicit
Extra arguments no longer disappear into a magic varargs variable. You declare them with ...:
{% macro tag(element, ...attrs) %} <{{ element }} {%- for name, value in attrs %} {{ name }}="{{ value }}"{% endfor -%} > {% endmacro %} {{ html.tag('input', type: 'text', name: 'username') }}
Everything after element collects into attrs, which the macro loops over to build the attribute list. The explicit syntax already works in Twig 3.29, and a macro that relies on the implicit varargs variable keeps working once it declares ...varargs.
Parentheses Are Required to Call a Macro
Calling a macro without parentheses is out. If you want to check whether a macro exists, use the defined test, which inspects the macro instead of calling it:
{{ forms.input() }} {% if forms.input is defined %}...{% endif %}
Dynamic Macro Names and Deprecations
Twig 3.28 added dynamic macro names, so the macro you call can be computed at runtime by wrapping an expression in parentheses after the dot:
{% set field = widget.multiline ? 'textarea' : 'input' %} {{ forms.(field)('description') }}
Macros can also be deprecated now, which is handy if you maintain a shared template library:
{% macro input(name, value = '') %} {% deprecated 'The "input" macro is deprecated, use "field" instead.' %} <input name="{{ name }}" value="{{ value|e }}"/> {% endmacro %}
How to Migrate
Twig 3.29 triggers a deprecation for every macro call and definition that will break in 4.0, and all the fixes work on 3.29. So if your app runs deprecation free on 3.29, you're ready for Twig 4.0.
For the full write-up and more examples, read New in Twig 4.0: A New Macro System on the Symfony blog.