In unserem vorletzten Adventskalender-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, eine 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 du hinzufügst und je mehr Code du berührst, desto schwieriger wird es, Probleme zu beheben.

Ja, du kannst die Templates in mehrere Teile aufteilen. Und jedes wird seine eigenen Dinge erledigen. Aber die meiste Zeit setzt du Daten vom Elternteil 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 nicht nur 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: Code, der testbar ist.

Meistens wird es schwierig, Code zu testen, wenn sich alles innerhalb eines Templates befindet. Das liegt daran, dass dein 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 werden Unittests in diesem Artikel nicht besprechen, aber wenn du tiefer in Unittests ohne WordPress einsteigen willst, bist du mit den Unit Test für Php Code ohne WordPress Artikeln Teil 1 und Teil 2 gut bedient.

Wie diese Probleme lösen?

Wenn du jemals von MVC gehört hast, dann hast du vielleicht eine Idee, 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, in dem die View das Markup ist 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 gut wartbaren 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

Auf einfachste Weise 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 sie 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, um Unterstützung für WordPress-Filter und -Funktionen zu Twig hinzuzufügen. 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 for WordPress Factory so zu verwenden:

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

Bei genauerem Hinsehen sehen wir, 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

Das Modell hier ist im interessantesten Teil. Als Entwickler möchten wir unseren Code kapseln. Also werden wir eine Klasse bauen, die die Schnittstelle `WordPressModel\Model` implementiert, die nur eine Methode `Daten` hat, die dafür verantwortlich ist, 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. Und wie du im folgenden Code sehen kannst, gibt es keine schwarze Magie oder schwierige Dinge zu verstehen. Wir verkapseln einfach ein wenig WordPress-Logik unter private Methoden, um sie besser zu pflegen. Und es gibt die für die View benötigten Daten zurück.

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 einen 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 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 ruft es `TwigEnvironment::display` auf, indem es die Daten aus dem Modell übergibt.

$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, was das Verschieben innerhalb des Codes erleichtert, 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 https://github.com/widoz/twig-wordpress-light-example 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, nicht unbedingt der beste, aber es ist einer.

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

Antwort abgeben

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.