WordPress und Twig: Wie man Data von Views trennt

WordPress und Twig: Wie man Data von Views trennt
WordPress und Twig: Wir erklären, wie du deinen Code mit Twig sauberer und besser testbar machen kannst.

In diesem Beitrag schreibt Inpsyder Guido über WordPress und Twig. Er konzentriert sich darauf, wie man Data von Views trennt, um einen saubereren und besser testbaren Code zu erhalten.

Bei der ersten Annäherung an WordPress hat jeder Entwickler damit begonnen, Templates entweder für Plugins oder Themes zu erstellen. Und jeder Entwickler weiß, wie schwierig es sein kann, ein Template zu pflegen, das HTML- und PHP-Code vermischt, wenn weitere Funktionen zu diesem Template hinzugefügt werden. Die Datei wird komplexer. Und je mehr Dinge man hinzufügt und je mehr Code man berührt, desto schwieriger wird es, Probleme zu beheben.

Templates können zwar in mehrere Teile aufgeteilt werden und jedes wird seine eigenen Dinge erledigen. Aber die meiste Zeit setzt man Daten vom Parent für das untergeordnete Template frei. Und das ist nicht gut.
Eine andere Sache, die Entwickler normalerweise tun, ist, die php extract Funktionen zu verwenden, um Variablen aus Datensammlungen zu erstellen (normalerweise Array-Strukturen). Das ist ein unkontrollierter Weg. Außerdem nimmt die Funktion einen zweiten Parameter flag, der standardmäßig auf EXTR_OVERWRITE gesetzt ist, und meistens verändern das Entwickler nicht. So werden andere Variablen mit dem gleichen Namen überschrieben. Und das ist nicht gut.

Abgesehen von diesen Problemen möchten wir als Entwickler einen sauberen und gut formatierten Code haben. Code, der einfach zu lesen, selbsterklärend, einfach zu warten – und noch wichtiger: testbar ist.

Meistens wird es schwierig, Code zu testen, wenn sich alles innerhalb eines Templates befindet. Das liegt daran, dass ein Test WordPress kennen muss. Du musst eine Menge Funktionen vortäuschen. Wenn du deinen Code testen wollen, willst du auch die Logik deines Codes testen und nicht WordPress. Und das ist etwas schwierig, wenn sich alles in einem Template befindet.

Wir in diesem Artikel keine Unit-Tests besprechen, aber wenn du tiefer in Unit-Tests ohne WordPress einsteigen willst, können diese Artikel zum Thema Unit-Test für Php Code ohne WordPress hilfreich sein: Teil 1 und Teil 2.

Wie diese Probleme lösen?

Wenn du jemals von MVC gehört hast, dann hast du vielleicht schon eine Idee davon, wie die oben genannten Probleme gelöst werden können.

Model-View-Controller ist ein Architekturmuster, das häufig für die Entwicklung von Benutzeroberflächen verwendet wird und eine Anwendung in drei miteinander verbundene Teile unterteilt. Dies geschieht, um interne Darstellungen von Informationen von der Art und Weise zu trennen, wie Informationen dem Benutzer präsentiert und vom Benutzer akzeptiert werden.

Auch wenn die Implementierung eines echten MVC-Musters manchmal für Over-Engineering gehalten wird, könnte die Logik dieses Musters die Grundlage für unseren Ansatz sein. In einer sehr abstrahierten Art und Weise könnten wir WordPress als unseren Controller betrachten und die View von dem Modell trennen. Dabei ist View das Markup und das Model die Daten, die wir in das Markup einfügen wollen. Wenn wir die Logik des Modells von der View trennen, können wir einen besser testbaren und leichter zu wartenden Code erzeugen, der auch weniger fehleranfällig ist.

Aber wie trennt man das Modell von der View?

Das ist die Stelle, an der Twig und Datenstrukturen ins Spiel kommen.

Twig

