How We Prevent Leaky Templates in Craft CMS

Trevor Davis, Former Front-End Development Technical Director

Article Categories: #Code, #Front-end Engineering, #Back-end Engineering

Posted on

Prevent the flood of leaky templates in Craft CMS with just a little bit of PHP.

Way back in September 2019 (which feels like a lifetime ago, but what even is time anymore), we came across an article about how to prevent leaky templates with Craft CMS.

What is a leaky template?

A leaky template is a template that is using a variable from the parent template that isn't explicitly passed to the child template.

{# parent.html #}
{% set greeting = 'Hello' %}
{% set name = 'world' %}

{% include 'child' with {
    name: name,
} %}

{# child.html #}
{{ greeting }} {{ name }}

{# outputs #}
Hello world

In that example, it really doesn’t seem that bad, but once you get into really complex templates and have to track down where variables are being set, it’s real bad, just trust me.

We were already pretty good about using the only parameter when including templates, but this does get super tedious to write:

{% include '_blocks/' ~ block.type ignore missing with {
    block: block,
} only %}

We’ve tried the macro/component approach before and that feels pretty good, but macros are such a weird aspect of Twig, and you have to write a macro for every component. Are they functions? Are they includes? Some strange hybrid.

So I set out to write something new with PHP that would streamline this. What I ended up with was a partial() Twig function. With my new function, I would write the code above like:

{{ partial('_blocks/' ~ block.type, {
    block: block,
}) }}

That feels a lot nicer! Not only is it shorter to write and accomplish the same functionality, but it also forces you to pass the variables you need in the child template every time. Leaky templates are no longer possible since the child template doesn’t have access to any of the variables not directly passed. This approach meant adding a new Twig Extension to my module.

<?php

namespace viget\base\twigextensions;

use Craft;
use Twig\TwigFunction;
use Twig\Extension\AbstractExtension;
use viget\base\services\PartialLoader;

/**
 * Custom Twig Extensions
 */
class Extension extends AbstractExtension
{
    /**
     * Register Twig functions
     *
     * @return array
     */
    public function getFunctions()
    {
        return [
            new TwigFunction(
                'partial',
                [PartialLoader::class, 'load'],
                ['is_safe' => ['html']]
            ),
        ];
    }
}

The Extension references my PartialLoader service, which I’ve broken out to a separate service so it’s more portable and easier to write an automated test for.

<?php

namespace viget\base\services;

use Craft;

/**
 * Shortcut for including a file with only the variables passed and ignore missing
 * Same as {% include 'path/file' ignore missing with { key: 'value' } only %}
 */
class PartialLoader
{
    /**
     * Load a template
     *
     * @param string $template
     * @param array $variables
     * @return string
     */
    public static function load(string $template, array $variables = []): string
    {
        // If the template can't be found, log it, and return nothing
        if (!Craft::$app->view->doesTemplateExist($template)) {
            Craft::error(
                "Error locating template: {$template}",
                __METHOD__
            );

            return '';
        }

        return Craft::$app->view->renderTemplate($template, $variables);
    }
}

As we continue to built more sites with Craft, we are trying to let Twig be a templating language and to shift more of the code that would be really complex in Twig to PHP modules.

We started to see that we are reusing module functionality from one site to the next, so we’ve been working on setting up a base module that we include on every Craft site we build. There’s some more neat stuff coming down the line, so keep an eye out.

Related Articles