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#
- Find out whether it belongs in an existing extension. Price formatting into
PriceLatteExtension, text handling intoStringLatteExtension. - If not, create a new extension in
app/Core/Latte/Extensions/,extends Latte\Extension, and implementgetFilters()/getFunctions()/getTests(). - Write nothing into the configuration. The extension is found by itself:
LatteExtensionsCompilerwalks theExtensions/andMacros/folders with the robot loader, registers every instantiable class extendingLatte\Extensionand attaches it toLatteFactory. - Put the logic in a helper, not in the closure's body — a filter should be a one-line redirect.
- 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.
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