Skip to content
V
For developers
Architecture, conventions, the core and security
UI layer / Latte templates

Latte templates#

This chapter is about the PHP side of templates: what the system has added to Latte, how to write your own filter, and how snippets and signals hold together.

What belongs where

How a template is split into files, how the system picks a layout and what flows into a template from the presenter — that is in the section for front-end coders: Splitting a template into Latte files. Here you will find only what a developer adding something to templates from PHP needs.

Custom extensions#

app/Core/Latte/Extensions/ — thirteen classes, each adding filters, functions or tests:

Extension What it adds
Url working with addresses
Date date and time formatting
Price formatting of a price by currency (priceFormat), a VAT rate (vatRate) and a quantity (quantity)
Email, Phone contact formatting
String text adjustments (e.g. replacing blacklisted words)
FormPair rendering a label–field pair
SafeLink the safeLink() function — a link that does not fail on a missing target
Theme themeAsset(), themeUrl(), the isTheme() test
ApproxPrice the approxPrice filter (approximate conversion into the website currency), the currencyById() function
CanonicalUrl the canonical filter — the address for <link rel="canonical">
Outline the outline filter — the outline of a text page and heading anchors
UiTexts the uiTexts() function — a translation subtree for JavaScript

Plus three macros in app/Core/Latte/Macros/: MultiControlMacros, SafeLinkMacro and ThemeMacro.

Procedure: adding my own filter#

  1. Find out whether it belongs in an existing extension. Price formatting into PriceLatteExtension, text handling into StringLatteExtension.
  2. If not, create a new extension in app/Core/Latte/Extensions/, extends Latte\Extension, and implement getFilters() / getFunctions() / getTests().
  3. Write nothing into the configuration. The extension is found by itself: LatteExtensionsCompiler walks the Extensions/ and Macros/ folders with the robot loader, registers every instantiable class extending Latte\Extension and attaches it to LatteFactory.
  4. Put the logic in a helper, not in the closure's body — a filter should be a one-line redirect.
  5. Verify it in both sections if the filter is meant to work on the front end and in the administration.

Formatting belongs in a filter, not in a template

A currency condition in the middle of a template will appear in fifteen places within a year — and be forgotten in fourteen of them when it is fixed.

A new extension only takes effect after the container cache is cleared

Registration runs while the DI container is compiled. Until temp/cache is deleted the filter does not exist and Latte reports an unknown filter — which looks like a typo in the name.

Filters and functions with a catch#

For most filters the name is enough. With these six you can make a mistake that does not fail, it just quietly does something else.

Filter / function What it does Watch out for
canonical (filter) drops parameters that do not change the content (_theme, _skin, view, order, count, spec, lspec, tag, categoryId, do, _fid and every component parameter containing a hyphen) and completes a relative address with scheme and host page stays, page two is different content; parameters in the path (route) are not touched
outline (filter) returns ['html' => …, 'items' => […]]; adds an id to the headings (h2 by default, another tag as the second argument) print the content from $outline['html'], the anchors exist only in that copy; a heading without text is skipped, an id set by the editor is kept, equal anchors get -2, -3 (otherwise the second outline link would jump to the first section)
uiTexts() (function) a flat array relative.key => text for the JS helper i18n.js (t(key, params)) the key list comes from the default locale (cs), the translation from the current one; %name% is substituted by JS
approxPrice (filter) "≈ €4,100" in the website currency, or an empty string when the currencies match or the conversion is impossible the original is still printed by priceFormat; with a second argument true it adds parentheses and a non-breaking space, so write it right after the original without a space
quantity (filter) a quantity without trailing zeros: "5" from '5.000', "1.5" from '1.500', separator by the localization (of the document, else of the request) in value and data attributes write \|quantity:true (full stop); without it "1,5" would not pass in <input type=number>. The argument switches to the machine form, it is not a separator: that always comes from the localization
safeLink() (function) a link that never breaks the render; a target that cannot be built gives '#', a failed build writes one line to log/warning.log per target and request a '#' in a link signals bad data (typically a page-to-route binding), not a template bug
<link rel="canonical" href="{$canonicalUrl|canonical}">
{var $outline = ($contentHtml|outline)}
<script type="application/json" id="front-ui-i18n">{uiTexts('front.general.ui')}</script>
{$price|priceFormat:$currency}{$price|approxPrice:$currency, true}
{$item['quantity']|quantity} {$item['unit']}
{var $link = safeLink(':Front:System:Page:show', ['slug' => $slug])}

