WordPress ist nicht Scheiße. Es ist nur anders.

WordPress aus Entwicklersicht – ein CMS mit vielen Fragezeichen.

Du kommst aus einem anderen CMS wie Joomla, Drupal, TYPO3, Craft – oder bist schon längere Zeit in Frameworks wie Symfony, Laravel oder Flow unterwegs? Jetzt stehst Du vor dem Kopfsprung in das am weitesten verbreitetste CMS der Welt… in die wunderbare Welt von WordPress? 

Dann erst Mal: Herzlich willkommen. Hier ist das Framework, bei der eine einzige PHP-Datei mit einem 5-zeiligen Kommentar reicht, um ein eigenes Plugin zu registrieren, das direkt im Backend erscheint und aktivierbar ist. Es ist das einzige Content-Management-System dieser Welt, bei der ein wiederverwendbarer „Shortcode“ mit einer einzige Zeile Code registriert werden kann, der direkt in allen Posts und Seiten funktioniert und nutzbar ist.

WordPress? Einfach maximal einfach.

WordPress hat zu Recht den Ruf, auch aus Sicht eines Entwicklers maximal „einfach“ zu sein. Der Core kommt mit der wahrscheinlich einfachsten API dieser Welt, um eigene Plugins/Extension zu programmieren. Das macht bei dem ersten Gehversuchen in WordPress nicht nur wahnsinnig viel Spaß und bringt schnelle Erfolgserlebnisse. Es zieht auch Massen an Entwicklern an, die wenig bis keine Kenntnisse in PHP mitbringen.

Auf der anderen Seite gibt es aber Entwickler, die eine Menge an Erfahrungen aus anderen Frameworks und CM-Systemen mitbringen und zum Teil seit Jahren nur noch in MVC-Patterns, Relationen, Klassen, Namespace, Abstraktionsschichten und Template-Engines denken. Diese Entwickler stehen bei WordPress plötzlich vielen, vielen Fragezeichen gegenüber. Globale Variablen? Keine Klassen im Core sondern (fast) alles basierend auf globalen Funktionen ohne Namespacing?

Coden, back-to-the-roots

Coden fühlt sich plötzlich sehr „back-to-the-roots“ an. Schnell hat man nostalgische Flashbacks an seine eigenen Jugendzeiten als Coder. „Weißt Du noch, wie wir damals statische Webseiten programmiert haben und dann die ersten PHP-Versuche gemacht haben? Kannst Du Dich an Deinen ersten PHP-Tag erinnern? Und dann die vielen ‚echos‘ im Code? Ja, ja, das waren noch Zeiten.“

Für genau diese Entwickler ist der folgende Blog-Beitrag gedacht. Anders als viele, viele Tutorials, die PHP-Einsteiger an die Entwicklung von WordPress heranführen, nimmt dieser Artikel erfahrene Entwickler an die Hand und macht einen Rundflug über die Themen, bei denen WordPress  von allen den gängigen Praxen und modernen Konzepten abweicht.

Sieh diese Liste bitte als eine Sammlung von Fragezeichen an. Es sind Dinge, die wir als selbstverständlich für ein so etabliertes CMS gehalten hatten. Dinge, die wir verzweifelt vermissen – oder (vielleicht aus Blindheit) einfach nicht finden.

Antworten? Immer her damit!

Es sind aber auch Fragezeichen, hinter die wir ein Ausrufezeichen setzen möchten. Deshalb die Aufforderung an alle WordPress-Cracks da draußen: Korrigert uns. Helft uns. Schreibt uns, wo wir auf dem Schlauch stehen oder uns in längst überholten Versionen von WordPress bewegen. Wir sind dankbar für jeden Tipp und Hinweis, der uns (und alle Entwickler, die vor ähnlich Fragen) weiter bringt! Und, falls ihr zusätzliche Dinge für die Liste habt: Immer her damit!

Dependencies

