Dependency injection via service container

Dank einem Service Container fällt es mir noch leichter als zuvor schnelle und leichgewichtige WordPress Plugins zu schreiben. Viel zu oft kommt es vor, dass an der einen Ecke vom Plugin Variablen und Einstellungen definiert werden, die dann an einer ganz anderen wieder gebraucht werden:

  • Einfache Einstellungen oder Zugangsdaten überall im Code verfügbar.
  • Bereits geladene Post, Pages oder andere Collections.
  • Eigene Objekte von unseren Plugin-Klassen wiederverwenden.

Doch wie komme ich von überall im OOP-Sinne an diese Daten? Es ist schwer ordentliches OOP in WordPress zu verfolgen, was dazu führt, dass manche Objekte stets global (z.B. als Singletone) verfügbar sind oder mehr als nur ein Mal instantiiert werden. Das kostet unnötig Speicher und Laufzeit für jeden Request.

Ein Service Container hilft hier, da er die Daten global verfügbar macht und erst Speicher belegt, sobald die Daten tatsächlich benötigt werden.

Ein einfacher Service Container

Was wir hier sehen ist ein leicht abgeänderter Code von Daniel Hüsken (Autor vom BackWPup Plugin), welcher durch Pimple inspiriert wurde. Es ist ein vereinfachter Service Container der mehr wie ein Object Pool mit Lazy-Loading funktioniert:

class Service_Container {
  private $values = array();
  private $raw = array();

  function __construct( $values = array() ) {
    $this->values = $values;
  }

  function set( $id, $value ) {
    $this->values[ $id ] = $value;
  }

  function has( $id ) {
    return array_key_exists( $id, $this->values );
  }

  function get( $id ) {
    if ( ! $this->has( $id ) ) {
      throw new \NotFoundException( // as in PSR-11
        sprintf( 'Service %s does not exist.', $id )
      );
    }

    if ( isset( $this->raw[ $id ] ) ) {
      return $this->raw[ $id ];
    }

    $value = $this->values[ $id ];

    if ( is_callable( $value ) ) {
      $value = $value( $this );
    }

    return $this->raw[ $id ] = $value;
  }
}

Alles in nur einer Klasse, ohne viel Magie mit Scopes oder Factories. Simpel und genau richtig für die oftmals flachen Anforderungen im WordPress-Bereich.

Sehr nützlich in einem Plugin oder Theme

Das könnte in der Hauptdatei vom Plugin stehen und ist so von überall aus erreichbar:

$_someslug_container = new Service_Container( [
  'api_key' => 'abba1972',
  'foo'     => new \Someslug\Not_Lazy_Loaded_Class(),
  'api'     => function ( $container ) {
    return new \Someslug\Api( $container->get( 'api_key' ) );
  }
] );

Es kann alles enthalten sein. Einfache Zugangsdaten oder einfache Objekte, die wir immer benötigen (z.B. die Plugin-Konfiguration). Objekte, die nicht immer gebraucht werden, sollen ebenfalls vorhanden sein, aber erst Speicher und Laufzeit kosten (instantiiert werden), wenn sie tatsächlich benötigt werden. Mit einer einfachen Funktion um den Service herum wird ein Lazy-Loading erreicht.

Den Service überall benutzen

Zunächst kann eine Funktion in der Hauptdatei dabei helfen, denn Service Container von überall aus zu erreichen:

function _someslug_container() {
  global $_someslug_container;

  return $_someslug_container;
}

Das ist nicht zwingend notwendig, macht es aber sehr leicht von überall darauf zuzugreifen. Die Zugangsdaten, API-Verbindung und vieles mehr erreichen wir nun immer, egal ob in einer Funktion, Methode oder Template:

$api = _someslug_container()->get( 'api' );

Jetzt merkt der Container zum ersten Mal, dass die API-Verbindung benötigt wird und lädt das Objekt wie in der „Lazy-Load-Funktion“ angegeben. Zuvor hat sie kein Speicher, CPU-Zeit oder API-Requests gekostet. Das hält das Plugin schlank und schnell.

WordPress-Möglichkeiten

Es gibt einige Klassen, mit denen eine Art von Service Container aufgebaut werden kann:

  • \WP_Hook (insb. die ::apply_filter Methode).
  • \WP_Session_Tokens ebenfalls sehr simpel gestrickt.
  • \SplObjectStorage aus PHP (since 5.3).
  • \WP_Object_Cache welcher nahezu ein Object Pool ist.

Doch diese Klassen brauchen einige Anpassungen und bringen in Summe dann mehr Logik mit als nötig. Daher ist es gut ein eigenen Service Container zu erstellen.

Trotzdem bietet es sich an zumindest die Konfiguration vom Container an den WP_Object_Cache zu hängen. Zum mindern von Overhead und vorhalten von oft genutzen Objekten.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *