WordPress Hooks entfernen, die Closures und Objektmethoden haben

Lies dir diese Einleitung in WordPress Hooks durch und Strategien, wie du sie entfernen kannst – (mit einem Package made by Inpsyde).

Hooks sind schon seit den Beginnen von WordPress eine wichtige Säule der WordPress Entwicklung. Selbst wenn Actions und Filter einen sehr unterschiedlichen Umfang haben, ist ihre innere Durchführung fast gleich. In diesem Blogpost möchte ich euch eine Einführung in WordPress Hooks geben. Dabei fokussiere ich mich auf die Frage, wie du WordPress Hooks entfernen kannst, die Objekte verwenden.

Das macht einen Hook aus

Jede Action oder Filter besteht aus vier Elementen:

  • ein “Hook Name”, manchmal auch bezeichnet als “Hook Tag” oder nur “Action” / “Filter” oder auch nur “Hook”
  • ein Callback
  • eine Priorität
  • ein mögliches Set von Argumenten

Intern generiert WordPress also einen Callback Identifier und sein Wert ist nur für benannte Funktionen und statische Klassenmethoden berechenbar. Für die anderen drei Arten von Callbacks basiert der Wert auf spl_object_hash() und ist daher nicht berechenbar.

Die Nicht-Berechenbarkeit des Identifieres verursacht ein weit bekanntes Problem mit Hooks, die Objekte verwenden: Sie sind schwer zu entfernen.

Ein tieferer Blick auf das Issue

Sowohl wenn remove_action als auch remove_filter gerufen werden, kalkuliert WordPress den Identifiers des zu entfernenden Callback auf Basis der Argumente, die an diese Funktionen weitergegeben werden. Dann enfernt er von den registierten Hooks für den spezifischen Tag und die Priorität denjenigen, der den Callback verwendet, welcher mit dem berechneten Identifier identifiziert wurde.

Aus diesem Grund ist es am besten, die genauen Argumente (Tag, Callback und Priorität) an remove_action / remove_filter  weiterzugeben, die verwendet wurden, um den Hook hinzuzufügen, damit das Entfernen erfolgreich wird.

Wenn Callbacks Objekte einbeziehen, bedeutet “genau” “genaue Instanz” 

Zum Beispiel:
class SomeClass {
  
  public function __construct() {
    
    add_action( 'init', [ $this, 'on_init' ] );
    
    add_action( 'shutdown', [ $this, 'on_shutdown' ] );
  }
}

Die im Constructor hinzugefügten Actions können außerhalb der SomeClass nicht wie folgt entfernt werden:


remove_action( 'init', [ $this, 'on_init' ] );

weil $this nur innerhalb von SomeClass zugänglich ist.

In diesem Beispiel:
function add_some_hooks() {
  
  add_action( 'init', function() { /* something happen here */ } );
  
  add_action( 'shutdown', [ new SomeClass(), 'on_shutdown' ] );
}

können der Action, der mit dem Closure und der Action, der mit der soeben erstellten SomeClass Instanz hinzugefügt wurden, nicht entfernt werden, weil es keinen Weg gibt, Zugang zur Instanz zu bekommen. Auch so etwas:

remove_action( 'shutdown', [ new SomeClass(), 'on_shutdown' ] );

wird nicht funktionieren, weil die Instanz der SomeClass, die an remove_action weitergegeben wurde, nicht identisch ist zu der, die an add_action weitergegeben wurde. Das bedeutet, dass spl_objec_hash einen anderen Wert für sie wieder geben wird. Die Konsequenz daraus ist, dass der Identifier unterschiedlich sein wird.

Die verpasste Gelegenheit

Aus diesen Gründen empfehlen viele Entwickler eine schlechte Methode Callbacks mit Objekten zu und sie raten dazu zu, immer benannte Funktionen oder statische Methoden zu verwenden.

Ich persönlich bin sehr gegen diese Haltung, wenn Plugins objekt-orientiert geschrieben sind.