Nehmen wir an, wir haben zwei Plugins. Das eine Plugin basiert / ist abhängig von Funktionen des zweiten Plugins oder erweitert diese. In den meisten CM-Systemen kann man Abhängigkeiten innerhalb der Extension definieren, z.B. in einer composer.json-Datei. Der Core übernimmt dann zentral das Laden der Plugins / Extensions in der erforderlichen Reihenfolge und ggf. auch das automatische Installieren der fehlenden Plugins. In WordPress gibt es eine solche Dependency-Definition nicht. Stattdessen muss man über PHP innerhalb seiner Extension nach der Präsenz der anderen Extension fragen und dann den User zum Installieren der fehlende Extension auffordern:

if (!is_plugin_active('nnhelpers/nnhelpers.php')) {
   return 'Plugin nnhelpers muss aktiviert sein, damit nninfografik funktioniert!';
}

Datenbank-Abstraktionsschicht

Beim Schreiben eigener Queries an die Datenbank sucht man zunächst nach einer Abstraktionsschicht wie Doctrine o.ä. um Queries systemunabhängig und vorallem sicher konstruieren zu können. MySQL-Queries in WordPress scheinen sehr low-level zu sein. Die API dazu sieht laut Dokumentation so aus:

global $wpdb;
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}options WHERE option_id = 1", OBJECT );

WordPress spricht selbst zwar von einer „Abstraktionsschicht der Datenbank“ – aber wer jetzt in Begriffen wie „queryBuilder“ denkt, liegt komplett daneben. Treffender wäre es wohl, die globale $wpdb-Klasse als „Wrapper“ zu bezeichnen. Das erste Argument der meisten Methoden erwartet nämlich eine „selbstgebaute“ MySQL-Query. Und für diese Query bist Du (fast) komplett auf Dich selbst gestellt – und auch selbst verantwortlich. Du musst Dich um den Schutz vor MySQL-Injections kümmern. Folglich gibt es auch kein Caching der Query-Ergebnisse. Wenn ein Plugin an mehreren Stellen die gleichen Daten aus der Datenbank braucht (z.B. für das Rendern verschiedener Views innerhalb einer Seite), dann musst Du Dich selbst um das Caching der Daten kümmern. Die API von WordPress führt die Query immer und immer wieder neu aus.

Ähnlich verhält es sich mit dem Anlegen eigener Datenbank-Tabellen oder dem Erweitern vorhandener Tabellen. Auch hier bietet WordPress scheinbar kein wirkliches Konzept. Das Prüfen, ob eine MySQL-Tabelle vorhanden ist und das Ergänzen oder Anlegen der Felder für die Tabelle läuft auf komplette Handarbeit hinaus. Es gibt aber einen Hook, der beim Installieren des Plugin aufgerufen wird und für diese Zwecke genutzt werden kann:

register_activation_hook( __FILE__, 'my_plugin_create_db' );

function my_plugin_create_db() 
{
   global $wpdb;
   $charset_collate = $wpdb->get_charset_collate();
   $table_name = $wpdb->prefix . 'my_analysis';

   $sql = "CREATE TABLE $table_name (
      id mediumint(9) NOT NULL AUTO_INCREMENT,
      time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
      views smallint(5) NOT NULL,
      clicks smallint(5) NOT NULL,
      UNIQUE KEY id (id)
   ) $charset_collate;";

   require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
   dbDelta( $sql );
}

Namespacing

