Tutorial: HookUp

Klassen und Objekte

Wir tauchen nun etwas tiefer ab in die Materie und dazu braucht es etwas Verständnis zu Klassen und Objekten. In PHP ist eine Klasse nicht nur ein Entwurfsmuster für Objekte, sondern kann selber zu einen Objekt werden. Das hat Vor- aber auch Nachteile, schauen wir es uns mal im Beispiel an:

class Demo_Hook
{
  public $msg = 'Hello World!';

  public function __construct( $msg = '' ) {

    if ( ! empty( $msg ) ) {
      $this->msg = $msg;
      $this->print_msg();
    }

  }

  public function print_msg() {

    echo $this->msg . '<br>';

  }

  public static function static_print_msg( $msg = '' ) {

    if ( ! empty( $msg ) )
      echo $msg . '<br>';

  }

  public function get_msg() {

    return $this->msg;

  }

}

Nehmen wir diese Klasse mal als Beispiel. Wir haben verschiedene Möglichkeiten um nun Methoden aus dieser Klasse Aufzurufen. Zuerst einmal die beiden “sauberen” Wege:

// 
// eine Objekt-Instanz erzeugen und in einer Variablen speichern
$demo = new Demo_Hook( 'Hallo Horst!' );

$demo = new Demo_Hook( 'Hallo Horst!' );
$demo->msg = 'Hallo Heidi!';
$demo->print_msg();

// Objekt-Instanz erzeugen, jedoch eine statische Methode aufrufen
$demo = new Demo_Hook();
$demo::static_print_msg( 'Hallo München!' );

// keine Objekt-Instanz erzeugen und eine statische Methode aufrufen
Demo_Hook::static_print_msg( 'Hallo Hallo!' );

Wir können aus der Klasse Demo_Hook ein Objekt erzeugen das in der Variablen $demo gespeichert wird. Somit können wir im weiteren Verlauf immer wieder auf das Objekt das in $demo gespeichert ist zugreifen. Die zweite Variante besteht darin, eine statische Methode aufzurufen. Entweder über eine Objekt-Instanz die wir vorher erzeugt haben. Oder ganz direkt über Klassenname::Methoden-Name.

Nun die dunkle Seite der Medaillie, die “schmutzigen” Wege:

new Demo_Hook( 'Hallo Horst!' );
new Demo_Hook( 'Hallo Heidi' );

Warum ist das “schmutzig”? Ganz einfach: Wir erzeugen etwas, dass wir nicht mehr ansprechen können. PHP erzeugt hier zwei Objekt-Instanzen ohne das sie einen Namen in Form einer Variablen zugewiesen bekommen. Man könnte sie auch “anonyme Objekte” nennen. Wir können die Objekte nicht mehr modifizieren, können sie nicht löschen und auch keine Methoden von ihnen aufrufen. Es wäre uns z.B. unmöglich die Methode get_msg() aufzurufen und heraus zu bekommen mit welchen Wert für $msg die Objekt-Instanzen erzeugt wurden.
Leider unterstützt PHP das Erzeugen von “anonymen Objekten”, womit eine Klasse nicht nur ein reines Entwurfsmuster für Objekte ist. Man sollte Klassen aber immer als reines Entwurfsmuster betrachten von dem man kein anonymes Objekt erzeugen darf. Denn wir sind nicht alleine im PHP-Universum, unsere Objekte interagieren in der Regel mit anderen PHP-Code. Das anderer PHP-Code von unseren anonymen Objekten abhängen kann, behalten wir an dieser Stelle mal im Hinterkopf und merken uns das eine Klasse ein Entwurfsmuster ist und ein Instanz-Objekt ein aus dem Entwurfsmuster erzeugtes Objekt.

Wie WordPress mit Objekten umgeht wenn sie als Callback für einen Hook eingesetzt werden

Wir wissen nun das WordPress die Callbacks für einen Hook als Index verwendet. Bei einfachen Funktionen nutzt es dazu den Funktionsnamen, bei statischen Methoden eine Kombination aus Klassennamen, Doppelten-Doppelpunkt und Methoden-Namen. Und wir hatten auch schon gesehen das WordPress bei Objekten einen Hash-SWert verwendet den man nicht voraus sagen kann.
Nun stellt sich doch die Frage warum WordPress diesen blöden Hash-Wert verwendet und nicht so wie beim Aufruf von statischen Methoden den Klassennamen und Methoden-Namen. Schauen wir uns dazu das zweite Plugin “02_HookUp Tutorial – Part Two” aus dem Git-repo an.

Das Plugin macht folgendes: Es zeigt auf allen Seiten im Backend (Admin-Bereich) eine Nachricht an, ausgenommen auf dem Dashboard, dort wird eine andere Nachricht angezeigt. Dazu holt sich das Plugin die Klasse PrintAdminNotice (require_once 'class-printadminnotice.php';) und erzeugt davon ein Objekt ($msg_red = new PrintAdminNotice( 'Woohoo! The plugin works!' );). Danach prüft es ob wir uns auf dem Dashboard befinden, ist dies der Fall, wird die ursprüngliche Nachricht deaktiviert und eine andere Nachricht Registriert. Und hier wird es für uns jetzt interessant. Denn anstatt das ursprüngliche Objekt zu modifizieren, also z.B. einfach den Nachrichtentext auszutauschen, erzeugt es ein zweites Objekt von der gleichen Klasse!
Wir haben nun zwei Objekte die aus der gleichen Klasse erzeugt wurden.