Twig ist eine Template-Engine. Eine template engine ist eine Software, die Daten und Views kombiniert, um ein Dokument zu erstellen. In unserem Fall ein Html-Dokument oder zumindest ein Teil davon.

Datenstrukturen

Am einfachsten könnten wir Datenstrukturen als eine Sammlung von Werten definieren. PHP wird mit einer sehr einfachen Datenstruktur namens Array ausgeliefert. Aber PHP 7 erweitert das, indem es spezifischere Datenstrukturen als Alternative zum Array einbezieht. Für weitere Informationen kannst du Effiziente Datenstrukturen für php 7 lesen. In unserer Implementierung bleiben wir bei Array, da alle PHP-Versionen diese unterstützen und sie einfach zu verwalten sind.

Kombiniere Twig und Datenstrukturen

Nehmen wir an, wir wollen das Thumbnail-Bild in einem einzigen Blogbeitrag anzeigen. Dazu hängen wir uns in den Filter the_content ein und verwenden einen Twig, um das analysierte Markup zu erhalten. Dann stellen wir das Bild dem Post-Inhalt vor. Für unsere Implementierung verwenden wir twig-wordpress, eine einfache Bibliothek, die Unterstützung für WordPress-Filter und -Funktionen zu Twig hinzufügt. Wir werden auch einige andere Bibliotheken verwenden, um das WordPress-Modell und die View zu definieren. Dann stellen wir alles zusammen.

Twig für WordPress

Wenn wir Twig für WordPress verwenden, können wir einen Dienst erstellen, der uns beim Rendern unserer Templates hilft. Der einfachste Weg, diesen Dienst zu erstellen, ist, den Twig für WordPress Factory wie folgt zu verwenden:

$twig = new Factory(new FilesystemLoader(__DIR__ . '/views/'), []);
$twig = $twig->create();
$twigController = new TwigController($twig);

Bei genauerem Hinsehen können wir erkennen, dass der Code eine neue TwigEnvironment-Instanz (TwigEnvironment wird unser Template rendern) mit dem FilesystemLoader (dem Lader des Templates) erstellt. Der an den FilesystemLoader-Konstruktor übergebene Pfad ist der Basispfad, in dem sich die Templates befinden. Wir gehen davon aus, dass wir nach Templates im Haupt-Root-Plugin-Verzeichnis suchen und den obigen Code von einem Skript ausführen, das sich ebenfalls unter dem Hauptverzeichnis befindet.

Twig hat verschiedene Arten von Ladern. Du kannst die Vorlage aus Dateien, aus Arrays oder Verkettungsladern laden (Lader, die in Reihe ausgeführt werden, um die zu ladende Vorlage zu finden).

So, jetzt haben wir die Twig-Umgebung, die für das Rendern unseres Templates verantwortlich ist. Was wir als nächstes brauchen, ist das Modell und die View. Außerdem benötigen wir einen Weg, diese in die Twig-Umgebung zu übergeben, um unsern Template-Markup-Output zu erhalten.

Model und View

Als Entwickler möchten wir unseren Code kapseln. Also werden wir eine Klasse bauen, die die Schnittstelle WordPressModel\Model implementiert, die nur eine Methode hat: Data. Diese ist dafür verantwortlich, die Datenstruktur zurückzugeben, die wir in unser Template einfügen wollen.

interface Model
{
    /**
     * Data
     *
     * @return array The list of elements to bind in the view
     */    public function data(): array;
}

Die genaue Implementierung kann in der Klasse PostThumbnail eingesehen werden. Diese wird dann in die TwigData eingeführt, eine Klasse, die das Modell und den Pfad, in dem sich die View befindet, enthält.

final class PostThumbnail implements Model
{
    const FILTER_DATA = 'twigwordpresslightexample.attachment_image';
    const FILTER_ALT = 'twigwordpresslightexample.attachment_image_alt';

    /**
     * @var
     */    private $attachmentSize;

    /**
     * @var int
     */    private $attachmentId;

