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

Response models

A template never holds a record itself: it holds the record's response model, which lets through only the get, has and is methods its class lists. The list is the template's API in both engines. A real one, what each engine does with a call that is not on it, how to see the list, and who can add to it.

Who this is for. The theme developer who has seen a method in the API, in an old template or in the platform's code and wants to know whether a template can call it. Afterwards you can check any call against the record's list before you write it, and recognise the failure when you did not.

The object a template receives

Before rendering, the platform wraps every record it hands to a template in a response model: a page becomes a Pages\Models\Frontend\Response\Page, a blog post a Blogs\Models\Frontend\Response\Post, and so on. The same happens to records a template gets back from a call, such as the children of a page or the records _query finds. The wrapping is done before either engine sees anything, so the callable surface of a record is identical in Smarty and in Twig.

The class's method list is the allow-list

A response model has almost no methods of its own. Its class comment lists, with @method lines, the methods of the record a template may reach. An extract from the page's:

PHP Pages\Models\Frontend\Response\Page (extract)
/**
 * @method string|null getName() Get the title of the page
 * @method string getFullPath() Gets the relative path that addresses this record within the site, ...
 * @method \Coredna\Kernel\Models\Response\ResponseModelsCollection|null getChildren() Child pages ...
 * @method bool isPublished() Determines if the record is published.
 * @method array getOtherLanguageVersions() Fetch an associative array with other language versions ...
 * @method bool hasAreaWithName(string $areaName) Check if an area with the specified name exists ...
 * @method string|null getAreaText(string $areaName) Get the text for the layout area ...
 * @method \Coredna\Kernel\Models\Response\ResponseModelsCollection getAllParents() Get all parents ...
 */
class Page extends ResponseModel

The rule a call has to pass:

  • The name starts with get, has or is, and
  • the class lists it with @method, or defines it itself.

Everything else is refused, even when the record's own class has the method. Setters are never on the list, and assigning a property on a record fails with Setting values is not allowed in response models. A method's result is wrapped the same way: getParent() on a page returns the parent's response model, with the parent's list.

Property-style access goes through the same list. page.name in Twig and $page->name in Smarty call getName() when the list has it, and otherwise read the layout area called name. The area shorthand covers that second step and its limits.

What a call that is not on the list does

You writeSmartyTwig
A method the list does not have, or a misspelt one: getNmae()An error: Method getNmae does not exist in Pages\Models\Frontend\Response\PageEmpty output, no error
A property that is neither a listed getter nor a layout area: nmaeAn error: Property nmae does not exist in Pages\Models\Frontend\Response\PageEmpty output, no error
Array syntax on a record: $page.nameA fatal error: Cannot use object of type … as arraypage.name is the normal property form and works

The Smarty error names the method and the class, so it is the quickest of the failures to fix: look the name up in the class's list. In Twig, a call the record's class does not list prints nothing, with no error, because Twig runs without strict variables here. A misspelt method, a method from another model and a blank value all look the same on the page, so when a value is unexpectedly empty in Twig, check the method against the list before you suspect the content.

In Twig, page.anything is defined is always true for a record, because a response model accepts every call and decides afterwards. Test the value (is not empty) or, for a layout area, hasAreaWithName().

Seeing the list

Every record's list is published in the generated Reference: the Templates section of each model page, such as Page, is the response model a template receives. The Reference is generated from the platform itself, so it matches the release the site runs.

A template can also ask the record. getModelMethods() returns the names on its list, and hasModelMethod(name) says whether one name is there:

Template code
{* while developing: every method this record allows *}
<!-- {implode(', ', $page->getModelMethods())} -->

{* in a partial that serves several record types *}
{if $record->hasModelMethod('getAllParents')}
  {foreach $record->getAllParents() as $parent}
    <a href="{$parent->getFullPath()}">{$parent->getName()|escape}</a> /
  {/foreach}
{/if}
{# while developing: every method this record allows #}
<!-- {{ page.getModelMethods()|join(', ') }} -->

{# in a partial that serves several record types #}
{% if record.hasModelMethod('getAllParents') %}
  {% for parent in record.getAllParents() %}
    <a href="{{ parent.getFullPath() }}">{{ parent.getName()|e }}</a> /
  {% endfor %}
{% endif %}

Remove the comment once you have your answer: it prints the list into the page for every visitor.

Lists differ between record types

Do not assume a method exists because a similar record has it:

  • A page has getChildren(); a book has getUnits() and a unit getPages() instead.
  • getFullPath() returns the record's custom URL when it has one, and an empty string when no path can be built: treat empty as “no link”.
  • Most content records have the area getters (getAreaText(), getAreaHtml(), getAreaRecord() and the rest), but which one fits depends on the layout area's input type: Area getters, by return type.
  • Printing a record, {$page} or {{ page }}, renders it for most content records: pages, blogs and posts, books, units and book pages, FAQ pages and questions, products with their descriptions, variants and categories, custom entity records and forms come out as their whole layout, an image as an <img> tag and a file as its path. Other records print their display name.

Who adds to the list

Not the site. The lists are part of the platform: they are generated from the public get, has and is methods of each platform model when the platform is built, minus any the model withholds, and a theme repository cannot change them. When a template needs a value the record does not offer, the routes are:

  1. Look again in the Reference, under a related record: the value is often one call away, through getActiveRevision(), a layout area or a relation.
  2. Read it another way: a layout area or custom field the editors fill, a helper such as _query, or the API from the browser.
  3. Ask for a platform change, naming the record, the method and what the page needs it for. A new getter on the platform's model appears on the list when the response models are regenerated.
Go deeper

Was this page helpful?

Your answer goes to the docs team.