21.2. Dependency Injection Beispiel-Plugin

Auf dieser Seite möchte ich Ihnen anhand eines Beispiels zeigen, wie eine Dependency Injection in einem WordPress Plugin aussehen könnte.

Wir treffen die Annahme, dass ein Rating-Plugin für WordPress erstellt werden soll. In diesem einfachen Beispiel beschränken wir uns dabei auf die Bewertung von Artikeln, Seiten oder benutzerdefinierte Artikeltypen.

Vorbereitungen und Annahmen

Bevor es los geht überlegen wir uns, was dazu nötig ist:

  • Ein Rating-Objekt welches die eigentlichen Bewertungsfunktionen enthält.
  • Ein PostRating-Objekt welches darauf abgestimmt ist, Bewertungen eines Artikels zu empfangen und zu schreiben.
  • Einen RestController der dafür sorgt, dass wir über die WordPress-REST-API Bewertungen schicken können.

Zusätzlich und für die Darstellung der Bewertungen im Frontend benötigen wir:

  • Einen FrontendController der unsere JavaScript- und CSS-Dateien einbindet sowie eine RatingView Klasse, die den HTML-Code dazu ausgibt.
  • Einen generellen PluginController der FrontendController und RestController ansprechen kann.

Autoloading

Um uns das Einbinden der einzelnen Dateien zu sparen, nutzen wir an dieser Stelle Composer und dessen Autoloader. Wie Sie Composer installieren, finden Sie auf der Website des Projekts. Ich gehe hier nicht näher darauf ein weil es letztlich auch immer möglich wäre, dass Sie die nötigen Dateien mittels require_once selbst einbinden.

Geben Sie im Terminal folgendes ein damit die composer.json-Datei erstellt wird.

cd /pfad/zum/wordpress/plugin/
composer init

Da wir aktuell noch keine abhängigen Pakete haben, installiert Composer auch kein Autoloading. Das müssen wir manuell machen.

composer dump

Nun haben wir die Folgende Verzeichnisstruktur, wobei wir die Datei mm-rating.php und den Ordner src manuell ergänzen müssen (siehe nächsten Schritt):

mm-rating/
    src/
    vendor/
        autoload.php
        composer/
            autoload_psr4.php
            autoload_namespaces.php
            ...
    composer.json
    mm-rating.php

In der Datei mm-rating.php definieren wir dann wie folgt:

<?php
/*
Plugin Name: MM Rating
*/

use mm\RatingPlugin;

if (!defined('ABSPATH')) {
    exit;
}

require_once __DIR__ . '/vendor/autoload.php';

Als nächstes editieren wir die composer.json-Datei um das PSR-4 Autoloading. Dabei teilen wir dem Composer-Autoloader mit, dass er alle Dateien des PHP-Namensbereichs mm im Unterverzeichnis src findet.

{
    "name": "floriansimeth/mm-rating",
    "description": "MM Rating Plugin",
    "type": "wordpress-plugin",
    "license": "GPLv3",
    "authors": [
        {
            "name": "Florian Simeth",
            "email": "florian@florian-simeth.de"
        }
    ],
    "require": {},
    "require-dev": {},
    "autoload": {
        "psr-4": {
            "mm\\": "src/"
        }
    }
}

PluginController

Nun benötigen wir den PluginController der unser Frontend und die REST-API anspricht. Ich habe das in zwei Dateien aufgetrennt.

Datei src/Plugin.php:

<?php

namespace mm;

abstract class Plugin
{
    protected static $instance;

    protected $pluginFilePath;

    public function __construct($pluginFilePath)
    {
        $this->pluginFilePath = $pluginFilePath;
    }

    public static function getInstance($pluginFilePath): Plugin
    {
        if (!self::$instance) {
            self::$instance = new static($pluginFilePath);
        }

        return self::$instance;
    }

    abstract public function startup(): void;

    public function getPluginFilePath()
    {
        return $this->pluginFilePath;
    }

    public function getPluginDirPath($path = '')
    {
        return plugin_dir_path($this->pluginFilePath) . $path;
    }

    public function getPluginDirUrl($path = '')
    {
        return plugin_dir_url($this->pluginFilePath) . $path;
    }
}

Datei src/RatingPlugin.php:

<?php

namespace mm;

class RatingPlugin extends Plugin
{
    public function startup(): void
    {
        add_action('init', [$this, 'registerScripts']);

        if (is_admin()) {
        } else {
            $frontendController = new FrontendController();
            $frontendController->init();
        }

        add_action('rest_api_init', [$this, 'initRestAPI']);
    }

