concretecms icon indicating copy to clipboard operation
concretecms copied to clipboard

Draft: Twig support

Open KorvinSzanto opened this issue 7 months ago • 7 comments

This PR attempts to add support for Twig templates while maintaining backward compatibility by modifying \Concrete\Core\Filesystem\FileLocator to optionally check for additional file extensions. Doing it this way, we can override a concrete/blocks/foo/view.php with a application/blocks/foo/view.html.twig and vice-versa.

Why?

Twig templates provide some strong benefits over our existing PHP templates:

  1. Output is sanitized by default. Rather than having to opt-in to safety with h($something), Twig has us opt-out with {{ something | raw }}. This makes it a lot easier to review code changes for XSS because you'll pretty much never opt-out unless you're specifically outputting known HTML
  2. Template files are distinguishable from class / php files. Today the PHP tooling ecosystem requires us to use different tools (or at least different configuration) for template files than we do for class / pure PHP files due to the mixed HTML being confusing. This leads to well-tested projects needing to manually list out the names of files that get one configuration vs files that get another. By using twig templates, we eliminate mixed php and html from the codebase and the .html.twig extension makes it obvious which files are intended to be templates and which files are not #11853
  3. Template designers know twig Using a more familiar paradigm like Twig allows symfony / drupal / python designers to work on templates for Concrete without needing to know or understand PHP. Front end developers / designers can build and iterate template files while back end / php developers focus on the back-end functionality.

Why Twig?

Feature Twig Blade Latte Plates
Standalone
Extensible
PHP 7.3
Secure by default
No dependencies
Well-established 1 2
  1. While Blade is well-established, its syntax isn't used anywhere other than within the Laravel ecosystem
  2. Latte has been around for a long time, but it also has unicorn syntax and isn't used much outside of nette

Overview

This PR adds template support with two main changes:

  1. Template support in FileLocator
    • Relevant FileLocator methods, and methods that exist solely to proxy to aforementione FileLocator methods have been updated to support a new $template = false flag. Passing true here tells the FileLocator that we're loading a template and not something like db.xml or controller.php or page_theme.php which allows the FileLocator to search for additional file extensions other than just .php.
  2. \Concrete\Core\Filesystem\TemplateService now is in charge of rendering templates.
    • Anywhere we're currently doing extract($scopeItems); include $file; to render templates, we want to start using TemplateService->renderTemplate($file, $scopeItems).
    • ->renderTemplate will automatically render Twig templates using Twig and .php templates in the current backwards compatible way.
    • ->renderTemplate also includes a $bindTo = null argument that allows binding the $this variable properly when including templates for backwards compatibility. For example: $service->renderTemplate($file, $scopeItems, $this);

Additionally, this PR includes some tooling to support configuring the Twig environment used.

  • app.twig config key
    • app.twig.extensions array<string, \Twig\Extension\ExtensionInterface|class-string<\Twig\Extension\ExtensionInterface>> is an array of \Twig\Extension\ExtensionInterface class strings or instances
    • app.twig.debug bool
  • \Core\Filesystem\TwigFactory handles creating Twig environments and all the wiring required.

Using Twig templates

In order to use a twig template, name your template file your_template.html.twig instead of your_template.php. Concrete's override detection should automatically identify the .html.twig file the same way it would the .php file and render it with Twig.

An example Twig default.html.twig for a theme might look like this:

{# Extend our skeleton #}
{% extends 'templates/skeleton.html.twig' %}

{% block content %}
    <main class='main-content'>
        {{ Area('Main') | raw }}
    </main>

    <footer>
        {% for i in 1..4 %}
            <div class='col-3'>
                {{ Area('Footer ' ~ i) }}
            </div>
        {% endfor %}
{% endblock %}

And an example block custom template wrapped_content.html.twig for the content block might look like this:

<div class="border-top border-primary border-5 pt-5 ccm-block-content">
    {% if not content and c.isEditMode() %}
        <div class="ccm-block-content">{{ t('Empty Content Block.') }}</div>
    {% else %}
        {{ content | raw }}
    {% endif %}
</div>

Extending Twig

Global extensions

If you'd like to add your extension to every Twig instance you can either add it to your app.php config file, or extend it using the Concrete App instance

With config

// /application/config/app.php
return [
    'twig' => [
        'extensions' => [
            'my_extension' => \My\Namespace\MyExtension::class
        ]
    ]
];

With Concrete App Instance

// In /application/bootstrap/app.php, or an on_start, or a service provider
use Concrete\Core\Filesystem\TwigFactory;
$app->extend(TwigFactory::class, function (TwigFactory $factory): TwigFactory {
    $factory->addExtension(new \My\Namespace\MyExtension());

    // You can also add global variables this way
    $factory->addGlobal('someGlobal', $someGlobal);

    return $factory
})

TODO

Support twig anywhere we support php templates today (Or enough for a v1)

  • [x] Block view support
  • [x] Block template support
  • [x] Theme view support
  • [x] Element support
  • [x] Single page support
  • [x] Container view support
  • [x] Board views
  • [ ] Board slots rendering properly
  • [ ] Auth type views
  • [ ] Permission views
  • [ ] Attribute views

Core twig extension needs minimal functionality

  • [x] t() Internationalization support is a requirement
  • [x] activeLocale() and activeLanguage() Mapping to Localization static methods of the same name
  • [x] h() (Even though it's not typically needed, we might need to do t('foo <b>%s</b>', h(someVar)))
  • [x] app() This is a bit of a slippery slope, but I think locking down twig templates at this stage would make adoption challenging
  • [x] include() / require() We want to still be able to include PHP in Twig at least in the beginning. There are examples in the atomik theme where block templates rely on the ability to load in PHP files from the core.
  • [x] config() This is already available with app('config') but I like having short functions for the things people might do often
  • [x] url() Maps to ResolverManager->resolve()
  • [x] Area() GlobalArea() Stack() ContainerArea() These currently map to new FluentArea(new $type). FluentArea enables chaining calls and simplifies ->display()
  • [x] element() For including elements
  • [x] getCurrentPage()
  • [x] pageByID()
  • [x] fileByID()
  • [x] authTypeGetList()
  • [x] authTypeByHandle()
  • [x] authTypeByID()
  • [x] foo | bufferMethod('methodName', [arg1, arg2], thisValue) for ob_start() output buffering of things that otherwise would output prematurely
  • [x] foo | preg_replace(regex, replace) for better replacement

Planning

  • [ ] Identify what needs to happen for translations to work happily https://github.com/concrete5-community/translation-library/issues/21
  • [ ] Identify all remaining views that need support
  • [ ] Identify necessary v1 extension functionality. Look at latte and blade for inspiration for necessary functions/filters/tags
  • [ ] Identify what else is left to do

KorvinSzanto avatar Mar 19 '25 02:03 KorvinSzanto