Im letzen Artikel habe ich die Verwendung von Namespaces erklärt, diese benötigen wir heute, um das Autoloading zu etablieren, das gleichzeitig unsere Ordner-Struktur definiert.
Um unseren Code übersichtlich zu halten, verteilen wir den Code sinnvoll auf mehrere PHP-Dateien. Jede Klasse sollte in einer eigenen Datei deklariert werden. Diese Datei muss dann im Haupt-Code zunächst geladen werden (z.B. über require_once()
), bevor die Klasse instanziiert werden kann.
Gehen wir einmal von folgender Ordernstruktur eines Beispiel-Plugins aus:
my-plugin
├── class-01.php
├── class-02.php
└── my-plugin.php
Inhalt class-01.php:
<?php
class class01
{
public function __construct()
{
echo 'class01 instantiated.';
}
}
Möchten wir nun in der Plugin-Datei my-plugin.php die Klasse class01
instanziieren, müssen wir die Datei zunächst einbinden und können dann die Klasse instanziieren:
<?php
/**
* Plugin Name: MyPlugin
*/
require_once __DIR__ . '/class-01.php';
$klasse01 = new class01(
Benötigen wir auch class02
in diesem Skript, müssen wir ebenfalls zunächst die Datei einbinden und können die Klasse dann instanziieren:
<?php
/**
* Plugin Name: MyPlugin
*/
require_once __DIR__ . '/class-01.php';
require_once __DIR__ . '/class-02.php';
$klasse01 = new class01();
$klasse02 = new class02();
Dies kann schon bei einer geringen Anzahl von Klassen schnell unübersichtlich werden und zu langen require_once()
-Listen führen.
Nun ändert sich eine Anforderung an unser Plugin und wir müssen einen Service einführen, der z.B. von class02
verwendet werden muss.
Wir fügen unseren Service als neue Klasse in die Datei service-01.php ein:
my-plugin
├── class-01.php
├── class-02.php
├── my-plugin.php
└── service-01.php
<?php
class service01
{
public function __construct()
{
echo 'service01 instantiated.';
}
}
Und ändern class02
:
<?php
class class01
{
protected service01 $service01;
public function __construct(service01 $service01)
{
$this->service01 = $service01;
echo 'class02 and service01 instantiated.';
}
}
Nun müssen wir in der Hauptdatei des Plugins zunächst die Datei des Service einbinden:
<?php
/**
* Plugin Name: MyPlugin
*/
require_once __DIR__ . '/class-01.php';
require_once __DIR__ . '/class-02.php';
require_once __DIR__ . '/service-01.php';
$service01 = new service01();
$klasse01 = new class01();
$klasse02 = new class02($service01);
Und spätestens hier wird es unübersichtlich, es folgt zunächst eine lange Liste mit require_once()
und wir müssen bei jeder neuen Klasse, die wir benötigen, diese Liste noch länger machen. Darüber hinaus sieht unsere Ordnerstruktur auch nicht mehr sehr übersichtlich aus. Im nächsten Schritt könnte also ein inc-Ordner eingeführt werden, in dem alle Datein liegen, die wir per require_once()
einfügen möchten:
my-plugin
├── inc
│ ├── class-01.php
│ ├── class-02.php
│ └── servie-01.php
└── my-plugin.php
Im Grunde ist an dieser Stelle auch noch nichts gegen einen solchen Aufbau einzuwenden. Wird das Plugin umfangreicher und wir haben unterschiedliche Klassen wie Models, Services und Klassen mit Hooks, brauchen wir auch eine Ordnerstruktur, die all die neuen Datein sinnvoll und übersichtlich gliedert.
Darüber hinaus möchten wir von der dann sehr langen Liste an require_once()
-Aufrufen wegkommen.
Autoloading bezeichnet das automatische Laden von benötigten Dateien und um dies verlässlich nutzen zu können, müssen wir unseren bisherigen Code zum einen mit Namespaces ausrüsten und zum anderen sollten wir bestimmten Coding-Standards folgen. Coding-Standards betreffen hier sowohl den Code selbst, als auch die Ordner-Struktur, die wir verwenden werden. Diese Standards sorgen dafür, ein wenig mehr Ordnung in das Chaos zu bringen, das PHP generell erlaubt. Es gibt Standards für das Einrücken von Code (der ewige Krieg: Tabs oder Spaces?), die Benennung von Variablen, Funktionen, Klassen und Methoden usw.
Standards im Coding führen zu mehr Übersichtlichkeit und sie geben generell ein Schema vor, wie etwas aussehen sollte, damit sich Entwickler*innen nicht auch noch darum Gedanken machen müssen und damit sich – vor allem im Team – jeder schnell und einfach in fremden Code einfinden kann.
Weil es ja sonst zu einfach wäre, gibt es unterschiedliche Standards. 🙂 Coding-Standards werden von unterschiedlicher Stelle definiert. Es gibt Standards, die WordPress selbst festlegt, Standards, die von der PHP Framework Interoperability Group (PHP-FIG) festgelegt werden und auch einzelne Firmen und Agenturen können natürlich ihre eigenen Standards definieren.
Uns interessieren an dieser Stelle primär zwei Definitionen:
An welche Standards halten wir uns denn nun?
Die Antwort, die dir nicht gefallen wird: it depends.
Es kommt auf deinen eigenen Geschmack an und es gibt hier kein richtig und kein falsch. Du kannst auch deine eigenen Standards definieren (was ich aber nicht empfehle). Wichtig ist, sich tatsächlich an eine Sache zu halten.
An welche Standards werden wir uns in dieser Serie halten?
Ich möchte dir nicht vorschreiben, an welche Standards du dich hältst, aber ich halte mich an die PSR, da sie auch von anderen PHP-Frameworks wie Symfony und Laravel eingehalten werden. Wenn du auf PSR setzt, bist du nicht im WordPress-Universum gefangen, sondern kannst nahtlos auch an Symfony- oder Lavarel-Projekten arbeiten. Hier gehen allerdings die Meinungen auseinander, es gibt zahlreicher Verfechter des WordPress-Standards, wenn schon für WordPress programmiert wird. Möchtest du einmal Core-Entwickler werden, solltest du auf jeden Fall wissen, dass es unterschiedliche Coding-Standards gibt.
Bitte nimm dir – falls du PSR noch nicht kennst – die Zeit, und sieh dir die folgenden PSRs einmal an:
WICHTIG:
PHP-Dateien, die entweder reinen PHP-Code enthalten, oder aber mit einem PHP-Code-Block enden, dürfen keinen schließenden PHP-Tag (?>
) enthalten (siehe PSR-2, 2.2 Files)! Dies kann vor allem in der Plugin-Entwicklung zu einem Headers already sent
-Fehler führen, wenn nach dem ?>
noch Whitespace oder eine Leerzeile folgt.
Da wir für das Autoloading in der Ordner-Benennung den Namspaces folgen müssen, möchte ich für die Ordnerstruktur keine festen Standards definieren. Das ist letztlich gerade in der WordPress-Plugin-Entwicklung etwas mehr Geschmackssache. Frameworks sind da allerdings wesentlich strenger.
Für später (und auch für das PSR-4-Autoloading, siehe weiter unten) ist es allerding wichtig, dass ausser der Plugin-Hauptdatei alle PHP-Dateien innerhalb eines src-Ordners liegen. Dies wird vor allem dann wichtig, wenn wir zu den Unit Tests kommen, da so der eigentliche Sourcecode in src und alle zugehörigen Tests in tests liegen. Aber auch für das Autoloading ist dies, wie wir gleich sehen werden, wichtig.
Um das Autoloading gut etablieren zu können, richten wir unseren Code nun komplett an den Standards aus und führen Namespaces ein. Folgende Änderungen führen wir durch:
Klasse01
statt klasse01
MKMyPlugin
nach dem Muster HerstellerPlugin und unsere Klassen 1 und 2 liegen im Subnamespace Main
, während der Service in Service
liegtWir etablieren nun die Ordnerstruktur, die der Benennung der Namespaces folgt:
my-plugin
├── src
│ ├── Main
│ │ ├── Class01.php
│ │ └── Class02.php
│ └── Service
│ └── Service01.php
└── my-plugin.php
src/Main/Class01.php
<?php
namespace MK\MyPlugin\Main;
class Class01
{
public function __construct()
{
echo 'Class01 instantiated.';
}
}
src/Main/Class02.php
<?php
namespace MK\MyPlugin\Main;
use MK\MyPlugin\Service\Service01;
class Class02
{
protected Service01 $service01;
public function __construct(Service01 $service01)
{
$this->$service01 = $service01;
echo 'Class02 instantiated.';
}
}
src/Service/Service01.php
<?php
namespace MK\MyPlugin\Main;
class Service01
{
public function __construct()
{
echo 'Service01 instantiated.';
}
}
my-plugin.php
<?php
/**
* Plugin Name: MyPlugin
*/
require_once __DIR__ . '/src/Main/Class01.php';
require_once __DIR__ . '/src/Main/Class02.php';
require_once __DIR__ . '/src/Service/Service01.php';
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Grundsätzlich gibt es zwei Wege, Autoloading zu verwenden:
spl_autoload_register()
Ich verwende grundsätzlich das Autoloading über composer und empfehle dies sehr. Um hier aber die wichtigsten Dinge abzudecken, behandle ich auch spl_autoload_register()
.
spl_autoload_register()
Mit der PHP-Funktion spl_autoload_register()
können wir unser eigenes Autoloading implementieren. Dazu sehen wir uns einmal den Funktionsaufruf an:
spl_autoload_register(?callable $callback = null, bool $throw = true, bool $prepend = false): bool
Die Funktion erwartet also mindests eine Callback-Funktion, und dieser Funktion wird der Name der zu ladenden Klasse mitgegeben:
callback(string $class): void
Wir können nun in unserer Hauptdatei my-plugin.php die require_once()
-Aufrufe auskommentieren und testweise einmal spl_autoload_register()
einfügen, als Callback verwenden wir eine anonyme Funktion:
<?php
/**
* Plugin Name: MyPlugin
*/
// require_once __DIR__ . '/src/Main/Class01.php';
// require_once __DIR__ . '/src/Main/Class02.php';
// require_once __DIR__ . '/src/Service/Service01.php';
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
spl_autoload_register(function(string $className) {
var_dump($className);
});
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Dadurch, dass wir die require_once()
-Aufrufe deaktiviert und spl_autoload_register
integriert haben, versucht PHP beim Aufruf von new Service01()
über den Autoloader die passende PHP-Datei zu erhalten. Dazu übergibt PHP den voll qualifizierten Klassennamen an die Callback-Funktion, die wir an spl_autoload_register
übergeben haben.
Alles, was unser Callback momentan macht, ist per var_dump()
den Klassennamen auszugeben. Unser obiger Code erzeugt also folgende Meldung:
string(29) "MK\MyPlugin\Service\Service01"
Fatal error: Uncaught Error: Class "MK\MyPlugin\Service\Service01" not found in
[(...)/wp-content/plugins]/autoload-plugin-02/autoload-plugin-01.php:18
Stack trace: #0 (...)/wp-settings.php(453): include_once() #1 (...)/wp-config.php(93):
require_once '...' #2 (...)/wp-load.php(50): require_once '...' #3 (...)/wp-admin/admin.php(34):
require_once '...' #4 (...)/wp-admin/plugins.php(10): require_once '...' #5 {main}
thrown in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 18
Ich habe in diesem Code meine lokalen Pfade durch (…) ersetzt und manuelle Umbrüche eingefügt, um die Meldung nicht zu lang zu machen.
Zunächst erscheint also die Ausgabe von var_dump()
:
string(29) "MK\MyPlugin\Service\Service01"
Dies ist der voll qualifizierte Name der Klasse, die als erstes instanziiert wird.
Dann folgt der Fatal Error, da die Klasse nicht gefunden werden konnnte. Wir müssen nun also in unserem Callback dafür sorgen, dass die entsprechende Datei geladen wird.
Da wir es uns mit den Namespaces und der entsprechenden Ordnerstruktur schon sehr einfach gemacht haben, gelingt es uns durch ein paar simple Anpassungen des voll qualifizierten Klassennamens den Dateinamen zu erhalten:
<?php
/**
* Plugin Name: MyPlugin
*/
// require_once __DIR__ . '/src/Main/Class01.php';
// require_once __DIR__ . '/src/Main/Class02.php';
// require_once __DIR__ . '/src/Service/Service01.php';
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
spl_autoload_register(function(string $className) {
// MKMyPlugin vom Klassennamen durch den Pfad zu src ersetzen:
$className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
// Die restlichen Backslashes durch Verzeichnis-Trenner (Slashes) ersetzen und .php anhängen
$classFile = str_replace('\\', '/', $className) . '.php';
// Klassen-Datei laden
require_once $classFile;
});
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Im Beispiel der Servie01-Klasse lautet der Klassenname MKMyPluginServiceService01
. Der Teil MKMyPlugin
kann dabei als Alias zum src-Verzeichnisses unseres Plugins gesehen werden.
Um daraus also den Pfad für die Klassen-Datei zu erhalten, müssen wir im ersten Schritt nur den Teil MKMyPlugin
durch __DIR__ . '/src/
ersetzen. Im Anschluss tauschen wir alle Backslashes () durch den Verzeichnis-Trenner (Slash,
/
) und hängen die Endung .php
an.
Aus MKMyPluginServiceService01
wird so also (...)/wp-content/plugins/my-plugin/src/Service/Service01.php
, was genau unserer Datei entspricht. Der Teil (...)
entspricht deinem lokalen Pfad zur WordPress-installation.
Mit dem obigen Code sollte unser Plugin also nun alle Klassen-Datein automatisch nachladen. Wir müssen nun also nur noch unseren überflüssigen, auskommentierten Code entfernen:
<?php
/**
* Plugin Name: MyPlugin
*/
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
spl_autoload_register(function(string $className) {
// MKMyPlugin vom Klassennamen durch den Pfad zu src ersetzen:
$className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
// Die restlichen Backslashes durch Verzeichnis-Trenner (Slashes) ersetzen und .php anhängen
$classFile = str_replace('\\', '/', $className) . '.php';
// Klassen-Datei laden
require_once $classFile;
});
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Doch wenn wir diesen Code nun so umsetzen und in unserer WordPress-Installation die Seite neu laden, werden wir mit einem neuen Fehler konfrontiert:
Warning: require_once(WP_Site_Health.php): Failed to open stream: No such file or directory
in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 27
Fatal error: Uncaught Error: Failed opening required 'WP_Site_Health.php' (include_path='.:/usr/share/php:/www/wp-content/pear')
in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php:27
Stack trace: #0 [internal function]: {closure}('WP_Site_Health')
#1 (...)/wp-settings.php(604): class_exists('WP_Site_Health') #2 (...)/wp-config.php(93):
require_once('...') #3 (...)/wp-load.php(50): require_once('...')
#4 (...)/wp-admin/admin.php(34): require_once('...') #5 (...)/wp-admin/plugins.php(10):
require_once('...') #6 {main} thrown in (...)/wp-content/plugins/autoload-plugin-02/autoload-plugin-01.php on line 27
Was passiert hier?
PHP versucht, die WordPress-Core-Klasse WP_Site_Health
zu laden, woraus unser Callback-Code die Datei WP_Site_Health.php macht, und diese ist so natürlich nicht zu finden.
Der Autoloader-Callback, den wir mit spl_autoload_register()
registrieren, wird im globalen Namespace deklariert. Wir müssen also dafür sorgen, dass wir nur Klassennamen verarbeiten werden, die auch tatsächlich in unserem Namespace vorhanden sind, da PHP den Callback bei jeder geladenen Klasse verwendet, die nach der Registrierung geladen wird. So also auch für WordPress-Core-Klassen.
Um zu gewährleisten, dass nur Klassennamen in unserem Autoloader behandelt werden, die tatsächlich in unserem Namespace liegen, fügen wir folgende Überprüfung ein:
<?php
/**
* Plugin Name: MyPlugin
*/
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
spl_autoload_register(function(string $className) {
if (false === strpos($className, 'MK\\MyPlugin')) {
return;
}
// MKMyPlugin vom Klassennamen durch den Pfad zu src ersetzen:
$className = str_replace('MK\\MyPlugin\\', __DIR__ . '/src/', $className);
// Die restlichen Backslashes durch Verzeichnis-Trenner (Slashes) ersetzen und .php anhängen
$classFile = str_replace('\\', '/', $className) . '.php';
// Klassen-Datei laden
require_once $classFile;
});
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Mit if (false === strpos($className, 'MK\MyPlugin'))
überprüfen wir also, ob der Anfang unseres Namespaces im Klassennamen vorkommt. Falls nicht (strpos
liefert false
), beenden wir den Callback mit return
.
Jetzt haben wir einen funktionierenden Autoloader, der nur die Klassennamen unseres Plugins behandelt.
Die manuelle Umformung des Klassennamens im Callback von spl_autoload_register()
und die Ausnahme-Behandlung von Klassennamen außerhalb des Namespaces des Plugins sind einerseits aufwändig und andererseits natürlich fehleranfällig.
Auch im Sinne der Code-Wiederverwendbarkeit schneidet diese Version des Autoloadings nicht gut ab, da wir hier immer manuell an mindestens zwei Stellen den Namespace anpassen müssen.
Es wäre doch viel schöner, wenn wir uns hier einer etablierten Lösung bedienen könnten. Und hier kommt Composer ins Spiel. Composer ist ein plattformübergreifendes Dependency-Management-Tool für PHP, das Entwickler*innen hilft, benötigte Bibliotheken und Pakete für ihre Projekte einfach zu verwalten und automatisch herunterzuladen. Zugleich bietet Composer auch ein auf PSR-4 basierendes Autoloading, das wir ab jetzt auch für unser Plugin einsetzen möchten.
Um Composer im Plugin verwenden zu können, müssen wir Composer zunächst einrichten. Dafür muss Composer bereits auf deinem Gerät installiert sein, dazu kannst du diesem Tutorial folgen.
Ist Composer installiert, öffnest du bitte ein Terminal (ich kann Warp empfehlen, wenn du auf einem Mac unterwegs bist, ansonsten hyper.js) und wechselst in dein Plugin-Verzeichnis.
Dann gibst du folgenden Befehl ein: composer init
. Der Composer config generator wird dich dann durch die Einrichtung führen. In den meisten Fällen kannst du die Standard-Angaben verwenden.
Da wir noch keine Abhängigkeiten definieren möchten, kannst du die Fragen Would you like to define your dependencies (require) interactively und Would you like to define your dev dependencies (require-dev) interactively mit no beantworten.
Dann folgt die Frage, die wir benötigen:
Add PSR-4 autoload mapping? Maps namespace “MarcuskoberMyPlugin” to the entered relative path. [src/, n to skip]:
Hier generiert Composer automatisch einen Namespace aus dem zurest angegebenen Package Name, der sich standardmäßig aus dem von dir gewählten Namen bei der Einrichtung von Composer und dem Verzeichnisnamen zusammensetzt. Wir bestätigen die Auswahl hier mit enter
, um den Namen und das vorgeschlagene src-Verzeichnis zu wählen.
Im Anschluss muss die Einrichtung noch mit einem yes bestätigt werden.
Composer legt nun ein vendor-Verzeichnis für dein Plugin an und die Config-Datei composer.json. Im vendor-Verzeichnis liegen nun bereits die Dateien, die Composer für das Autoloading benötigt und in diesem Verzeichnis landen später auch Pakete, falls du welche über Composer installierst.
Die Config-Datei sieht bei mir nun wie folgt aus:
{
"name": "marcuskober/my-plugin",
"autoload": {
"psr-4": {
"Marcuskober\\MyPlugin\\": "src/"
}
},
"authors": [
{
"name": "Marcus Kober",
"email": "marcus.kober@gmail.com"
}
]
}
In meinem Fall muss ich hier unter "psr-4"
noch den Namespace korrigieren, damit er zu unserem gewählten Namespace passt. Das kann bei dir – je nach Wahl des Namens, Verzeichnisses und Namespaces – auch nötig sein.
Wir ändern dies (Marcuskober zu MK) also entsprechend ab:
"psr-4": {
"MK\\MyPlugin\\": "src/"
}
Haben wir den Namespace in der composer.json geändert, müssen wir Composer dies noch mitteilen, damit er seine Autoload-Dateien entsprechend anpasst. Dazu geben wir im Terminal den folgenden Befehl ein: composer dump-autoload
. Meldet Composer im Terminal Generated autoload files
zurück, haben wir Composer korrekt eingerichtet.
Das einzige, was wir tun müssen, um Composer Autoloading in unserem Plugin zu verwenden, ist die require_once()
-Aufrufe zu entfernen und die Datei vender/autoload.php zu laden:
<?php
/**
* Plugin Name: MyPlugin
*/
use MK\MyPlugin\Main\Class01;
use MK\MyPlugin\Main\Class02;
use MK\MyPlugin\Main\Service01;
require __DIR__ . '/vendor/autoload.php';
$service01 = new Service01();
$class01 = new Class01();
$class02 = new Class02($service01);
Und lediglich mit dem Einfügen der Zeile require __DIR__ . '/vendor/autoload.php';
funktionert das Autoloading mit Composer direkt out of the box!
Wie du siehst, ist das Autoloading mit Composer kein Hexenwerk und im Gegensatz zu spl_autoload_register()
schnell und unkompliziert umgesetzt.
Haben deine Plugins einen gewissen Umfang erreicht, empfehle ich dir, das Autoloading über Composer zu verwenden. Auch wenn du die Einstellungen frei anpassen kannst, zwingt dich das Autoloading doch zu einer sauberen Benennung der Klassen, einer logischen Ordner-Struktur und die Verwendung eines einzelnen src-Verzeichnisses für deine PHP-Dateien. Und glaube mir, der Zwang ist an dieser Stelle rein positiv zu betrachten.
In den kommenden Artikeln dieser Serie werden wir uns tiefer mit der Entwicklung und Strukturierung umfangreicher Plugins befassen und dabei wird der Code immer anschaulicher werden, da wir nach diesen recht theoretischen ersten Artikeln tatsächlich verwendbaren Code sehen werden!
Im nächsten Artikel befassen wir uns mit Hooks und deren Registrierung aus deinem Plugin heraus.
Abonniere meinen Newsletter
Abonniere jetzt meinen Newsletter und verpasse nie mehr einen neuen Artikel. Kein Mist, kein Spam – ich sende dir nur eine Mail, sobald ein neuer Artikel erscheint.
Ist dir ein Fehler oder ein Problem in diesem Artikel aufgefallen, oder möchtest du sonst etwas zum Thema sagen? Schreib mir gerne!