Without |canonical every filter combination is its own canonical

The canonical is built from link('this', getParameters()), i.e. from the whole request. Without the filter a search engine sees every combination of theme switcher, view, sorting and filters as a separate page and reports them as duplicates.

Why safeLink() switches invalidLinkMode

Presenter::link() does not throw a bad link as an exception: in debug mode it turns it into E_USER_WARNING. config/Shared/tracy.neon has strictMode on, so Tracy turns the warning into a bluescreen and ends the render in the middle of the page (catch does not catch a warning). A single bad page-to-route binding then kills every page on a dev server, while production silently returns '#'. That is why safeLink() forces Presenter::InvalidLinkException for the duration of the call and restores the original mode in finally. Your own helper for links that must not bring a page down should restore the mode the same way.

Do not add JS texts as data-* attributes

Each front-end template has its own copy of the layout. A new text for a JS component therefore belongs in the front.general.ui subtree in .neon; uiTexts() hands it to all templates at once. A key missing in en or sk is translated by fallback and shows up as missing in the Tracy panel.

Snippets and their PHP counterpart#

A snippet's name is also the argument to redrawControl(). The convention: a short lowerCamelCase name, Wrapper shortened to Wrap, and the wrapping area named after the inner snippet plus Area.

{snippetArea linksSnippetArea}
    {snippet linksSnippet}
        …
    {/snippet}
{/snippetArea}
$this->redrawControl('linksSnippet');

A snippet's name must be a LITERAL

A name composed from a variable is not registered and redrawControl() then silently does nothing. No error is printed — the page simply is not redrawn.

AJAX redraws only the INSIDE of a snippet

The attributes of the element carrying the snippet stay stale — the class and any data- attribute alike. When a redraw is supposed to change a state expressed by a class, that class has to be inside.

{var} inside a {snippet} has its own scope

A variable defined outside the snippet may not exist inside it after a redraw — during an AJAX redraw only that snippet is rendered, not the template around it.

Redrawing blindly is not the way

redrawControl() without an argument redraws everything. Always name the target — otherwise snippets that changed for an unrelated reason end up in the response too.

In detail: Components and Code conventions.

Translations in a template#

Three different behaviours depending on where you are — a Front presenter needs {translator $translatorDomain}, an Admin presenter and components do not. It is the most common source of "why did it not translate" and has a chapter of its own: Translations.

A missing translation in a route can bring down a whole language version

Not an empty space — a page error. It concerns the texts an address is composed from, not labels. In detail: Translations, the section on the database layer.

Reserved variable names#

Some names are taken by the template system

$form, $flashes, $control, $presenter, $lang, $locale, $theme and others. Overwriting one surfaces not as an error but as strange behaviour — typically as something entirely different breaking on the same page.

Debug mode and AJAX#

A deprecated call in debug mode breaks the AJAX response

Tracy adds output to the response that breaks the JSON. It surfaces as "AJAX does not work" — with no warning and no console error, because the response simply cannot be parsed.

Where to look#

I want Where
the extensions and their filters app/Core/Latte/Extensions/
the macros app/Core/Latte/Macros/
the canonical filter and the list of dropped parameters app/Core/Latte/Extensions/CanonicalUrlLatteExtension.php (DROP)
the outline of a text page app/Core/Utils/Helpers/HtmlOutline.php
the automatic registration app/Core/DI/LatteExtensionsCompiler.php
the engine settings config/Shared/latte.neon
the structure of templates and layouts Splitting a template into Latte files

Follow-up chapters: Components · Forms · Translations · Front-end themes

Cookie settings