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 eineRatingView
Klasse, die den HTML-Code dazu ausgibt. - Einen generellen
PluginController
derFrontendController
undRestController
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:
- Das Rating unterhalb des Inhalts darstellen.
- 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:
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.
Rating-Klassen
Das eigentliche Rating-Objekt habe ich auf zwei Klassen aufgeteilt um es später wiederverwenden zu können. Dazu aber gleich mehr.
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.