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 01 · The shared model

Global template variables

Besides the record it renders, every front-end template receives the same platform helpers, named with a leading underscore: the request, the response, URLs, the site, the visitor, menus, queries and translation. The full list, what each is for, and which exist only for a signed-in visitor.

Who this is for. The theme developer who needs something the record does not carry: the query string, the visitor, a menu, a list of other records. Afterwards you will know which helper holds it, what it is called in your engine, and which ones can be missing.

Two kinds of variable

A template rendered for a page request receives two sets of variables. The controller's own, which depend on what it rendered (page and record for a page, post for a blog post; the full list), and the platform's common set below, which every front-end render gets. The names are the same in both engines: Smarty writes $_request, Twig _request.

PHP's superglobals are not among them. In Smarty, {$smarty.get.page}, {$smarty.server.HTTP_HOST} and their siblings fail to compile with (secure mode) super globals not permitted, and $_GET is an undefined variable. Twig has no way to reach them at all. The request, the cookies and the session are read through the helpers.

The common variables

VariableWhat it isWhat you call on it
_requestThe current requestgetPath(), getParameter(name), getParameters(), getQueryString(), getMethod(), getCookie(name, default), getCookies(), getHostname(), getScheme(), getSchemeAndHost(), getLocale(), getLanguage(), isPdfRequest()
_responseThe response being builtpreventCaching(), enforceCaching(), setCacheTTL(seconds), setCacheExpiryDate(date), addCacheVaryParameter(name), setCacheVaryParameters(names), setCookie(name, value, expire, path), redirect(url, status), returnNotFound(), forceDownload(filename)
_urlPaths built from route namesgetPath(moduleRoute, controllerRoute, parameters), getFrontendPath(…), getModulePath(moduleRoute), hasPath(…), getCurrentPath(), isCurrentPath(path)
_currentSiteThe site and the environment serving the requestgetId(), getName(), getHostname(), getPrimaryHostname(), getEnvironmentName(), isPrimaryEnvironment(), getLastVcsHash()
_currentUserWhoever is signed in, member or backend userisLoggedIn(), isFrontendUser(), isBackendUser(), isAdmin(), isDeveloper(), getType(), getId(), getFullName(), getUsername(), getPerson()
_siteDataValues editors keep under site data, without a deployget(key): the current site's value, else the client-wide one, else nothing
_configThe site's settingsget('Module.setting'), has(key). A protected setting reads as ***
_menuMenus, by their exact admin nameget(name), nothing when no menu has that name
_globalComponentsGlobal components, by identifierget(identifier)
_formsForms, by identifierget(identifier)
_queryRecords the controller did not loadfindBy(entity, criteria, orderBy, limit, offset), findOneBy(…), findCustomEntityRecordsBy(type, …), findCustomEntityRecordBy(type, …)
_imageAn image from the Files module, by pathget(path)
_searchThe Search modulesearch(view, parameters)
_cartThe visitor's cartgetCart(), isIn(productCode), getItem(productCode)
_messagesMessages queued for the visitor, such as a sign-out confirmationhasMessages(type), getMessages(type), with success, warning or error
_notificationsThe signed-in user's notificationsgetUnseenCount(), getNotifications()
_templateContextA store shared by every template of one renderset(key, value), get(key, default), all()
_stringString helpersslugify(text), camelToSlug(text), snakeToWords(text), getHtmlSafeJson(array)
__The translatortranslate(key, parameters, domain), setDefaultDomain(domain)
_inlineScriptNonceA string: the nonce every inline <script> needsPrint it in the nonce attribute
_csrfTokenA string: the form security token. Only when someone is signed inPrint it in forms that post to the platform

The table leaves out _widgets: widgets are deprecated, and a template reads records with the query helper _query instead. The helpers that look a record up by name or identifier answer with nothing when there is no match, rather than an error. A renamed menu or a mistyped form identifier therefore leaves that part of the page empty: test the result before using it.

The ones used on every site

Template code
{* the request: read, then check against a list before using *}
{$view = $_request->getParameter('view')|default:'grid'}
{if !in_array($view, ['grid', 'list'])}{$view = 'grid'}{/if}

{* paths from route names, not typed URLs *}
<a href="{$_url->getPath('login')}">Sign in</a>