    /**
     * @param int $attachmentId
     * @param mixed $attachmentSize
     */    public function __construct(
        int $attachmentId, 
        $attachmentSize = 'thumbnail'
    ) {

        $this->attachmentId = $attachmentId;
        $this->attachmentSize = $attachmentSize;
    }

    /**
     * @return array
     */    public function data(): array
    {
        $imageSource = $this->attachmentSource();

        /**
         * Figure Image Data
         *
         * @param array $data The data arguments for the template.
         */        return apply_filters(self::FILTER_DATA, [
            'image' => [
                'attributes' => [
                    'url' => $imageSource->src,
                    'class' => 'thumbnail',
                    'alt' => $this->alt(),
                    'width' => $imageSource->width,
                    'height' => $imageSource->height,
                ],
            ],
        ]);
    }

    /**
     * @return \stdClass
     *
     * @throws \InvalidArgumentException If the attachment isn't an image.
     */    private function attachmentSource(): \stdClass
    {
        if (!wp_attachment_is_image($this->attachmentId)) {
            throw new \InvalidArgumentException(
                'Attachment must be an image.'
            );
        }

        $imageSource = wp_get_attachment_image_src(
            $this->attachmentId,
            $this->attachmentSize
        );

        if (!$imageSource) {
            return (object)[
                'src' => '',
                'width' => '',
                'height' => '',
                'icon' => false,
            ];
        }

        $imageSource = array_combine(
            ['src', 'width', 'height', 'icon'],
            $imageSource
        );

        return (object)$imageSource;
    }

    /**
     * @return string
     */    private function alt(): string
    {
        $alt = get_post_meta(
            $this->attachmentId, 
            '_wp_attachment_image_alt', 
            True
        );

        /**
         * Filter Alt Attribute Value
         *
         * @param string $alt The alternative text.
         * @param int $attachmentId The id of the attachment from which 
         *                          the alt text is retrieved.
         */        $alt = apply_filters(self::FILTER_ALT, $alt, $this->attachmentId);

        return (string)$alt;
    }
}

Was die Klasse im Wesentlichen macht, ist der Aufbau der Daten für die View. Wie man im folgenden Code sehen kann, ist das nicht sehr kompliziert. Wir verkapseln einfach ein wenig WordPress-Logik unter private Methoden, um sie besser zu pflegen. Und die für die View benötigten Daten werden ausgegeben.

return apply_filters(self::FILTER_DATA, [
            'image' => [
                'attributes' => [
                    'url' => $imageSource->src,
                    'class' => 'thumbnail',
                    'alt' => $this->alt(),
                    'width' => $imageSource->width,
                    'height' => $imageSource->height,
                ],
            ],
        ]);

Wir geben die Daten auch an einen Filter weiter, falls andere Entwickler Änderungen daran vornehmen wollen.

Jetzt, da wir unser Modell haben, brauchen wir eine View. Das Erstellen einer View ist einfach. Da wir definiert haben, dass unser Twig-Lader ein Dateisystemlader ist, erstellen wir einfach eine neue .twig-Datei im Verzeichnis views mit dem Namen thumbnail.twig, das das Markup und die Property-Zugriffe auf unser Modell enthält.

{% if image.attributes.url %}
    <img src="{{ image.attributes.url }}"
         class="{{ image.attributes.class }}"
         alt="{{ image.attributes.alt }}"
         width="{{ image.attributes.width }}"
         height="{{ image.attributes.height }}"
    />
{% endif %}

Twig verwendet die doppelten geschweiften Klammern, um zu identifizieren, welche Art von Daten durch Werte ersetzt werden müssen.

Daher wird die `{{{ image.attributes.url }}}` auf den in `$imageSource->src` enthaltenen Wert interpoliert. Die `{{{ image.attributes.class }}}` zu `Thumbnail` und so weiter.

Am Ende sieht unser Markup wie folgt aus:

<img
     src="http://dev.local/wp-content/uploads/2018/10/thumbnail.jpg"
     class="thumbnail" 
     alt="Alternative text for the image" 
     width="800" 
     height="800"
/>

Mit dem Twig kannst du auch Kontrollstrukturen verwenden. Dies ist nützlich, um Werte zu prüfen, sodass der Ausdruck {% if image.attributes.url %} bedeutet, dass das Markup nur angezeigt wird, wenn die URL einen nicht-leeren Wert enthält.

Und nun: Alles zusammenfügen

Jetzt, da wir das Modell und die View haben, wollen wir sie mit Hilfe der Twig-Umgebung rendern. Dazu können wir die Klassen aus dem Package Twig WordPress View verwenden. Das Paket besteht aus zwei Klassen, einem Controller und einer ViewData. Die erste Klasse wird verwendet, um das Modell in die richtige Ansicht zu injizieren, indem die Twig-Umgebungs-Instanz verwendet wird. Und die zweite ist eine Datenklasse, die die Instanz des Modells und den View-Dateipfad enthält.

Das erste, was wir nun tun, ist, eine Instanz von ViewData und eine Instanz des Controller zu erstellen. Dann übergeben wir die Instanz von ViewData an die Controller::render-Methode. Intern verwendet die Methode Controller::render die Twig-Umgebung, um die View-Datei zu laden. Dann wird TwigEnvironment::display aufgerufen, indem die Daten aus dem Modell übergeben werden.

$model = new Model\PostThumbnail($postThumbnailId, 'post-thumbnail');
$viewData = new TwigData($model, 'thumbnail.twig');

Dann wollen wir es unserem Controller weitergeben, damit es gerendert wird:

$twigController->render($viewData);

Lasst uns alles zusammen anschauen:

add_filter(
    'the_content', 
    function ($content) use ($twigController) {
        if (!is_singular()) {
            return $content;
        }

        ob_start();

        $postThumbnailId = (int)get_post_thumbnail_id();
        if ($postThumbnailId < 1) { 
            return $content; 
        } 
    
        $model = new Model\PostThumbnail( $postThumbnailId, 'post-thumbnail' ); 
        $viewData = new TwigData($model, 'thumbnail.twig'); 
        $twigController->render($viewData);

        return ob_get_clean() . $content;
    }
);

Ein Hinweis auf die Verwendung von ob_start() und ob_get_clean()-Methoden. Da die Rendermethode versucht, das Markup auszugeben, müssen wir den Inhalt dieser Ausgabe aus dem Ausgabepuffer holen, damit wir sie als Zeichenkette mit dem Inhalt des Beitrags verknüpfen können. Und das heißt, wir können den $twigController wiederverwenden, um andere Twig-Daten zu drucken, indem wir einfach eine neue Modellklasse und eine View erstellen.

Ich habe eine Klasse TwigData erstellt, anstatt das Modell und den Template-Pfad direkt in die Rendermethode zu übergeben, um sie zu kapseln. Das erleihtert das Verschieben innerhalb des Codes, wenn man zum Beispiel die Daten für den Twig woanders erstellen möchte. Ich habe einen Repo erstellt, in dem es möglich ist, ein wenig mit dem Code zu spielen. Falls du Interesse hast, kannst du den Repo unter hier klonen.

Fazit

Es gibt noch viel mehr Dinge, die wir uns anschauen können, wenn wir in Template-Engines einsteigen, aber ich hoffe, dass du mit diesem Artikel einen anderen Blickwinkel darauf erhalten hast, wie Templates in WordPress erstellt werden können. Außerdem gibt es viele Wege, die Anliegen für Modelle und Views aufzuteilen. Dies ist nur einer von ihnen.

Wenn du mehr über Twigs lesen möchtest, würde ich dir vorschlagen, einen Blick hierauf zu werfen: Sandboxes, Partials und Cache. Du kannst mehr darüber unter dem Abschnitt Erweiterter Twig lesen.