    public function registerScripts()
    {
        wp_register_script(
            'mm/rating',
            $this->getPluginDirUrl('assets/js/rating.js'),
            ['wp-api-fetch'],
            filemtime($this->getPluginDirPath('assets/js/rating.js')),
            true
        );
    }

    public function initRestAPI(): void
    {
        $restController = new RestController();
        $restController->init();
    }
}

Damit das Plugin auch lauffähig ist, müssen wir in der datei mm-rating.php folgendes ergänzen:

RatingPlugin::getInstance(__FILE__)->startup();

FrontendController

Die FrontendController Klasse muss zwei Dinge für uns tun:

  1. Das Rating unterhalb des Inhalts darstellen.
  2. Die JavaScript-Datei für die Bewertungsfunktion einbinden.

Wir erstellen die Datei src/FrontendController.php:

<?php

namespace mm;

class FrontendController
{
    public function __construct()
    {
    }

    public function init(): void
    {
        add_action('the_content', [$this, 'injectRatingView']);
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
    }

    public function injectRatingView(string $content): string
    {
        $post = get_post(get_queried_object_id());

        $rating = new PostRating($post);
        $ratingView = new RatingView($rating);

        return $content . $ratingView->getHTML();
    }

    public function enqueueScripts(): void
    {
        if (!get_post_type(get_queried_object_id())) {
            return;
        }

        wp_enqueue_script('mm/rating');

        $scriptData = call_user_func(
            function () {
                $o = new \stdClass();
                $o->postId = get_queried_object_id();
                return $o;
            }
        );

        wp_add_inline_script(
            'mm/rating',
            sprintf('var MM = %s;', json_encode($scriptData))
        );

        wp_enqueue_style('dashicons');
    }
}

Dazu benötigen wir noch die Klasse RatingView in der Datei src/RatingView.php. Sie soll die Bewertung darstellen.

<?php

namespace mm;

class RatingView
{
    protected $rating;

    public function __construct(Rating $rating)
    {
        $this->rating = $rating;
    }

    public function print(): void
    {
        echo $this->getHTML();
    }

    public function getHTML(): string
    {
        $html = '';
        $ratingValue = $this->rating->getAverageRating();

        $fullStars = (int) floor($ratingValue);
        $halfStars = (int) ceil(fmod($ratingValue, $fullStars));
        $emptyStars = (int) $this->rating->getMaxRating() - $fullStars - $halfStars;

        $dashicon = '<span class="rating-star dashicons dashicons-star-%s"></span>';

        $html .= str_repeat(
            sprintf($dashicon, 'filled'),
            $fullStars
        );

        $html .= str_repeat(
            sprintf($dashicon, 'half'),
            $halfStars
        );

        $html .= str_repeat(
            sprintf($dashicon, 'empty'),
            $emptyStars
        );

        return $html;
    }
}

Nun können wir uns schon einmal das Frontend, genauer gesagt, einen Blogpost ansehen. Wir sehen, die fünf leeren Sterne am Ende des Blogbeitrags:

Ein Blogpost in WordPress mit Sterne-Bewertung.
Die fünf leeren Sterne sind gut zu erkennen.

Eine Bewertung ist allerdings noch nicht möglich. Dazu brauchen wir die Logik für das Bewerten an sich. Und das soll über die REST-API möglich sein:

RESTController

Wie oben erwähnt soll der REST-Controller die Bewertungs-Anfrage entgegennehmen.

You’re not allowed to see this content. Please log in first.

Rating-Klassen

Das eigentliche Rating-Objekt habe ich auf zwei Klassen aufgeteilt um es später wiederverwenden zu können. Dazu aber gleich mehr.

You’re not allowed to see this content. Please log in first.

Fazit

Fertig. Haben Sie entdeckt, wo die Dependency-Injection steckt? Ich hoffe doch sehr. Hier habe ich allerdings noch einige Anmerkungen dazu:

  • Um nicht gegen das Single-Responsibility-Prinzip zu verstoßen sollte die Klasse RatingPlugin nur die Controller-Klassen instantiieren, nicht jedoch JavaScript- und CSS-Dateien registrieren.
  • Die gleiche Klasse ist auch als Singleton erstellt. D.h. auf sie kann global über RatingPlugin::getInstance() zugegriffen werden. Ein Singleton wird generell als Anti-Pattern bezeichnet weil Code, der global verfügbar ist, meist von Nachteil ist. Das heißt nicht, dass er per-se schlecht ist. In diesem Fall finde ich es ganz gut, weil es anderen Dritt-Plugins die Möglichkeit gibt, die vom Plugin erstellten REST-Endpunkte zu deaktivieren, falls das gewünscht ist.