Die komplette API von WordPress basiert auf globalen functions ohne Namespacing. Viele der Plugins folgen diesem Vorbild. Das ist kein größeres Problem, so lange alle Plugins sich „aus dem Weg gehen“ und Funktionen nicht gleich benennen. Bei > 55.000 WordPress Plugins besteht hier allerdings ein gewisses Risiko (wenngleich man natürlich kaum alle in einer Installation haben wird 😉

Es gibt einige Blogbeiträge, wie man Namespacing in WordPress nutzen kann – allerdings wird es wohl keine wirkliche „Konvention“ dazu geben, bis der Core nicht grundsätzlich auf dieses Konzept setzt. Das wiederum wäre ein großer Bruch und würde eine Inkompatibilität / ein Update fast aller WordPress-Plugins dieser Welt bedeuten…

autoloading

Fast überflüssig zu sagen: Ein System, dem composer und namespacing fremd ist, bringt auch keine API oder ein Konzept für das autoloading von Klassen mit sich. Auch hier ist man auf sich selbst gestellt. Das bedeutet: Viele require_once(...) – oder eine alternative Strategie. PHP bringt z.B. seit Version 7 die schöne Funktion spl_autoload_register mit.  Die Implementierung muss man als Entwickler des WordPress Plugins selbst vornehmen, z.B. in dieser Art:

spl_autoload_register( function($classname) 
{
   $paths = explode('\\', str_replace( 'Nng\Nnhelpers\\', '', $classname ));
   $pathParts = [dirname(__FILE__) . '/Classes'];
   $filename = array_pop($paths) . '.php';

   foreach ($paths as $part) {
      $pathParts[] = $part;
   }
   $pathParts[] = $filename;

   $filename = join( DIRECTORY_SEPARATOR, $pathParts );

   if ( file_exists( $filename) ) {
      include_once $filename;
   }
});

Die Template-Engine in WordPress heißt: PHP

Jeder, der in einem CMS ein Interface oder eine View bauen will, fragt als erstes: „Und mit welcher Template-Engine arbeitet das CMS?“. Template-Engines sind einer der wichtigsten Bausteine bei der Entwicklung der sichtbaren Oberfläche (des HTML-Codes), um nicht mit tausenden von <?php echo '...' ?> arbeiten zu müssen. Durch eine Template Engine wird die Logik von der View getrennt (MVC), der Code wird übersichtlicher, besser lesbar und wiederverwendbar. Einige der bekanntesten Projekte sind: Twig, Mustache, Smarty und Fluid.

Also: Zurück zur Frage. Mit welcher Template Engine arbeitet WordPress? Kurze Antwort: Mit PHP. Als Argument für PHP für das Templating wird oft herangezogen: Es ist von der Performance schneller, jeder versteht es – und es kann alles.

Das Argument „schnell“ ist schnell entkräftigt: Jede vernünftige Engine übersetzt die Templates ohnehin in „reinen“ PHP-Code, der gecached wird. Ein Performance-Vorteil ist nur marginal, weil es evtl. ein paar weniger Funktionsaufrufe gibt.

„Jeder versteht es“ stimmt zwar – allerdings nur, wenn man PHP versteht. In einer Agentur gibt es aber auch klassische „Frontender“, die sich hervorragend durch die Gewässer von HTML, CSS und JS navigieren – aber PHP eher fremd sind. Eine gute Arbeitsteilung im Team ist also kaum möglich – und der „Backender“ lebt permanent mit einer gewissen Angst, sein „Frontender“ könnte durch sein PHP-Template das gesamte Plugin zum Abschmieren bringen. Oder noch schlimmer: Durch Unwissenheit gravierende Sicherheitslücken ins Template einschleusen (Stichwort: Code-Injections).

„Es kann alles“ kann man stehen lassen. Aber eben auch nur mit gewissen Einschränkungen. Wer das Konzept von ViewHelper kennt, weiß: Eine gute Template-Engine ist wunderbar erweiterbar – und zwar deutlich übersichtlicher, zentraler und wiederverwendbarer als mit reinem PHP-Code.

Um das Drama plastisch zu machen, ein Beispiel für die gängige Praxis bei WordPress-Plugins. PHP wird quer durch Funktionen geschossen und damit die Business-Logik direkt und unzertrennlich mit den templates verwoben. Das sieht dann so aus:

<?php
function edit_form_fields ($tag) {
    $termid = $tag->term_id;
    $cat_meta = get_option( "tax_$termid");
?>
    <tr class="form-field">
       <th valign="top" scope="row">
           <label for="catpic"><?php _e('Detailed Description', ''); ?></label>
       </th>
       <td>
            <textarea name="demo"><?php echo  $cat_meta;?></textarea><br />
            <span class="description">Entering detail description</span>
       </td>
    </tr>
<?php
}

Tatsächlich sieht man diese Art von Code-Wirrwar auch in vielen gut strukturierten und etablierten WordPress-PlugIns. Man findet Klassen, die mit Namespacing arbeiten, was auf erfahrenere Entwickler schließen lässt – dann aber, mitten in einer Methode trifft man auf schließendes PHP-Tag ?> mit HTML-Code, Tags, Inline-JavaScript und CSS (!) und nach dem kurzen Schwenk in eine komplett andere Sprache und Logik geht es fröhlich mit <?php ... zurück ins Eingemachte.

Die Frage, ob und wie man jetzt vernünftig (und updatefähig) in seinem Child-Theme ein solches Template überschreiben könnte, stellt man sich eigentlich gar nicht erst.

Überschreiben/Vererbung von Templates

Absolute URLs – und das überall

Das WordPress-Projekt für den Kunden ist fertig. Der Umzug von Staging-System zum Live-System steht an. Also: Dump der MySQL-Datenbank, ein .tar der Dateien, wget auf dem Live-System. Datenbank-Zugangsdaten in der wp-config.php ändern. Seite aufrufen. Nichts geht mehr.

Einige Links führen auf den Staging-Server, ein Teil der Bilder und Medien wird noch vom Staging-Server geladen – das ist besonders blöd, wenn das erst auffällt, nachdem der Staging-Server abgeschaltet wurde. Aus anderem CM-Systemen kommend sucht man als erstes in der wp-config.php oder „Settings / Options“-Tabelle in der Datenbank nach einer Art zentraler baseURL, die geändert werden muss. Dort ist zwar etwas, aber das greift nicht überall. Am Ende stellt man fest: Ja, die Pfade zu den Medien und Links liegen tatsächlich als absolute URLs in der Datenbank – quer verteilt über die Felder der Blogs, Posts, Meta-Daten etc.

Ist doch kein Problem, sagen die WordPress-Experten jetzt. Dafür gibt es (wie für alles) schließlich ein Plugin. Und überhaupt: Warum machst Du noch ein eigenes tar, wo es doch dieses Plugin gibt. Du musst absolute URLs in der Datenbank ersetzen? Dann nimm doch einfach dieses Plugin.

Ja – es gibt immer für alles ein Plugin. Darum lieben wir WordPress. Aber sind wir mal ehrlich: Jedes dieser Plugins gleicht eigentlich nur ein fehlendes, vernünftiges Konzept im Core aus. Ein Plugin, das nach einem String in allen Datenbank-Tabellen und -Feldern sucht und ersetzt ist eine gefühlt eher „unheimliche“ Notlösung. Man könnte es auch anders bezeichnen: Als eine per se Einladung für massive Problem… nicht nur aus Sicht der Sicherheit. Dieses Plugin sollte aus unserer Sicht nach erfolgreichem Umzug direkt wieder entfernt werden.

Debugger? Kennt ihr noch „var_dump“ und „print_r“?

Dependency Injections

Posts, posts… und nochmal posts!

Shortcodes

Convention over Configuration?

WordPress selbst lässt unendlich viele Möglichkeiten zu, Plugins zu entwickeln. Es gibt natürlich zahlreiche Empfehlungen, aber da das Entwickeln eines Plugins mit absolute PHP-Basiskentnissen zu bewältigen ist, herrscht im Plugin-Universum von WordPress nur ein Dogma: Chaos over Convention.

Skurriles

Noch eine Sammlung an skurrilen Besonderheiten, die in Projekten aufgefallen sind:

Hooks und Anzahl der Argumente

Hooks gibt es massenhaft in WordPress. Registriert werden sie über die Funktion add_action(), meistens in dieser Art:

add_action( 'category_edit_form_fields', 'my_function');
function my_function() {
  // ...
}

Damit add_action weiß, wie viele Parameter die Funktion erwartet, übergibt man mein Registrieren als 4. Parameter eine Zahl. In diesem Beispiel werden an die Funktion my_function zwei Parameter übergeben.

add_action( 'category_edit_form_fields', 'my_function', 10, 2 );
function my_function( $param1, $param2 ) {
// ...
}