Außerdem denke ich, dass die Tatsache, dass einige der Funktionen der Plugins API gut mit einigen Callback Typen funktionieren und mit anderen nicht, ein Mangel der Plugins API ist. Etwas, dass in 2004 total verständlich war, als PHP von Version 4 nach Version 5 gewandelt wurde und Closures und unsichtbare Objekte noch nicht existierten und OOP PHP an seinen Anfängen war. Aber 14 Jahre später kann das meiner Meinung nach sicherlich als Defekt betrachtet werden, der vor langer Zeit besser hätte angesprochen werden müssen. Insbesondere weil vor gerade einmal einem Jahr (mit WP 4.7) ein großes Umschreiben des Inneren der Plugins API released wurde, ohne dass die Möglichkeit, das Issue irgendwie anzugehen, auch nur diskutiert wurde.

Strategien, um das Issue “WordPress Hooks entfernen” anzugehen

Plugins zu erstellen, die dynamische Methoden und Closures verwenden und dennoch Drittanbietercode die Möglichkeit geben, diese zu entfernen ist möglich, wenn du diese Strategien verwendest:

Strategie #1: Der “enabler” Filter

Wenn es ein Objekt gibt, das Hooks hinzufügt, ist es möglich einen Filter anzubieten, der als Flag handelt, um Abhängigkeit vom Hook zu aktivieren oder zu deaktivieren:


class SomeClass {
  
  public function initialise() {
    
    if ( ! apply_filters( 'some_class_enable', true ) ) {
      add_action( 'init', function() { /* */ } );
        add_action( 'shutdown', function() { /* */ } );
    }
  }
}

Mit solch einer Klasse kann Drittanbietercode verhindern, dass Hooks hinzugefügt werden, statt sie zu entfernen:

add_filter( 'some_class_enable', '__return_false' );

Dieser Einzeiler ist nicht nur einfach und kurz, er ist auch sehr lesbar und explizierter als vielfache Calls nach remove_actions , die keinerlei Effekt auf die API haben, um Klassenfunktionalitäten zu deaktivieren.

Außerdem ist es besser für die Rückwärtskompatibilität. Wenn das nächste Release der SomeClass andere Hooks hinzufügen wird, ist es möglich die Kompatibilität mit Drittanbietern zu erhalten, indem du einfach die 'some_class_enable' dort lässt: Drittanbieter schauen nicht danach, was innerhalb des if Blocks passiert.

Strategie #2: Enthülle das Objekt via Action

Wenn Objekte Hooks hinzufügen, ist es möglich sie mit Methoden auszustatten, die Funktionen aktivieren und deaktivieren und den zugehörigen Hookoperationen in diese Methoden einzuschließen.

Danach können die gleichen Methoden Actions auslösen, die $this als Argument übertragen, also das Objekt Drittanbietern zugänglich machen, welche Gebrauch von “Aktiviere / Deaktiviere Methoden” machen können.

Ein Beispiel:

class SomeClass {
  
  private $enabled = true;
  
  public function init() {
    
    add_action( 'init', function() {
      if ( $this->enabled ) {
        /* do something here*/      }
    } );
    
    do_action( 'some_class_init', $this );
  }
  
  public function enable() {
    $this->enable = true;
  }
  
  public function disable() {
    $this->enable = false;
  }
 
}

Wenn Hooks auf solche Weise hinzugefügt werden, können Drittanbieter solche Dinge machen:


add_action( 'some_class_init', function( SomeClass $some_class ) {
  $some_class->disable();
} );

Diese Strategie ist wie die Vorherige explizit, lesbar und sehr nützlich in Sachen Rückwärtskompatibilität. Es bringt große Vorteile, wenn es mehrere Methoden gibt, um Features zu aktivieren/deaktivieren und so Drittanbietern zu ermöglichen genau das Feature herauszunehmen, das sie brauchen.

Eine Variation dieser Strategie nimmt einen externen Initialisierer in Anspruch, der die Klasse erstellt und dann die verwendete Instanz enthüllt:

class SomeClass {
  
  public function on_init() {
    /* */  }
  
  public function on_template_redirect() {
    /* */  }

}

function initialize_some_class() {
  
  $class = new SomeClass();
  
  add_action( 'init', [ $class, 'on_init' ] );
  
  do_action( 'some_class_init', $class );
}

In dieser Variante sind Aktiviere / Deaktiviere – Methoden nicht nötig, weil Drittanbieter Hooks direkt entfernen könnten, indem sie remove_action / remove_filter aufrufen:


add_action( 'some_class_init', function( SomeClass $some_class ) {
  remove_action( 'on_init', [ $some_class, 'on_init' ] );
} );

Dieser Zugang zielt auf ein saubereres Design der Klasse ab, die sich nicht mit Flags und verknüpften Bedingungen beschäftigen muss. Stattdessen macht er all jene Methoden, die in Hooks verwendet werden, öffentlich (was erwünscht oder unerwünscht sein kann) und ist in Sachen Rückwärtskompatibilität weniger flexibel: Jedes mal, wenn sich die initialize_some_class Funktion ändert, müssen Drittanbieter, die irgendeinen some_class_init Hook verwenden, sich ebenfalls ändern.

Strategie #3: Gemeinsame Instanz / Registierungsmuster

Die Schwierigkeit, Hook Callbacks zu entfernen, welche Objekte nutzen, liegt darin, die verwendete Instanz zugänglich zu machen. Muster wie gemeinsame Instanzen und Registrierungsmuster erlauben den Zugang zu spezifischen Objektinstanzen, und so wird Drittanbietern ermöglicht Hooks zu entfernen.

Ein sehr triviales Beispiel:

function some_class_instance() {
  static $instance;
  $instance or $instance = new SomeClass();
  
  return $instance;
}

add_action( 'init', [ some_class_instance(), 'on_init' ] );
add_action( 'shutdown', [ some_class_instance(), 'on_shutdown' ] );

und Drittanbieter können Folgendes tun:


remove_action( 'init', [ some_class_instance(), 'on_init' ] );
remove_action( 'shutdown', [ some_class_instance(), 'on_shutdown' ] );

Dieser Zugang kann noch mächtiger und rückwärtskompatibel sein, wenn die gemeinsame Instanz eine gewidmete API zum Aktivieren / Deaktivieren von Funktionalitäten hat – wie in Strategie #2 gesehen.

Bitte merke dir, dass:

  • gemeinsame Instanzen keine Einlinge sind
  • selbst beim Speichern eines Objektes in eine globale Variable dies in irgendeiner Weise eine gemeinsame Instanzmusterimplementierung ist. Es ist die schlechteste Implementierung, die man sich vorstellen kann.

Wenn alles verloren scheint …

Die oben gezeigten Strategien funktionieren gut für neue Plugins, an denen du arbeiten wirst, aber häufig müssen sich Entwickler mit existierenden Plugins beschäftigen, die sie nicht geschrieben haben.

Wenn du in solchen Fällen Hooks entfernen musst, besteht der letzte Ausweg in:

  1. Zugang zur globalen $wp_filter Variable, wo WordPress sowohl alle Actions als auch alle Filter speichert
  2. Überprüfe die verwendete Klasse und die Methode bei jedem hinzugefügten Callback mit einem Objekt, um das zu findet, was zum Ziel passt
  3. Entferne es, wenn du es gefunden hast

Auch wenn das funktionieren kann, hat es zwei große Probleme:

  • Es funktioniert nicht wirklich mit Closures: Alle Closures sind eine Instanz der gleichen Klasse und haben keine Methoden, also wenn mehr Closures zum gleichen Hook bei der gleichen Priorität hinzugefügt werden, gibt es keinen Weg, zwischen den Closures zu unterscheiden
  • Es benötigt einiges an Code, der immer und immer wieder für jeden Hook, den derjenige anvisiert (es skaliert nicht!), gemacht werden muss

Wie du WordPress Hooks entfernst – mit “Object Hooks Remover”

Inpsyde hat kürzlich ein Package mit dem Namen “Object Hooks Remover” released, welches eine Lösung für das Entfernen von Hooks mit Objektmethoden oder Closures anbietet.

Das Package bietet 5 Funktionen an:

  • Inpsyde\remove_object_hook
  • Inpsyde\remove_closure_hook
  • Inpsyde\remove_class_hook
  • Inpsyde\remove_instance_hook
  • Inpsyde\remove_invokable_hook

Jedes hat unterschiedliche Anwendungsfälle.

Inpsyde\remove_object_hook

Nutze diese Funktion, um Hook Callbacks zu entfernen, die dynamische Objektmethoden verwenden.

