Getting Started¶
Installation¶
- Install the Twig transport package through the MODX package manager.
- The installer creates a
twignamespace pointing to{core_path}components/twig/. - The
twigparserservice is now available site-wide.
No other configuration is needed. The extra activates automatically.
This extra uses Twig 3 (the Twig 3.x documentation is the right reference for syntax, filters, and functions).
Where Twig Syntax Works¶
Twig syntax is processed in:
- Templates - your MODX page templates
- Chunks - both when called via
[[$ChunkName]]and via thechunk()Twig function - Resource content - the content field of any resource
- Snippet output - if a snippet returns a string containing Twig syntax, it gets rendered
- ContentBlocks fields - when the ContentBlocks plugin is enabled (see the ContentBlocks guide)
Twig is not processed in the MODX manager interface.
Built-in Functions¶
The extra provides functions that bridge Twig templates to MODX features.
chunk(name, properties)¶
Renders a MODX chunk. Properties are passed as chunk placeholders.
The chunk itself can also contain Twig syntax.
snippet(name, properties)¶
Runs a MODX snippet and outputs the result.
placeholder(key, default) / ph(key, default)¶
Reads a MODX placeholder. Returns the default if the placeholder is not set.
option(key, default) / config(key, default)¶
Reads a MODX system setting.
lexicon(key, params, language)¶
Returns a lexicon string. The lexicon topic must already be loaded.
trans(key, topic, params, language)¶
Loads a lexicon topic and translates in one call.
link(id, params, context, scheme, options)¶
Generates a URL for a MODX resource.
field(name, default, resource)¶
Reads a resource field or Template Variable from the current resource. Falls back to the default if the field is empty. For accessing fields on the current resource, prefer the resource global instead.
{# Preferred: use the resource global #}
{{ resource.pagetitle }}
{{ resource.HeroImage }}
{# field() is useful for defaults and cross-resource lookups #}
{{ field('HeroImage', '/images/fallback.jpg') }}
{{ field({'name': 'CustomTV', 'default': 'none', 'resource': 42}) }}
Global Variables¶
Four globals are available in every Twig template:
resource¶
The current MODX resource. Access built-in fields and Template Variables as properties:
{{ resource.pagetitle }}
{{ resource.alias }}
{{ resource.id }}
{{ resource.parent }}
{{ resource.content|raw }}
Template Variables work the same way -- no need for a separate function call:
{{ resource.HeroImage }}
{{ resource.CustomField }}
{% if resource.ShowBanner %}
<div class="banner">{{ resource.BannerText }}</div>
{% endif %}
TV values are processed/rendered by default (same as [[*myTv]]). If you need the raw stored value before MODX applies output rendering:
In CLI, API, or manager contexts where no resource is loaded, resource is null. Guard against this if your template may run in those contexts:
modx¶
The MODX instance. Use resource for accessing resource fields. The modx global is available for other MODX features:
Use modx sparingly. The helper functions and resource global are usually clearer.
placeholders¶
An array of all currently set MODX placeholders.
Twig Filters¶
All standard Twig filters work. Some commonly useful ones:
{{ title|upper }}
{{ title|lower }}
{{ title|capitalize }}
{{ description|default('No description available') }}
{{ content|raw }}
{{ price|number_format(2, '.', ',') }}
{{ items|length }}
{{ html_content|striptags }}
{{ name|trim }}
{{ list|join(', ') }}
{{ date_string|date('d/m/Y') }}
HTML Escaping¶
Twig auto-escapes all output by default. This is a security feature that prevents XSS attacks, but it means HTML content comes out as visible tags if you are not expecting it.
{# Variable contains: <p>Hello <strong>world</strong></p> #}
{{ value }} {# outputs: <p>Hello <strong>world</strong></p> #}
{{ value|raw }} {# outputs: <p>Hello <strong>world</strong></p> #}
Use the |raw filter when you know the content is safe HTML that should be rendered as markup. This is common with:
- richtext field content from ContentBlocks
- chunk output that contains HTML
- snippet output that returns HTML
- MODX resource fields like
contentorintrotext
Do not use |raw on user-supplied input that has not been sanitised.
When in doubt, leave auto-escaping on. Only add |raw when you see escaped HTML appearing as text on the page.
Twig Control Structures¶
Conditionals¶
{% if field('HeroImage') %}
<img src="{{ field('HeroImage') }}" alt="{{ field('pagetitle') }}">
{% else %}
<div class="placeholder">No image</div>
{% endif %}
Loops¶
{% set items = ['Home', 'About', 'Contact'] %}
<nav>
{% for item in items %}
<a href="#">{{ item }}</a>
{% endfor %}
</nav>
Setting Variables¶
{% set site = option('site_name') %}
{% set year = 'now'|date('Y') %}
<footer>© {{ year }} {{ site }}</footer>
Mixing MODX Tags and Twig¶
Twig and MODX tags work together in the same template. Twig runs first, then MODX processes its tags in the output.
{# Twig handles the logic #}
{% if resource.parent == 5 %}
<nav>[[!SiteNav? &startId=`5`]]</nav>
{% endif %}
{# MODX handles the content #}
<h1>[[*pagetitle]]</h1>
<div>[[*content]]</div>
You can also call MODX elements through the Twig functions instead of tags:
{% if resource.parent == 5 %}
<nav>{{ snippet('SiteNav', {'startId': 5}) }}</nav>
{% endif %}
<h1>{{ resource.pagetitle }}</h1>
Both approaches work. Choose whichever is clearer for your template.
Processing order¶
Understanding the order of operations prevents surprises:
- Twig runs first. All
{{ }},{% %}, and{# #}blocks are evaluated and replaced with their output. - MODX runs second. The result from step 1 is then processed by the MODX parser, which handles
[[tags]]. - Fenom runs last (only if pdoTools is installed and the
pdotools_fenom_parsersetting is enabled). Any{$var}or other Fenom syntax in the output is processed after both Twig and MODX tags have been resolved.
This means:
- Twig can wrap MODX tags in conditionals. The MODX tag is only present in the output if the condition is true, so MODX only processes it when needed.
- Twig cannot read MODX tag output. By the time MODX processes
[[*pagetitle]], Twig has already finished. You cannot use{% if [[*pagetitle]] == 'Home' %}-- use{% if field('pagetitle') == 'Home' %}instead. - MODX snippet output containing Twig is rendered. If a snippet returns a string with
{{ }}syntax, it goes through the Twig pass. This is intentional and useful for snippets that return Twig-powered markup. - Twig variables cannot be interpolated into MODX tags.
[[*{{ fieldname }}]]does not work because Twig processes{{ fieldname }}first, but the result is just text that gets concatenated into the MODX tag string. Use thefield()helper instead.
JavaScript frameworks and verbatim¶
Vue, Angular, Alpine.js, Handlebars, and other frontend frameworks also use {{ }} syntax. Twig will try to parse those expressions and throw an error.
Wrap frontend template blocks in {% verbatim %} to tell Twig to leave them alone:
{% verbatim %}
<div id="app">
<p>{{ message }}</p>
<span v-if="show">{{ count }} items</span>
</div>
{% endverbatim %}
Everything inside {% verbatim %}...{% endverbatim %} is output as-is without Twig processing.
Custom Twig Functions¶
Initializers¶
Register a function directly on the Twig environment:
$twigParser = $modx->services->get('twigparser');
$twigParser->registerInitializer(function (\Twig\Environment $twig) {
$twig->addFunction(new \Twig\TwigFunction('price', fn ($cents) => number_format($cents / 100, 2)));
});
Then in your template:
Extensions¶
Register a full Twig extension class:
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class PriceExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('price', fn ($cents) => number_format($cents / 100, 2)),
new TwigFunction('vat', fn ($cents, $rate = 0.2) => number_format($cents * $rate / 100, 2)),
];
}
}
$twigParser = $modx->services->get('twigparser');
$twigParser->registerExtension(new PriceExtension());
Shared Runtime¶
If your extension needs to call MODX features (chunks, snippets, URLs), use the shared runtime instead of reimplementing MODX calls:
use Boffinate\Twig\Support\ModxRuntime;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class CardsExtension extends AbstractExtension
{
public function __construct(private ModxRuntime $runtime) {}
public function getFunctions(): array
{
return [
new TwigFunction('card', fn (string $title) =>
$this->runtime->chunk('CardTpl', ['title' => $title])
),
new TwigFunction('card_url', fn (int $id) =>
$this->runtime->link($id)
),
];
}
}
$twigParser = $modx->services->get('twigparser');
$twigParser->registerExtension(new CardsExtension($twigParser->getRuntime()));
The runtime provides: chunk(), snippet(), placeholder(), option(), lexicon(), translate(), link(), field().
OnTwigInit Event¶
MODX plugins can listen to the OnTwigInit system event to register functions or globals when the Twig environment starts up:
// Plugin code listening to OnTwigInit
$twig->addFunction(new \Twig\TwigFunction('build_id', fn () => 'v2.1'));
$twig->addGlobal('release', '2026.03');
return '';
The event receives $twig (the Twig Environment), $parser (the Twig parser instance), and $modx.
Caching¶
Compiled Twig templates are cached under {core_cache_path}/twig/. When you clear the MODX cache, the Twig cache is also cleared automatically by the TwigCacheClear plugin.
If template changes do not appear after editing, clear the MODX cache. Make sure the TwigCacheClear plugin is enabled.
Debugging with dump()¶
The Twig debug extension is controlled by the twig.debug system setting (enabled by default). The dump() function outputs a variable inspection, or the template's own variables when called with no arguments. When Symfony VarDumper is installed (it is included as a dev dependency), you get interactive, collapsible HTML output rendered inside an iframe, instead of plain var_dump.
Dump a single variable¶
Dump everything available in the template¶
With no arguments, dump() shows every variable that was passed to the current template. This is the quickest way to find out what data you have to work with.
Globals (modx, resource, placeholders) are excluded from no-arg dump() output because they are always present and would obscure the template-specific data you are looking for. To inspect a global, dump it explicitly:
What you see when dumping modx¶
dump(modx) shows all properties of the MODX instance, but only the ones useful for template work are expandable:
config-- all system settings (expandable)context-- the current context object (expandable)resource-- the current resource (expandable)request-- the modRequest object (expandable)response-- the modResponse object (expandable)user-- the current user (expandable)placeholders-- all set placeholders (expandable)version,cultureKey,resourceIdentifier,resourceMethod,site_id,uuid-- scalar metadata (always visible)
Framework internals (pdo, driver, cacheManager, classMap, services, etc.) are still listed but shown as collapsed stubs -- you can see their type but cannot expand them. This keeps the dump readable and prevents memory issues from serialising the entire MODX runtime.
Block form¶
The block form writes output to the Symfony dump collector if available, but in a MODX context it works the same as the function form:
Output size limits¶
Two safety measures prevent memory exhaustion from large dumps:
- Per-dump limit (2 MB): If a single
dump()call produces more than 2 MB of HTML, it is replaced with a truncation message. Dump specific variables instead of the full context to stay under this limit. - Per-render limit (5 MB): If a single
renderString()call produces more than 5 MB of total output, Twig discards the result and returns the original template. Check the MODX error log for details when this happens.
Fenom compatibility¶
When pdoTools Fenom is enabled, dump output is rendered inside an <iframe> with srcdoc to isolate VarDumper's JavaScript and CSS from Fenom's { parser. This happens automatically -- no configuration needed. The iframe auto-resizes as you expand and collapse dump nodes.
Remove dump() before going live¶
dump() only works when twig.debug is enabled (it is by default). Set twig.debug to false in production -- dump() calls will silently return nothing, but it is still good practice to remove them from production templates.