Twig

aus der Entwicklerperspektive

Die heutige Expedition wird geleitet von…

  • Name: Moritz Vondano
  • GitHub: @m-vo
  • #Contao Core
    #Twig für Contao

Tal der unzähligen Anforderungen

Packen & Los geht's

01
© Rasmus Lerdorf | 25 years of PHP | 2019

                <html>
                    <title><?= $this->title ?></title>
                    <body>
                        <main id="main">
                          <div class="inside">
                            <?= $this->main ?>
                          </div>
                            <?php $this->sections('main'); ?>
                        </main>
                    </body>
                </html>
            
Neue Anforderungen,
neue Möglichkeiten.
… eine schöne Syntax🗸

                    Glückwunsch, {{ destination }} ist unser schönstes Ziel!
                
… eine schöne und mächtige Syntax🗸

                    Glückwunsch, {{ destination }} ist unser schönstes Ziel!

                    Du benötigst dafür:
                    {% for thing in packing_list %}
                     - {{ thing }}
                    {% endfor %}
                
… eine schöne und mächtige Syntax🗸

                    {# Psst, this is a lie! #}
                    Glückwunsch, {{ destination }} ist unser schönstes Ziel!
                    Schwierigkeitsgrad: {{ (stats.deaths_per_year / 3)|round }}.

                    {% if date(last_incident) < date('-3days') %}
                       * Diese Route gilt als sehr sicher! *
                    {% endif %}

                    Du benötigst dafür:
                    {% for thing in packing_list %}
                     - {{ thing }}
                    {% endfor %}
                
mehr mehr mächtig!🗸

                    {% extends "expedition_list.html.twig" %}

                    {% block info %}
                        {# Psst, this is a lie! #}
                        Glückwunsch, {{ destination }} ist unser schönstes Ziel!
                        Schwierigkeitsgrad: {{ (stats.deaths_per_year / 3)|round }}.

                        {{ parent() }}

                        {% if date(stats.last_incident) < date('-3days') %}
                           * Diese Route gilt als sehr sicher! *
                        {% endif %}
                    {% endblock %}
                
… doch nicht sooo mächtig 😳

                    {% block user_info %}
                        {% eval %}
                            $db = new mysqli('localhost', 'admin', 's3cr3t');
                            $user = $db->query(
                                "SELECT name, lastname
                                 FROM users
                                 WHERE id=' ~ {{ user_id }} ~ '"
                            );
                        {% endeval %}

                        […]

                    {% endblock %}
                
… Sicherheit🗸
red
green
blue
XSS alert(1)
alert(1)

                    <?php /* boxes.html5 */ ?>

                    <style>
                      .box {
                        background: <?php echo $this->color; ?>
                      }
                    </style>

                    <div class="box"><?php echo $this->color; ?></div>
                

                    {# boxes.html.twig #}

                    <style>
                      .box {
                        background: {{ color|escape('css') }}
                      }
                    </style>

                    <div class="box">{{ color|escape('html') }}</div>
                

                    {# boxes.html.twig #}

                    <style>
                      .box {
                        background: {{ color|escape('css') }}
                      }
                    </style>

                    <div class="box">{{ color }}</div>
                
autoescape
… Erweiterbarkeit🗸

Highlights auf deiner Route:

  • Quelle des Glücks
  • ️Gipfel der Vernunft 🏔
  • Wald vor Bäumen 🌳
Boring!
Nice!

                    {# track_description.md.twig #}

                    Highlights auf deiner Route:

                    {% for highlight in track.highlights %}
                      * {{ highlight }}
                    {% endfor %}
                

                    {# track_description.md.twig #}

                    Highlights auf deiner Route:

                    {% for highlight in track.highlights %}
                      * {{ highlight|emojify }}
                    {% endfor %}
                
… Performance🗸

Abstieg in die Tiefen

Höhlen der Compiler-Maschinerie

02

Compiler-Workflow

  1. Lexen/Parsen
  2. AST bearbeiten
  3. Code generieren (compilen)

                        Hallo {{ firstname }},
                        hier ist deine Route:

                        {% for waypoint in route %}
                          <br> {{ waypoint }}
                        {% endfor %}
                    
TEXT_TYPE(Hallo ) VAR_START_TYPE() NAME_TYPE(firstname) VAR_END_TYPE() TEXT_TYPE(, hier ist deine Route:) BLOCK_START_TYPE() NAME_TYPE(for) NAME_TYPE(waypoint) OPERATOR_TYPE(in) NAME_TYPE(route) BLOCK_END_TYPE() TEXT_TYPE( <br> ) VAR_START_TYPE() NAME_TYPE(waypoint) VAR_END_TYPE() BLOCK_START_TYPE() NAME_TYPE(endfor) BLOCK_END_TYPE() EOF_TYPE()
ModuleNode
  ├──TextNode('Hallo')
  ├──PrintNode
  │   └──NameExpression('firstname')
  ├──TextNode(', hier ist deine Route:')
  └──ForNode
      ├──Node
      │   ├──value_target: AssignNameExpression('waypoint')
      │   ├──seq:          NameExpression('route')
      │   └──body:
      │      ├──TextNode(' <br> ')
      │      └──PrintNode
      │          └──NameExpression('waypoint')
      └──ForLoopNode
                    
ModuleNode
  ├──TextNode('Hallo')
  ├──PrintNode
  │   └──FilterExpression
  │      ├──node:     NameExpression('firstname')
  │      ├──filter:   ConstantExpression('escape')
  │      └──arguments:[ConstantExpression('html')]
  ├──TextNode(', hier ist deine Route:')
  └──ForNode
      ├──Node
      │   ├──value_target: AssignNameExpression('waypoint')
      │   ├──seq:          NameExpression('route')
      │   └──body:
      │      ├──TextNode(' <br> ')
      │      └──PrintNode
      │          └──FilterExpression
      │              ├──node:     NameExpression('waypoint')
      │              ├──filter:   ConstantExpression('escape')
      │              └──arguments:[ConstantExpression('html')]
      └──ForLoopNode
                    

                        <?php

                        class __TwigTemplate_bbb7da3f704a85b7f7a47f925a470c9b extends Template
                        {
                            protected function doDisplay(array $context)
                            {
                                echo "Hallo ";
                                echo $this->env->getFilter('escape')->getCallable()(
                                    $this->env, $context["firstname"], "html"
                                );
                                echo ", hier ist deine Route:";

                                $context['_parent'] = $context;
                                $context['_seq'] = $context["route"];
                                foreach ($context['_seq'] as $context["waypoint"]) {
                                    echo " <br> ";
                                    echo $this->env->getFilter('escape')->getCallable()(
                                        $this->env, $context["waypoint"], "html"
                                    );
                                }
                                $_parent = $context['_parent'];
                            }
                        }
                    
EscaperNodeVisitor
SafeAnalysisNodeVisitor
OptimizerNodeVisitor
PhpTemplateProxyNodeVisitor
ContaoEscaperNodeVisitor

Etappe ins Zentrum

Symfony-Land

03

Twig in Symfony 😊 🙃 😯 😬

  • Templates aus dem Filesystem (FilesystemLoader), organisiert in Namespaces (@…)
    • Bundles ➡️ @NatureBundle/mountain/trail.html.twig
    • App ➡️ @__main__/bucket_list.html.twig
  • Logische Namen sind eindeutig
    $loader->exists()
  • Templates lassen sich in der App überschreiben
    • templates/bundles/<Bundle> ➡️ @<Bundle>/…

                    {# templates/bundles/NatureBundle/mountain/trail.html.twig #}

                    <div class="trail-map" data-map>
                        <script type="application/json">
                            {{ trail_data|json_encode }}
                        </script>
                    </div>

                    {# todo: re-add the original stuff here #}
                

                    {# templates/bundles/NatureBundle/mountain/trail.html.twig #}
                    {% extends "@NatureBundle/mountain/trail.html.twig" %}

                    {% block main %}
                        {{ parent() }}

                        <div class="trail-map" data-map>
                            <script type="application/json">
                                {{ trail_data|json_encode }}
                            </script>
                        </div>
                    {% endblock %}
                

Twig in Symfony 🫣

  • Templates aus dem Filesystem
    • Bundles ➡️ @NatureBundle/mountain/trail.html.twig
    • App ➡️ @__main__/bucket_list.html.twig
  • Logische Namen sind eindeutig
  • Templates lassen sich in der App überschreiben
    • templates/bundles/<Bundle> ➡️ @<Bundle>/…

                    {# templates/bundles/NatureBundle/mountain/trail.html.twig #}

                    <div class="trail-map" data-map>
                        <script type="application/json">
                            {{ trail_data|json_encode }}
                        </script>
                    </div>

                    {# todo: re-add the original stuff here #}
                

                    {# templates/bundles/NatureBundle/mountain/trail.html.twig #}
                    {% extends "@NatureBundle/mountain/trail.html.twig" %}

                    {% block main %}
                        {{ parent() }}

                        <div class="trail-map" data-map>
                            <script type="application/json">
                                {{ trail_data|json_encode }}
                            </script>
                        </div>
                    {% endblock %}
                

                    {# templates/bundles/NatureBundle/mountain/trail.html.twig #}
                    {% extends "@!NatureBundle/mountain/trail.html.twig" %}

                    {% block main %}
                        {{ parent() }}

                        <div class="trail-map" data-map>
                            <script type="application/json">
                                {{ trail_data|json_encode }}
                            </script>
                        </div>
                    {% endblock %}
                
WTF?

                {# scenery.twig #}
                {% block view %}
                    🌲🌳🌲
                {% endblock %}
            

                {# peaceful_scenery.twig #}
                {% extends "scenery.twig" %}

                {% block view %}
                       🕊️
                    {{ parent() }}
                {% endblock %}
            

                {# cloudy_scenery.twig #}
                {% extends "peaceful_scenery.twig" %}

                {% block view %}
                    ☁️🌤️☁️
                    {{ parent() }}
                {% endblock %}
            

                    🌲🌳🌲
                

                       🕊️
                    🌲🌳🌲
                

                    ☁️🌤️☁️
                       🕊️
                    🌲🌳🌲
                
scenery.twig?

                    ☁️🌤️☁️
                    🌲🌳🌲
                
render original
 "scenery.twig"?

Langer Weg zum Ziel

Twig im Contao-Ökosystem

04
Dear all,
greetings from
the Contao
ecosystem!

😘

P.S. Send money!

Input Encoding?

Output Encoding bleibt!

Contao Templates bekommen automatisch eine eigene Escaper Strategy mit deaktiviertem Double Encoding.


                /*
                 * "contao_html" escaper strategy
                 * @see ContaoEscaperNodeVisitor
                 */
                public function escapeHtml(string $string): string
                {
                    // […]
                    return htmlspecialchars($string, double_encode: false);
                }
            

                title: "Route von <a> nach <b>"
                text:  "Dieser Abschnitt hat es <i>wirklich</i> in sich."
            

                title: "Route von &lt;a&gt; nach &lt;b&gt;"
                text:  "Dieser Abschnitt hat es <i>wirklich</i> in sich."
            

                        {# @Contao/my_text.html.twig #}
                        <h2>{{ title }}</h2>

                        <div class="rte">
                            {# Allow trusted HTML here #}
                            {{ text|raw }}
                        </p>
                    
_____
title|escape('contao_html')
________
text
Route von <a> nach <b>
Dieser Abschnitt hat es wirklich in sich.

                <h2>Route von &lt;a&gt; nach &lt;b&gt;</h2>

                <div class="rte">
                    Dieser Abschnitt hat es <i>wirklich</i> in sich.
                </p>
            

Legacy Interop?

Twig Templates können als Ersatz für existierende Contao PHP Templates verwendet werden.

Twig Templates können Contao PHP Templates erweitern und Blöcke anpassen.
{% extends "@Contao/fe_page.html5" %}

Don't ask how. 🪄

"Multi-Tenant" Vererbung?

  • Templates aus Contao Template-Verzeichnissen (ContaoFilesystemLoader)
    • Bundles ➡️ @Contao_NatureBundle/…
    • App ➡️ @Contao_App/…
    • Global ➡️ @Contao_Global/…
    • Themes ➡️ @Contao_Theme_my_theme/…
  • Managed Namespace: @Contao/…
    → Hierarchisch organisierte Templates

                    {# scenery.twig (CoreBundle) #}
                    {% block view %}
                        🌲🌳🌲
                    {% endblock %}
                

                    {# scenery.twig (PeaceBundle) #}
                    {% extends "@Contao/scenery.twig" %}

                    {% block view %}
                           🕊️
                        {{ parent() }}
                    {% endblock %}
                
_________________________
"@Contao_CoreBundle/scenery.twig"

                    {# scenery.twig (WeatherBundle) #}
                    {% extends "@Contao/scenery.twig" %}

                    {% block view %}
                        ☁️🌤️☁️
                        {{ parent() }}
                    {% endblock %}
                
_________________________
"@Contao_PeaceBundle/scenery.twig"

                        🌲🌳🌲
                    

                           🕊️
                        🌲🌳🌲
                    

                        ☁️🌤️☁️
                           🕊️
                        🌲🌳🌲
                    
render "@Contao/scenery.twig"
DynamicExtendsTokenParser
DynamicIncludeTokenParser
DynamicUseTokenParser

Core Templates, Runtimes & mehr?

Runtimes für Funktionen und Filter

  • FigureRuntime
  • FormatterRuntime
  • FragmentRuntime
  • HighlighterRuntime
  • InsertTagRuntime
  • SanitizerRuntime
  • SchemaOrgRuntime
  • UrlRuntime

                            {# Do not replace anything, escape everything #}
                            {{ title }} → von &lt;a&gt; {{br}} nach &lt;b&gt;

                            {# Replace insert tags, escape everything #}
                            {{ title|insert_tag }} → von &lt;a&gt; &lt;br&gt; nach &lt;b&gt;

                            {# Replace insert tags, escape nothing #}
                            {{ title|insert_tag|raw }} → von <a> <br> nach <b>

                            {# Replace insert tags, escape only the text around the insert tags #}
                            {{ title|insert_tag_raw }} → von &lt;a&gt; <br> nach &lt;b&gt;
                        

Tag für Response Context


                    {% add to body %}
                        <script>…</script>
                    {% endadd %}
                

Helper Klasse für HtmlAttributes


                        <?php
                        class HtmlAttributes {
                           //  …
                        }
                    

                        {% set box_attributes = attrs(box_attributes|default)
                            .addClass('box')
                            .setIfExists('data-color', color)
                        %}

                        <div{{box_attributes}}>…</div>
                    

Content Elemente als Fragments mit Twig Templates

  • CodeController
  • DownloadsController
  • HeadlineController
  • ImagesController
  • ListController
  • MarkdownController
  • PlayerController
  • TableController
  • TeaserController
  • TemplateController
  • TextController
  • TopLinkController
  • UnfilteredHtmlController
  • VideoController

Templates für wiederverwendbare Komponenten

  • _download.html.twig
  • _figure.html.twig
  • _headline.html.twig
  • _list.html.twig
  • _picture.html.twig
  • _read_more.html.twig
  • _splash_screen.html.twig
  • _stylesheet.html.twig
  • _table.html.twig

Debug Command

$ vendor/bin/contao-console debug:contao-twig --tree foo/bar
    └──foo
       └──bar (@Contao/foo/bar.html.twig)
          └──templates/foo/bar.html.twig
             Original name: @Contao_App/foo/bar.html.twig

IDE autocompletion

. . .
Twig aus der Entwicklerperspektive
— Eine Expedition
2023 | Moritz Vondano
Alle Hintergrundbilder sind AI-generiert via NightCafe SDXL