Die zu entfernende Objektmethode wird über die Klasse und den Methodenname identifiziert. Ein wie folgt hinzugefügter Hook:


add_action( 'init', [ new SomeClass(), 'on_init' ] );

kann wie folgt entfernt werden:


Inpsyde\remove_object_hook( 'init', SomeClass::class 'on_init' );

Wenn so aufgerufen, entfernt die Funktion den Callback ganz gleich von der Priorität (nicht wie remove_action / remove_filter), aber es ist möglich, die Priorität auf ein Ziel zu beschränken, indem du sie als dritten Parameter übergibst.

Lies mehr über diese Funktion.

Inpsyde\remove_closure_hook

Hooks zu entfernen, die Closures nutzen, ist schon immer extrem schwierig gewesen, weil, wie eben gesagt, ist es programmatisch schwer zwischen Closures zu unterscheiden.

Das neue Inpsyde Package versucht, dieses Problem zu lösen und mithilfe von zwei Charakteristiken Closures voneinander zu unterscheiden:

  • Das Objekt, das an das Closure gebunden ist (Das gebundene Objekt ist das Objekt, das als $this  innerhalb des Closures aufgelöst wird. Es wird automatisch an das Objekt gesetzt, von dem aus das Closure erklärt wird, kann aber auch explizit gesetzt werden, siehe Closure::bind und Closure::bindTo)
  • Die Closure Signatur (das heißt Parameternamen und -typen)

Zum Beispiel kann ein Hook wie folgt hinzugefügt werden: 

class SomeClass {
  
  public function __construct() {
    add_action( 'init', function( $foo, $bar ) {
      /* */    } );
  }
}

und kann wie folgt entfernt werden:


Inpsyde\remove_closure_hook(
  'init',
  SomeClass::class [ '$foo', '$bar' ]
);

Das gebundene anvisierte Objekt wurde im obigen Beispiel über seinen Klassennamen identifiziert. Aber Zugang zur genauen Instanz des Objekts zu haben, von welchem die Funktion ausging, gibt die Möglichkeit die Objektinstanz als zweiten Parameter weiterzugeben. Darüber wird die Übereinstimmung der Closures auf diejenigen limitiert, die von dieser spezifischen Instanz hinzugefügt worden sind.

Diese Funktion entfernt, wie remove_closure_hook (und nicht wie remove_action / remove_filter), übereinstimmende Closure(s), die zu irgendeiner Priorität hinzugefügt worden sind. Es ist jedoch möglich, die Priorität auf das Ziel zu begrenzen, indem sie als dritter Parameter übergeben wird.

Lies mehr über diese Funktion.

Inpsyde\remove_class_hook

Ähnlich zu remove_object_hook zielt diese Funktion nur auf statische Methoden ab. Selbst wenn statische Klassenmethoden über remove_action / remove_filter entfernt werden könnten, kann diese Funktion immer noch sinnvoll sein, weil sie Callbacks von jeder Priorität entfernen kann und das auch ohne dass ein Methodenname spezifiziert worden ist.

Lies mehr über diese Funktion.

Inpsyde\remove_instance_hook

Entfernt Hook Callbacks, die mit einer bestimmten Objektinstanz hinzugefügt wurde, welche bereitgestellt werden muss.

Wenn du Zugang zur genauen Instanz hast, die von einigen Hooks verwendet wird, wäre es möglich, diese Hooks über remove_action / remove_filter zu entfernen. Aber diese Funktion kann immer noch nützlich sein, weil sie in einem einzigen Aufruf alle Hooks, die die Instanz verwenden, entfernen kann – unabhängig der verwendeten Methode oder Priorität.

Lies mehr über diese Funktion.

Inpsyde\remove_invokable_hook

Nicht mehr als ein Shortcut zur Verwendung von Inpsyde\remove_object_hook, wobei __invoke als Methodenname übergeben wird (zweiter Parameter).

Lies mehr über diese Funktion.

Mehr über das Package

“Object Hooks Remover” ist ein Composerpackage, was bedeutet, dass jedes Plugin, MU-Plugin oder Theme, welches das Package über inpsyde/object-hooks-remover aufrufen kann, dieses Package nutzen (und wiederverwenden) kann.

Es benötigt mindestens PHP 7+ und ist unter der MIT Lizenz lizensiert.