Guide 02 · Developer docs

Template languages: Smarty and Twig

Core dna renders server-side sites with Smarty or with Twig, one engine per site, and treats the two as peers. This guide is the one place both are documented: what they share, each engine's language and the platform's additions and limits, the two side by side, and translation.

L1–L2
Unit 02 · Smarty

Smarty syntax: output, assignment and control flow

The Smarty you write every day on Core dna, checked against the version the platform runs: printing values and chaining modifiers, assigning and the scope rules that bite, conditions and loops with their loop properties, the ternary that needs parentheses, and quoting and escaping inside expressions.

Who this is for. The developer writing templates on a Smarty site who wants the working subset of the language in one place, without the upstream manual. Afterwards you can write output, assignments, conditions and loops that compile first time and behave as you meant.

Output and modifiers

A tag that is only a value prints it. Values come from variables, method calls on records, array keys, literals and PHP functions the policy allows. A modifier transforms the value: it follows a |, takes its arguments after colons, and modifiers chain left to right.

Template code Smarty only
{$page->getName()}                                  {* a method on a record *}
{$item.title}   {$item['data-id']}                   {* array keys *}
{$page->getName()|escape}                           {* one modifier *}
{$page->getAreaText('summary')|strip_tags|truncate:160:'…'|escape}   {* a chain, left to right *}
{trim($_request->getPath(), '/')}                   {* a PHP function the policy allows *}
{strtoupper($code)|escape}                          {* a function, then a modifier *}

Order matters in a chain: |strip_tags before |truncate, so the cut does not land inside a tag, and |escape last, so nothing after it can undo it. Which modifiers exist, and why a PHP function such as trim is called rather than piped, is on The template security policy.

Assignment and scope

Assign with the inline form; the {assign} tag does the same and is older. {capture} assigns rendered markup.

Template code Smarty only
{$count = $items|count}
{$heading = $component->getAreaText('heading')|default:'Latest news'}
{assign var='layoutClass' value='wide'}       {* the same as {$layoutClass = 'wide'} *}

{$tags = []}
{$tags[] = 'featured'}                        {* append to an array *}
{$sizes = ['width' => 800, 'height' => 600]}

{capture name='cardTitle'}<h3>{$heading|escape}</h3>{/capture}
{$smarty.capture.cardTitle}
{capture assign='footer'}<p>{$count} items</p>{/capture}   {* straight into a variable *}

The scope rule that catches people: a variable assigned inside an included template stays there. The partial sees all of its caller's variables, but its own assignments vanish when it ends. The scope attribute changes that:

AttributeWhere the value lands
noneThe current template only
scope='parent'The template that included this one
scope='root'The outermost template of this render
scope='global'Every template rendered for the rest of the request, including components and records rendered later with getHtml()

It works on the inline form, {$seen = true scope='parent'}, on {assign}, and on an {include} as a whole. Prefer _templateContext for anything that crosses templates: it reads the same in both engines and the name says where the value lives (The template context). Use scope='global' sparingly: the value outlives the template that set it and can surprise one rendered much later.

Conditions and loops

{if}, {elseif} and {else} take PHP-style comparisons (==, !=, ===, >, <=), &&, || and !, plus Smarty's tests such as is even, is odd and is div by 3. A loop over a page's component collection, in the shape production templates use:

Template code templates/pages/pages/landing.html Smarty only
{foreach $page->getAreaCollectionElements('pageSections') as $element}
  {$section = ($element->isGlobal()) ? $element->getGlobalComponent() : $element->getComponent()}
  {if $section}
    <section class="section{if $element@first} section--first{/if}{if $element@last} section--last{/if}"
             data-position="{$element@iteration}">
      {$section->getHtml()}
    </section>
  {/if}
{foreachelse}
  <p class="empty">No sections yet.</p>
{/foreach}

{* the key as well as the value *}
{foreach $sizes as $name => $pixels}{$name}: {$pixels}px {/foreach}
Loop propertyValue
$item@iterationThe pass number, from 1
$item@indexThe position, from 0
$item@first, $item@lastTrue on the first and on the last pass
$item@totalThe number of items
$item@keyThe current key

{foreachelse} renders when the list is empty or not a list at all, which covers a collection area an editor has left blank. {break} and {continue} work inside loops. The other loop tags work too, though production templates rarely need them: {for $i = 1 to 5} (with step for other increments), {while $i < 3}, and {section}, the pre-{foreach} loop, which you may meet in older templates.

A ternary needs parentheses

Smarty accepts the ternary operator only when its condition is in parentheses. Without them the template does not compile, and the error points at the question mark:

Template code Smarty only
{$label = ($count == 1) ? 'item' : 'items'}     {* compiles *}
{$label = $count == 1 ? 'item' : 'items'}       {* fails: Unexpected " ? ", expected one of: "}" *}
{$flag ? 'yes' : 'no'}                          {* fails the same way, even with a bare variable *}

{($count > 1) ? 'items' : 'item'|upper}         {* the modifier applies to 'item' only *}
{(($count > 1) ? 'items' : 'item')|upper}       {* the modifier applies to the result *}

{$title = $heading ?? 'Untitled'}               {* fails: ?? is not Smarty *}
{$title = $heading|default:'Untitled'}          {* the Smarty way *}

A modifier written after the last branch binds to that branch alone. Wrap the whole ternary in parentheses when a modifier should apply to the result. |default replaces a value that is unset, null or an empty string; it leaves 0 and '0' alone.

Strings, quoting and escaping inside expressions

  • Double quotes interpolate, single quotes do not. "Results for {$query}" inserts the value, and so does a variable in backticks, "Page `$page->getId()`", which also takes a method call. 'Results for {$query}' is the literal text.
  • Joining strings is |cat: {$label = 'Page '|cat:$number}.
  • A value inside an HTML attribute needs |escape, which turns quotes into entities, so a title containing " cannot end the attribute: title="{$title|escape}".
  • The right escape for the context. |escape:'url' for a value in a query string, |json_encode for a value handed to JavaScript, plain |escape for HTML. Escaping twice shows as &amp; on the page: |escape encodes the ampersand of an entity that is already there unless you pass |escape:'html':'UTF-8':false.
  • Do not escape richtext. A Richtext area holds HTML on purpose; escaping it prints the tags.
Go deeper

Was this page helpful?

Your answer goes to the docs team.