{* the visitor *}
{if $_currentUser->isLoggedIn()}
  <p>Signed in as {$_currentUser->getFullName()|escape}</p>
{/if}

{* a menu, by its exact name *}
{$menu = $_menu->get('Header Menu')}
{if $menu}
  {foreach $menu->getRootElements() as $item}
    <a href="{$item->getUrl()}"{if $item->isActive()} aria-current="page"{/if}>{$item->getName()|escape}</a>
  {/foreach}
{/if}

{* records the controller did not load: always pass a limit *}
{$posts = $_query->findBy('Blogs\Entities\Post', ['blog' => 3], ['published' => 'DESC'], 5)}
{# the request: read, then check against a list before using #}
{% set view = _request.getParameter('view')|default('grid') %}
{% if view not in ['grid', 'list'] %}{% set view = 'grid' %}{% endif %}

{# paths from route names, not typed URLs #}
<a href="{{ _url.getPath('login') }}">Sign in</a>

{# the visitor #}
{% if _currentUser.isLoggedIn() %}
  <p>Signed in as {{ _currentUser.getFullName()|e }}</p>
{% endif %}

{# a menu, by its exact name #}
{% set menu = _menu.get('Header Menu') %}
{% if menu %}
  {% for item in menu.getRootElements() %}
    <a href="{{ item.getUrl() }}"{% if item.isActive() %} aria-current="page"{% endif %}>{{ item.getName()|e }}</a>
  {% endfor %}
{% endif %}

{# records the controller did not load: always pass a limit #}
{% set posts = _query.findBy('Blogs\\Entities\\Post', {'blog': 3}, {'published': 'DESC'}, 5) %}

Notes on what those calls do:

  • getParameter() reads the query string only, not a posted form, and returns nothing when the parameter is absent. It also adds the parameter to the page's cache key, so a cached ?view=list is not served to a request for ?view=grid. getParameters() does not do that. Anything read from the request came from the visitor: check it against the values you expect before it chooses anything, and escape it before printing it.
  • _url.getPath() takes route names, such as login, logout, register or a module's name, so links survive a site changing a module's URL prefix. A name that does not exist is an error.
  • _query reads a fixed list of entity types: pages, books, units and book pages, blogs, posts and comments, FAQ pages and questions, products, variants, descriptions and categories, forms, menus and menu elements, files, images and folders, global components, layouts, categories, tags, languages, custom URLs, rewrites, search views and a few lookups. Users, people, orders and carts are not on it, and asking for one fails with Entity … my not be queried (the platform's spelling). Results obey the same publishing and permission rules as the page, and records of other sites are left out. Pass a limit every time; The three query-per-row shapes explains why.
  • In Twig, the backslashes in an entity's class name are doubled inside quotes, 'Blogs\\Entities\\Post'. A single backslash is dropped by Twig's string parser.

The ones that depend on a signed-in visitor

  • _csrfToken exists only when someone is signed in. On a page a signed-out visitor can see, write {$_csrfToken|default:''} in Smarty or {{ _csrfToken|default('') }} in Twig. Smarty prints nothing for an undefined variable too, but records a warning in the site's error log on every view.
  • _currentUser.getPerson() is null when nobody is signed in; test isLoggedIn() first.
  • _notifications.getNotifications() fails with Cannot get notifications when not logged in for a signed-out visitor; getUnseenCount() answers 0.
  • Members and backend users are both “signed in”. A content editor viewing the site while signed in to the admin is logged in too. Use isFrontendUser() when a block is meant for members only.

A page that shows anything from these varies by visitor and must not be served from the shared cache. Authenticated requests and the cache covers what the platform does about it and when to call preventCaching().

How the two engines hold them

  • Smarty: plain template variables. An included template sees them because it sees all of the caller's variables.
  • Twig: the platform registers every variable whose name starts with an underscore as a Twig global. Globals are visible inside macros and inside an include marked only, where the record variables are not.

Calls made for their effect, such as _response.preventCaching() or _templateContext.set(), print nothing in either engine; in Twig the conventional form is {% do _response.preventCaching() %}. Values that must travel between templates, such as a heading level or a list of scripts to load, go through _templateContext: in Smarty and in Twig.

Go deeper

Was this page helpful?

Your answer goes to the docs team.