Wie würden wohl die Index-Einträge aussehen wenn WordPress nach dem Muster Klassenname::Methoden-Name vorgehen würde? Da beide Objekte aus der gleichen Klasse erzeugt wurden und in beiden Fällen die gleiche Methode aufgerufen wird, wäre es für beiden Objekte AdminPrintNotice::print_notice. Ziemlich blöd, denn so wüsste WordPress nicht welchen Callback es entfernen muss sobald wir uns im Dashboard befinden. Und da in inem Array jeder Index einzigartig ist, wäre es auch gar nicht möglich zwei unterschiedliche Objekte zu registrieren.

Wobei das somit auch gleich die Frage beantwortet warum man es nicht mit statischen Methodenaufrufen umsetzen kann (PrintAdminNotice::print_notice()). Mit statischen Methodenaufrufen können wir immer nur direkt auf die Methoden der Klasse zugreifen, was bedeutet das wir auf das Entwurfsmuster zugreifen. Um mit statischen Methodenaufrufen eine andere Nachricht auszugeben, müssten wir das Entwurfsmuster (also die Klasse) ändern. Dies ist zwar möglich, jedoch nicht Sinn und Zweck der Geschichte.

Der Hash der Probleme macht

Wie schafft WordPress also die beiden Objekte voneinander zu unterscheiden und das richtige Objekt bzw. den Callback zu entfernen? Indem es einen Hash-Wert von dem Objekt erzeugt. Dazu nutzt es die PHP-Klasse spl_object_hash() die für jedes Objekt einen eindeutigen Hash-Wert erzeugt. WordPress bildet aus dem Hash-Wert und den Methoden-Namen einen eindeutigen Index, dies erledigt die Funktion _wp_filter_build_unique_id(). Wen es interessiert, der kann sich die Funktion mal in den Core-Files anschauen.
Wollen wir nun einen Callback aus einem Objekt wieder entfernen, benötigen wir genau diesen Hash-Wert bzw. die von WordPress erzeugte unique id. Um an diese ran zu kommen, benutzen wir einfach die gleiche Funktion wie WordPress und erhalten so den benötigten Index.

  remove_action(
    'admin_notices',
    _wp_filter_build_unique_id(
      'admin_notices',
      array( $msg_red, 'print_notice' ),
      false
    )
  );

Sollten wir nicht etwas im Hinterkopf behalten? Irgendwas von wegen wir wären nicht alleine im PHP-Universum, Code der von unserem abhängt und anonyme Objekte? Genau, da sind wier wieder beim Thema “Gute Scripte – Schlechte Scripte (GSSS)”. Wer es schon wieder vergessen oder erst gar nicht verinnerlicht hat, springt noch mal schnell zum Abschnitt “Klassen und Objekte”.
Klar hätte ich es mir einfach machen können und die Klasse PrintAdminNotice so schreiben können das ein Aufruf á la AdminPrintNotice( 'Some message' ); gereicht hätte. Aber wie hätte ich dann den Callback wieder entfernen sollen? Die Funktion _wp_filter_build_unique_id() benötigt nun einmal das Objekt um den Hash-Wert zu erzeugen. Habe ich das Objekt nicht in einer Variablen gespeichert, kann ich nicht mehr darauf zugreifen und somit keinen Hash-Wert erzeugen lassen. Nebebei erwähnt, ich hätte dann auch nicht mehr die Farbe von Rot auf Grün wechseln können.

Ein “anonymes Objekt” zu erzeugen ist mitunter die dümmste Idee die man als PHP-Programmierer haben kann. Allerdings ist es genauso dumm ein Objekt in einer Funktion zu erzeugen und das Objekt dann nicht global verfügbar zu machen (siehe das vierte Plugin im Git-Repo).

Schlimmer geht immer

Ab PHP5.3 gibt es dann noch eine Steigerung. Die nennt sich Closures oder auch anonyme Funktionen. Closures sind praktisch wenn man mal schnell eine Callback-Funktion benötigt. Da add_action() und add_filter() ja Callbacks brauchen, kommt man schnell mal auf die Idee in diesen Funktionen ein Closure zu verwenden.

add_action(
  'test_hook',
  function( $var ){
    Demo_Hook::print_var( $var );
  },
  0,
  1
);

add_filter(
  'the_title',
  function( $title ) {
    return strtoupper( $title );
  },
  0,
  1
);

Wer solche Boshaftigkeiten verwendet will offenbar absolut sicher gehen das wirklich niemand in der Lage ist den Callback wieder zu entfernen. Der Eintrag im globalen Array wp_filter könnte dann in etwa so aussehen:

array (size=1)
  '00000000050dbf4a000000004ae55b58' => 
    array (size=2)
      'function' => 
        object(Closure)[4778]
      'accepted_args' => int 1

Hier hat man absolut keinen Anhaltspunkt wonach man suchen sollte. Weder einen Hash den man irgendwie reproduzieren könnte noch einen Funktions- oder Methoden-Namen. Wer nicht ganz so boshaft sein möchte und dennoch Closures verwenden will, der weist den Closure einfach einer Variablen zu ($closure = functio(){}; und verwendet die Variable. So besteht zumindest die Chance das, wenn die Variable global verfügbar ist, der Callback wieder entfernt werden kann.