Das Leben eines WordPress-Widgets


Ich bin noch immer mit dem Optimieren von WordPress beschäftigt und mache ganz gute Fortschritte. Auch das dazugehörige Plugin „WP Performance Pack“ kommt voran, aber leider lässt sich vieles nicht in das Plugin schieben, da Core-Funktionen angepasst werden müssen. Aktueller Stand: Laufzeit der Test-Installation ohne Optimierung 980 MS und mit Optimierung 494 ms. Aber darum soll es jetzt nicht gehen.

In meinem letzten Post lamentierte ich bereits, dass die WordPress-Entwickler alles immer instanziieren (bzw. in dem Fall immer übersetzten). Von der derzeit noch beim Aufruf der Startseite ausgeführten 140 Übersetzungen stammen gute 35 von Widgets. Zwar benutzt das verwendete Theme (Twenty Thirteen) einige Widgets, aber nicht alle. Dennoch werden alle Widgets instanziiert. Wohl auch wieder nur weil’s geht. Das kostet Zeit und Speicher. Das Problem: Die Verarbeitung von Widgets in WordPress ist nicht so einfach zu durchschauen (und zeugt IMO nicht gerade von gutem Verständnis von OOP). Deshalb will ich versuchen das „Leben eines Widgets“ hier einmal zu „dokumentieren“.

Seit WordPress-Version 3.irgendwas beginnt jedes Widget sein Dasein als Ableitung der Klasse WP_Widget. So auch WP_Widget_Pages, das erste Widget, das in default-widgets.php deklariert wird. Am Ende der Datei wird das Widget mittels register_widget(‚WP_Widget_Pages‘); im System registriert.

An register_widget wird einfach der Klassenname übergeben und die Funktion macht nichts anderes, als die Klasse im globalen Objekt wp_widget_factory zu registrieren, indem die Methode register aufgerufen wird. Diese Methode wiederum legt einen Eintrag im Array widgets des Factory-Objekts mit dem Klassennamen als Key an, und weist diesem eine Instanz der zu registrierenden Widget-Klasse zu:

function register($widget_class) {
  $this->widgets[$widget_class] = new $widget_class();
}

Warum?! Die Klasse heißt Widget_Factory, sollte also dem Namen nach dafür zuständig sein, Instanzen zu erzeugen. Das macht sie aber nur ein einziges Mal, bei eben jenem Register Aufruf, danach nie wieder. Liegt wohl daran, dass Widget-Klassen in einer verqueren Mischung als Namespace und Klasse verwendet werden. Naja, dazu später mehr. Konzentrieren wir uns zunächst auf das Leben des Widgets, das derzeit als Objekt im Array widgets existiert.

Die nächste Stufe seines Lebens erreicht das Widget wenn nach dem Registrieren aller Default-Widgets in der gleichnamigen Datei do_action(‚widgets_init‘); ausgeführt wird. Dies ruft wieder die Widget-Factory auf den Plan, zum zweiten und eigentlich auch letzten Mal. Die Methode _register_widgets die wie folgt aussieht:

function _register_widgets() {
  global $wp_registered_widgets;
  $keys = array_keys($this->widgets);
  $registered = array_keys($wp_registered_widgets);
  $registered = array_map('_get_widget_id_base', $registered);

  foreach ( $keys as $key ) {
    // don't register new widget if old widget with the same id is already registered
    if ( in_array($this->widgets[$key]->id_base, $registered, true) ) {
      unset($this->widgets[$key]);
      continue;
    }

    $this->widgets[$key]->_register();
  }
}

Hier werden alle im internen Array widgets gespeicherten Widgets daraufhin geprüft, ob ein gleichnamiger Eintrag bereits im globalen Array wp_registered_widgets existiert. Ich geht anscheinen um Abwärtskompatibilität, darauf deutet ja auch der Kommentar hin.

Existiert kein „altes“ Widget mir gleich ID, dann darf das Widget seine Methode _register ausführen (Funktion Nummer vier mit irgendwas mit register im Namen – und ja, es werden noch mehr werden). Dieser Methode der Klasse WP_Widget ist der Kommentar

// Private Functions. Don't worry about these.

vorangestellt. Der führende Unterstrich im Funktionsnamen soll das scheinbar verdeutlichen (ist das PHP-typisch?). Dabei bietet PHP doch sogar die Möglichkeit zur Deklaration als private. Aber egal, darum geht es ja nicht.

Was macht WP_Widget->_register()? Als erste lädt es mal die Einstellungen des Widgets. Oder genauer, es werden die Einstellungen aller Widget-Instanzen dieser Klasse geladen. Denn die werden zusammen gespeichert. Mit den Einstellungen jeder einzelnen „Instanz“ wird dann nochmal die Methode _register_one aufgerufen – dazu wird zuvor mit der Methode _set eine Art interner Datenpointer auf die Nummer der zu bearbeitenden Instanz gesetzt. Immerhin werden tatsächlich mal Datenfelder des Objekts verwendet. Sind keine Einstellungen für irgendwelche Instanzen gespeichert, dann wird trotzdem eine Pseudo-Instanz mit der Nummer eins registriert:

if ( $empty ) {
  // If there are none, we register the widget's existence with a
  // generic template
  $this->_set(1);
  $this->_register_one();
}

Sollte die Existenz des Widgets nicht schon durch eine der zig vorangegangenen Register-Funktionen bekannt sein? Naja, wie auch immer. Hier ist _register_one:

/** Helper function: Registers a single instance. */
function _register_one($number = -1) {
  wp_register_sidebar_widget( $this->id, $this->name, $this->_get_display_callback(), $this->widget_options, array( 'number' => $number ) );
  _register_widget_update_callback( $this->id_base, $this->_get_update_callback(), $this->control_options, array( 'number' => -1 ) );
  _register_widget_form_callback( $this->id, $this->name, $this->_get_form_callback(), $this->control_options, array( 'number' => $number ) );
}

Die insgesamt noch drei Register-Funktionen aufruft. Zählt noch jemand mit? Viel verschlungener kann es doch nicht mehr werden, oder? Hat bestimmt alles mit Abwärtskompatibilität zu tun. Aber man hätte im Laufe der Versionen vielleicht das ein oder andere über Bord werfen können. Und wenn schon nicht das, dann sollte es doch eigentlich so sein, dass die alten Funktionen intern auf die neuen Strukturen umbiegen, und nicht die neuen Klassen intern mit den alten Funktionen arbeiten. Aber so wirkt es hier.

Zurück zu _register_one:  Zunächst wird wp_register_sidebar_widget aufgerufen. Diese Funktion macht im Prinzip nichts anderes, als das Widget (bzw. Informationen über die Widget-Instanz, darunter eine Referenz auf die Methode widget der Ableitung von WP_Widget, also auf die Methode, die das Widget „zeichnet“) im globalen Array wp_registered_widgets einzutragen. Die beiden anderen Funktionen machen ähnliches, nur verwenden sie andere globale Arrays (wp_registered_widget_controls und wp_registered_widget_updates) und tragen dort Verweise auf die entsprechenden update bzw. form Methoden ein.

Das Widget (und seine Instanzen) ist jetzt fertig registriert. Mehr nicht. Dazu wurde es von einer Klasse (im ersten register-Aufruf wird noch der Klassenname übergeben) zu einem Objekt (die Factory legt zum Klassennamen eine Instanz davon an) und wird dann zerrupft in „Instanz-Einstellungen“ (da handelt es sich schon nicht mehr um Instanzen im OOP-Sinn) und diese werden dann nochmal zerteilt und in diverse Arrays gespeichert. Gestrecktes und gehacktes Widget. Würden wir WordPress-Burger machen wollen ggf. nicht schlecht.

Der ganze Spaß wird mit jedem Widget gemacht, egal ob eine „Instanz“ davon im aktuellen Aufruf angezeigt oder anderweitig verwendet wird, oder nicht. Ich bin gerade zu faul zu zählen, aber sehen wir vom unnötig verwendeten Speicher und den unnötigen Funktionsaufrufen ab, dann haben wir vor allem unnötige Datenbank-Abfragen für Widget-Einstellungen, die höchstwahrscheinlich niemand braucht. Effizienz sieht anders aus. Einfach zu verstehender Code auch.

Aber wann werden denn nun die Widgets verwendet? Das kommt drauf an: Im Theme wird es auf Umwegen über dynamic_sidebar aufgerufen. Hier wird weder die Widget-Factory, noch die Widget-Klassen verwendet, sondern lediglich die in den Arrays gespeicherten Informationen. Auch im Backend läuft ein Großteil der Anzeige von Widgets über Sidebars. Darauf will ich jetzt hier nicht weiter eingehen, sonst wird alles noch unübersichtlicher.

Aber eines muss doch sein: Für die Anzeige der Widget-Formulare im Backend (zum Anpassen der Widget-Einstellungen) wird unter anderem temporär im Array wp_registered_widgets der die Callback-Funktion zur Anzeige des Widgets auf die interne Funktion wp_widget_control umgebogen, die den HTML-Code des Formulars erzeugt. Eigentlich etwas, das eher in die Widget-Klasse gehört, aber egal. Packen wir’s einfach in eine völlig andere Datei (die genauso heißt – es gibt drei Dateien mit dem Namen widgets.php eine in wp-includes, eine in wp-admin und eine in wp-admin/includes).

Genug lamentiert, es wird Zeit für Verbesserungsvorschläge. Darauf werde ich erst einmal nur allgemein eingehen, da ich ja noch genau daran arbeite. Und einer der Gründe für diesen Post war es, selbst einen Überblick über die Funktionsweise der Widgets zu erhalten. Für mich hat’s einigermaßen geklappt. Ob es für den unbedarften Leser alles verständlich ist, ist eine andere Sache. Hier also die Punkte für meine geplante Implementierung der Widgets:

  • Nur noch Verwendung der Klassen und deren Instanzen (könnte zu Problemen mit Plugins führen, die sich auf das Vorhandensein der diversen Arrays verlassen. Ggf. kann man hierfür die Arrays umbiegen auf die Klassen/Objekte).
  • Alle „globalen“ Eigenschaften eines Widgets (also Eigenschaften, die unabhängig von der Instanz sind) über statische Felder/Methoden implementieren.
  • Instanzen der Widgets sind Instanzen der Klassen, also Objekte.
  • Widget-Factory ist zuständig für das Erzeugen einer Instanz, die auch erst dann erzeugt werden, wenn sie wirklich angezeigt werden sollen.
  • Bündeln diverser Widget-Funktionen in der Widget-Basisklasse und der Widget-Factory.
  • und was weiß ich nicht noch alles…

Das wird nicht einfach werden.