Who this is for. The developer who knows one engine and has to read, review or write the other, and anyone estimating a port. Each row has an id, so a review comment can link straight to it. The engine pages are Smarty and Twig; the behaviour that differs, beyond syntax, is on Behaviour that differs between the engines.
The equivalence table
| Construct | Smarty | Twig |
|---|---|---|
| Print a value | {$title} | {{ title }} |
| Call a method on a record | {$page->getName()} | {{ page.getName() }} |
| Getter or area as an attribute | {$component->title} | {{ component.title }} |
| Array key | {$item.title}, {$item['data-id']} | {{ item.title }}, {{ item['data-id'] }} |
| Modifier or filter | {$title|truncate:60:'…'} | {{ title|u.truncate(60, '…') }} |
| Fallback for a missing value | {$title|default:'Untitled'} | {{ title|default('Untitled') }} or {{ title ?? 'Untitled' }} |
| Escape for HTML | {$title|escape} | {{ title|e }} |
| Escape for a URL | {$q|escape:'url'} | {{ q|e('url') }} |
| Encode for JavaScript or JSON | {$data|json_encode} | {{ data|json_encode }} |
| Join strings | {'Page '|cat:$n} | {{ 'Page ' ~ n }} |
| A variable inside a string | "Results for {$q}" | "Results for #{q}" |
| Condition | {if $a == 1 && !$b}…{elseif $c}…{else}…{/if} | {% if a == 1 and not b %}…{% elseif c %}…{% else %}…{% endif %} |
| Ternary | {($n == 1) ? 'item' : 'items'}, condition in parentheses | {{ n == 1 ? 'item' : 'items' }} |
| Loop | {foreach $items as $item}…{/foreach} | {% for item in items %}…{% endfor %} |
| Loop with keys | {foreach $map as $key => $value} | {% for key, value in map %} |
| Empty list | {foreachelse} | {% else %} |
| Pass number, position | $item@iteration (from 1), $item@index (from 0) | loop.index, loop.index0 |
| First, last, count | $item@first, $item@last, $item@total | loop.first, loop.last, loop.length |
| Leave or skip a pass | {break}, {continue} | None: filter the list first, items|filter(i => …), or |slice |
| Count from 1 to 5 | {for $i = 1 to 5}…{/for} | {% for i in 1..5 %}…{% endfor %} |
| Assign | {$count = 3} | {% set count = 3 %}, also stored in the template context |
| Capture markup | {capture assign='intro'}…{/capture} | {% set intro %}…{% endset %} |
| Append to a list | {$list[] = $item} | {% set list = list|merge([item]) %} |
| Array or map literal | ['width' => 800] | {width: 800} |
| Extend a base template | {extends 'includes/base.html'} | {% extends 'includes/base.html' %} |
| Define or fill a block | {block name='content'}…{/block} | {% block content %}…{% endblock %} |
| Keep the base's content | {$smarty.block.parent}, or append, prepend | {{ parent() }} |
| Render a block only when a child fills it | {block name='promo' hide}…{$smarty.block.child}…{/block} | {% if block('promo') is defined %}…{% endif %} |
| Include with values | {include 'elements/card.html' title=$t} | {% include 'elements/card.html' with {title: t} %} |
| Include with only the values passed | None | {% include 'elements/card.html' with {title: t} only %} |
| Reusable fragment | {function name=badge label=''}…{/function}, then {badge label='New'} | {% macro badge(label) %}…{% endmacro %}, then {% import … as ui %}{{ ui.badge('New') }} |
| Wrapper the caller fills | Capture the parts, pass them to an include | {% embed 'elements/panel.html' %}{% block body %}…{% endblock %}{% endembed %} |
| Comment | {* … *} | {# … #} |
| Print braces literally | {literal}…{/literal}; a brace followed by a space is literal anyway | {% verbatim %}…{% endverbatim %} |
| Remove whitespace | {strip}…{/strip} | {%- … -%}, {{- … -}} |
| Is it set? | {if isset($x)} | {% if x is defined %} (always true for a record's attributes) |
| Is it empty? | {if empty($x)} | {% if x is empty %}; a record is never empty |
| Convert a type | |intval, |strval | |integer, |float, |string, |boolean, which fail on null |
| A PHP helper | Called from the policy's list: {trim($x, '/')} | A filter: {{ x|trim('/') }} |
| Translate a key | {'cart.add'|_:'site-shop'} | {{ __.translate('cart.add', {}, 'site-shop') }}, usually through a macro |
| A date in the site's format | {$page->getPublished()|format:'date'} | {{ page.getPublished()|date(_config.get('Globals.i18n.dateFormat')) }} |
| JSON written out as text | {json}…{/json} | Build a map and use |json_encode |
| Dump values on development | {debug_vars page=$page} | {{ debug_vars(page) }} |
| Stop the render with an error | {error message='…'} | None |
| A query-string parameter | {$_request->getParameter('q')} | {{ _request.getParameter('q') }} |
| Store for the rest of the render | {$_templateContext->set('key', $value)} | {% do _templateContext.set('key', value) %} |
| Call a method for its effect | {$_response->preventCaching()} (prints nothing) | {% do _response.preventCaching() %} |
What only Smarty has
- Registered translation tags:
{_},|_and{__}. In Twig, a macro over__.translate()(Translating in Twig). |format: a date in the site's configured format. In Twig,|datewith the format read from_config, as in the table, kept in one macro.- The
{json}block. In Twig, build a map withsetand|json_encode, which cannot produce invalid JSON. {error}. Twig has no way to stop a render on purpose; render nothing, or a visible message on development environments.{break}and{continue}. In Twig, filter or slice the list before the loop.- The function list as a safety net: a call outside the policy does not compile. Twig has no sandbox here; review takes its place (Twig security).
- A loud failure for a method a record does not have. In Twig the same call prints nothing; check the method against the record's list (Response models).
What only Twig has
embed. In Smarty, capture each part with{capture}and pass the captures to an include.- Macros imported from a library. Smarty's
{function}is the nearest equivalent; a function defined in another file is available only after that file has been included. - Isolated includes,
only. In Smarty, pass every value the partial needs and name partial variables so they cannot collide. - Strict variables, used by the platform only for Mailer emails. Smarty never raises on an undefined variable; it logs a warning.
- The string extension:
|uand|slug. In Smarty,|truncate,|lowerand|regex_replacecover most uses, and$_string->slugify()makes slugs. - Typed coercion:
|boolean,|integer,|float,|string. Smarty's|intvaland|strvaldo not fail onnull. ignore missingon an include. In Smarty, keep the possible paths to a fixed list.