Layouts und Themen in Twig

Die Template Engine Twig bietet die Möglichkeit von Vererbung, Includes und sogar Traits. So können Blöcke in den Templates auch ohne Vererbung wiederverwendet werden. Einen möglichen Weg möchte ich hier beschreiben.

Bei Projekten, in denen die Template-Engine Twig zum Einsatz kommt, besteht unter Umständen die Herausforderung darin, dass einige oder auch mehrere Template-Bestandteile immer wieder verwendet werden sollen. Da kommt schnell die Frage auf, wie organisiere ich meine Template-Struktur?

Ein Beispiel möchte ich hier zeigen, nachdem ich auf diesen Beitrag gestoßen bin. Danach habe ich eine, für mich optimale Struktur, entwickelt. Beginnen wir mit der Ordner-Struktur innerhalb eines Symfony Projektes:

templates/
    ├─ components/
    │  ├─ form/
    │  │  └─ form-horizontal.html.twig
    │  ├─ sidebars/
    │  │  └─ sidebar-blog.html.twig
    │  └─ alerts.html.twig
    ├─ emails/
    │  ├─ contact.html.twig
    │  └─ contact.txt.twig
    ├─ layouts/
    │  ├─ abstracts/
    │  │  ├─ boxed.html.twig
    │  │  ├─ fluid.html.twig
    │  │  └─ nogrid.html.twig
    │  ├─ themes/
    │  │  ├─ blank.html.twig
    │  │  ├─ fullwidth.html.twig
    │  │  ├─ sidebar-left.html.twig
    │  │  └─ sidebar-right.html.twig
    │  ├─ error.html.twig
    │  └─ blog.html.twig
    ├─ modules/
    │  ├─ blocks.html.twig
    │  └─ macros.html.twig
    ├─ pages/
    │  ├─ blog/
    │  │  ├─ listing.html.twig
    │  │  └─ single.html.twig
    │  ├─ error/
    │  │  ├─ 404.html.twig
    │  │  └─ generic.html.twig
    │  ├─ security/
    │  │  └─ login.html.twig
    │  └ homepage.html.twig
    └─ base.html.twig

Damit ist es relativ einfach, verschiedene Templates für Unterseiten des Projektes zu erstellen. Nehmen wir zum Beispiel die Startseite einer Website. Dazu sieht die Controller Methode in etwa so aus:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    /**
     * @return Response
     */
    public function homepage(): Response
    {
        return $this->render('pages/homepage.html.twig');
    }
}

Das Template templates/pages/homepage.html.twig sieht dann wie folgt aus:

{% extends "layouts/abstracts/boxed.html.twig" %}

{% block layout %}
    {% embed 'layouts/themes/fullwidth.html.twig' %}
        {% block content %}
            <p>Homepage</p>
        {% endblock %}
    {% endembed %}
{% endblock %}

Wie wir sehen, wird als erstes das abstrakte Layout boxed.html.twig erweitert. Dieses definiert den Block layout und bindet ein weiteres Thema namens fullwidth.html.twig ein. In diesem Thema wird der Block content definiert, welchen wir dann für die Homepage überschreiben. Der Inhalt des Boxed Layouts ist Folgender:

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

{% set template = 'container' %}

...und das Thema für die volle Breite des Bildschirms:

<div class="row">
    <section class="col content">
        {% block content %}{% endblock -%}
    </section>
</div>

Fehlt zu guter Letzt noch die base.html.twig, welche diesen Inhalt hat.

{% use 'modules/blocks.html.twig' %}

<!doctype html>
<html lang="{{ app.request.locale }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>{%- block title '' -%}</title>
        {{ encore_entry_link_tags('css/app') }}
        {{ encore_entry_link_tags('css/themes/'~theme|default('default')) }}
        {% block stylesheets %}{% endblock -%}
    </head>

    <body class="{{ classes|default()|join(' ') }}" itemscope itemtype="https://schema.org/WebPage">
        {{- block('header') -}}

        <main class="{{ template|default('container') }}"{% if schema is defined and schema is not same as(null) %} itemscope itemtype="{{ schema }}"{% endif %}>
            {% block layout %}{% endblock -%}
        </main>

        <footer class="container-fluid footer">
            <div class="{{ template|default('container') }} text-center">
                {{- block('footer') -}}
            </div>
        </footer>

        {{ encore_entry_script_tags('js/app') }}
        {% block javascripts %}{% endblock %}
    </body>
</html>

Das ist recht viel Code auf einmal. Also nehmen wir das Renedering mal auseinander.

  1. Startpunkt ist das Template templates/pages/homepage.html.twig
    1. Dieses erweitert das Layout templates/layouts/abstracts/boxed.html.twig
      • hier wird das Basis Template templates/base.html.twig erweitert, welches u.A. den Block layout definiert
      • Außerdem wird eine Twig Variable namens template mit dem Wert `container` gesetzt. Damit wird eine Bootstrap Klasse für das Grid-System gesetzt.
    2. Weiterhin wird das Theme templates/themes/fullwidth.html.twig eingebunden
      • Das Thema definiert den Block content, welchen wir dann auf der Homepage überschreiben können.

Zusätzlich haben wir noch die Möglichkeit, wiederkehrende Bestandteile (Blöcke) zu definieren und dann mehrfach zu verwenden. In meinem Beispiel wird das Markup für den Header und Footer so generiert. Aber auch Widgets für die Sidebar sind somit möglich. Das und die Verwendung von Macros ist aber Bestandteil eines anderen Beitrags.