Wohin mit den Daten – Ab in den DataContainer

Beim Schreiben von Plugins und Themes stößt man früher oder später auf ein Problem das WordPress recht pragmatisch gelöst hat: Der globale Zugriff auf Daten.

Auf die Variable x die in Funktion y definiert wurde, kann nicht ohne weiteres in Funktion z zugegriffen werden. Das Problem ist der sogenannte Scope, also der Gültigkeitsbereich in PHP. WordPress umgeht das Problem einfach dadurch, indem es recht rücksichtslos fast alle seine Variablen mit den Schlüsselwort global in den globalen Namensraum verfrachtet und somit überall verfügbar macht. Das ist zwar eine sehr einfache Lösung, jedoch auch eine sehr egoistische. Nehmen wir einfach mal an wir hätten ein Plugin oder Theme das die Variable foo verwendet.

<?php
function set_foo( $var ) {
  global $foo;
  $foo = $var;
}

function print_something() {
  global $foo;
  print( "Foo is {$foo}<br>" );
}

function do_something() {
  global $foo;
  $foo = (int) $foo;
  $foo++;
}

Prefixen

Nun kommt die nächste Version von WordPress raus und BÄMM, WordPress verwendet selber die Variable foo und macht sie mittels global auch noch überall verfügbar. Eine Katastrophe, denn nun müssten wir unser Plugin/Theme komplett umschreiben.
Bisher vermied man dieses Problem indem man alle seine Variablen und Funktionen mit einem Prefix versah, also anstatt einfach nur $foo mit einem Prefix wie z.B. $fb_foo. Diese Lösung hat jedoch auch seine Nachteile. Frank Bültge hatte seit jeher den Prefix fb_ verwendet, dieser wird jedoch mittlerweile häufig im Zusammenhang mit FaceBook verwendet. Einen kurzen und recht eindeutigen Prefix zu finden, dürfte so langsam schwer werden. Könnte man hingehen und einen längeren Prefix verwenden, zum Beispiel frabue_ der so lange funktioniert, bis jemand anderes ihn ebenfalls verwendet. Eine Rüstungsspirale die dazu führt das der Prefix irgendwann länger ist als der Variablen- oder Funktionsname.

Namespaces

Seit PHP5.3 haben wir die Möglichkeit Namespaces zu nutzen. Namspaces lösen eine ganze Reihe solcher und vergleichbarer Probleme, weshalb ich inzwischen nur noch für PHP5.3+ schreibe. Man sollte aber auch nicht verschweigen, dass man sich angewöhnen muss seinen Code anders zu schreiben. Am Anfang bedeutet dies etwas Frust, der schwindet jedoch recht schnell weil die Vorteile überwiegen. Leider hat nicht jeder die Möglichkeit PHP5.2 einfach zu ignorieren. Arbeitet man im Kundenauftrag und weigert der Kunde sich beharrlich auf PHP5.3+ zu wechseln, kann man nicht auf Namespaces zurück greifen.

Containern

Aber wir können zumindest für unsere Variablen unseren eigenen kleinen Namespace basteln. Und den können wir dann auch noch wesentlich komfortabler ausstatten als den globalen Namensraum. Dazu nutzen wir eine Klasse die in der Grundversion aus lediglich zwei magischen Methoden besteht, __set() und __get(). Der Klasse geben wir noch eine statische Eigenschaft mit in der wir dann alle unsere Variablen speichern können. Der große “Zauber” an dieser Klasse besteht im Grunde genommen darin, dass statische Eigenschaften immer ihren Wert behalten, egal wie viele Instanzen wir von der Klasse erstellen.

<?php 
class DataContainer
{
  public static $data = array();

  public function __set( $name, $value ) {
    self::$data[$name] = $value;
  }

  public function __get( $name ) {
    if ( isset( self::$data[$name] ) )
      return self::$data[$name];
    else
      return null;
  }
}

Die Verwendung der Klasse ist recht simpel. Wollen wir eine Variable setzen, erzeugen wir eine Instanz der Klasse und setzen die Variable. Wollen wir an einer anderen Stelle die gleiche Variable wieder zurück lesen, erzeugen wir eine Instanz und lesen ganz einfach die Variable.

<?php
function set_foo( $var ) {
  $dc = new DataContainer;
  $dc->foo = $var;
}

function print_something() {
  $dc = new DataContainer;
  $foo = $dc->foo;
  print( "Foo inside DataContainer: {$dc->foo}
 Foo as local variable: {$foo}
" );
}

function do_something() {
  $dc = new DataContainer;
  $foo = &$dc::$data['foo'];
  $foo = (int) $foo;
  $foo++;
}

set_foo( '1' );
do_something();

$dc = new DataContainer();
echo "Foo is now: {$dc->foo}";

Wie man an der Funktion do_something() sieht, ist es auch möglich die Variablen innerhalb vom DataContainer mittels Referenz zu bearbeiten, so dass man bearbeitete Werte nicht zwangsweise wieder zurück in den DataContainer schreiben muss.

Vererbung

Nun ist es unter Umständen etwas mühselig in jeden Funktionsaufruf eine neue Instanz des DataContainers zu erzeugen. Arbeitet man objektorientiert, ist es etwas einfacher, da die Klassen den DataContainer erweitern können.

<?php
class Bar extends DataContainer
{
  public function hello_foo() {
    printf( 'Foo used inside %s: %s
', __CLASS__, $this->foo );
  }
}

$bar = new Bar();
$bar->hello_foo();

Alle Variablen die im DataContainer abgelegt, gelöscht oder geändert werden, sind in der Klasse Bar genauso verfügbar. Vererbung ist nicht gerade die Paradedisziplin von PHP weswegen man wohl eher auf Dependency Injection zurück greifen würde. Ein weiterer, meinem persönlichen Geschmack nach, besonders wertvoller Vorteil von DI ist, man kann seinen DataContainer Mocken und die testbarkeit seiner Klassen deutlich erhöhen. DI, Unittest und Mockings sind aber ein ganz anderes Thema.

RODC – Read Only DataContainer

Es kann natürlich sein das unser Theme oder Plugin durch Filter und Hooks erweitert werden kann, dann könnte es durchaus sein das wir einmal gesetzte Variablen davor schützen wollen das sie von unbekannten Code überschrieben werden. Nutzt man den globalen Namensraum, so ist das nahezu unmöglich. In WordPress gibt es z.B. keine Möglichkeit die globale Variable $post, die eine zentrale Rolle spielt, vor dem Überschreiben zu schützen. Jeder kann sie jederzeit löschen oder ändern, was dazu führt das andere Codeabschnitte, die diese Variable benötigen, unter Umständen versagen. Mit einem DataContainer ist das deutlich einfacher zu handhaben, wir bauen einfach eine Methode ein die bestimmte Variablen als Read Only kennzeichnet.

<?php
class DataContainer
{
  public static $data = array();

  public static $protected = array();

  public function __set( $name, $value ) {
    if ( ! in_array( $name, self::$protected ) )
      self::$data[$name] = $value;
  }

  public function __get( $name ) {
    if ( isset( self::$data[$name] ) )
      return self::$data[$name];
    else
      return null;
  }

  public static function protect( $name ) {
    if ( ! in_array( $name, self::$protected ) )
      array_push( self::$protected, $name );
  }
}

function good_foo() {
  $dc = new DataContainer();
  $dc->foo = 'protected foo';
  $dc->bar = 'unprotected bar';
  $dc->protect( 'foo' );
}

function bad_bar() {
  $dc = new DataContainer();
  $dc->foo = 'hahaha!';
  $dc->bar = 'I change everything!';
}

$dc = new DataContainer();

echo "Set <code>foo</code> to <b>protected foo</b> and <code>bar</code> to <b>unprotected bar</b><br>";
good_foo();

echo "Try to overwrite <code>foo</code> and <code>bar</code><br>";
bad_bar();

echo "<code>foo</code> is still <b>{$dc->foo}</b> and <code>bar</code> is now <b>{$dc->bar}</b><br>";

Das dürfte nun nicht jeden Tag vorkommen das man seine Variablen schützen muss, zeigt aber schön welche Vorteile ein DataContainer gegenüber den globalen Namensraum hat. Man ihn nämlich nahezu nach belieben an seine Bedürfnisse anpassen.

Shutdown

Zum Schluss noch ein kleines Beispiel wie ich bei einem Plugin, an dem ich gerade schreibe, mit Hilfe eines DataContainers ein paar Probleme gelöst habe.
Das Plugin erforderte die Zwischenspeicherung einiger Daten da diese zur Laufzeit erstellt werden und über verschiedene Seitenaufrufe verfügbar bleiben müssen. Gegeben sei also z.B. ein Array $a das zur Laufzeit befüllt wird und bei einem Ajax-Request verfügbar sein muss. Mein erster Lösungsansatz war die Verwendung von $_SESSION, denn genau dazu sind Sessions da. Um Daten über verschiedene Seitenaufrufe breit zu stellen. Um sicher zu gehen das am Ende des Skripts auch alle Daten in der Session gespeichert wurden, habe ich shutdown Hook von WordPress benutzt. Dieser Hook wird bei Beendigung des Skriptes ausgeführt, zu einem Zeitpunkt also, wenn alle Daten vorhanden sein sollten. In der Callback-Funktion habe ich dann einfach alle Daten in die Session geschrieben und die Session geschlossen.
Nun taten sich verschiedene Probleme auf. Zum einen ist das recht unflexibel. Fügt man noch weitere Daten hinzu, muss man sich darum kümmern das auch diese in der Session gespeichert werden. Genauso wenn Daten entfallen, muss man sich darum kümmern das diese, nicht mehr vorhandenen Daten, nicht gespeichert werden. Der erste Lösungsansatz war dann anstatt einzelner Arrays einen DataContainer zu verwenden und dann den kompletten DataContainer in der Session zu speichern. Das funktioniert erstaunlich gut, jedoch musste ich mich immer noch selber darum kümmern das die Daten in der Session gespeichert werden.
Zudem ist der shutdown Hook nicht wirklich sicher. Wird das Skript z.B. mit einem exit() die() beendet bevor der Hook registriert wird, werden die Daten nicht in der Session gespeichert. Dieses Problem lässt sich dadurch lösen, indem man das Speichern und lesen der Daten in bzw. aus der Session in den DataContainer verlagert.

<?php
class DataContainer
{
	const SESSION_KEY = 'Example_DataConatiner';

	public static $data = array();

	public function __construct() {
		( ! session_id() ) AND session_start();

		if ( isset( $_SESSION[self::SESSION_KEY] ) && ! empty( $_SESSION[self::SESSION_KEY] ) )
			self::$data = $_SESSION[self::SESSION_KEY];

		register_shutdown_function( array( $this, '__destruct' ) );
	}

	public function __destruct() {
		( ! session_id() ) AND session_start();

		$_SESSION[self::SESSION_KEY] = self::$data;
		session_write_close();
	}

	public function __set( $name, $value ) {
		self::$data[$name] = $value;
	}

	public function __get( $name ) {
		if ( isset( self::$data[$name] ) )
			return self::$data[$name];
		else
			return null;
	}

	public static function reset() {
		self::$data = array();
		unset($_SESSION[self::SESSION_KEY]);
		session_write_close();
	}
}

$dc  = new DataContainer();
$uri = $_SERVER['PHP_SELF'];

if ( null === $dc->session_data ) {
	$dc->session_data = 'Value from previous page request';
	echo 'This is the first run<br>';
	die( "<a href='{$uri}?run=2'>Klick</a>" );
}

$pagerequest = filter_input( INPUT_GET, 'run', FILTER_SANITIZE_NUMBER_INT );
echo "This is the {$pagerequest}nd run with <b>{$dc->session_data}</b> from session-data";
$dc::reset();

Das funktioniert schon recht wunderbar. Durch die __destruct() Methode wird der Inhalt des DataContainers in der Session gespeichert sobald er zerstört wird. Da PHP am Ende eines Skriptes alle bestehenden Objekte zerstört (schließt), wird die __destruct Methode automatisch am Ende eines Skriptes aufgerufen. Im Beispiel oben wird die __destruct() Methode mittels register_shutdown_function() noch zusätzlich registriert, so dass sie beim Beenden des Skriptes auch definitiv ausgeführt wird. Verwendet man anstatt der magischen __destruct() Methode eine eigene Methode und registriert sie mit register_shutdown_function(), so kann man den DataContainer verwerfen ohne das seine Werte gespeichert werden, die Werte werden jedoch automatisch am Ende des Skriptes gespeichert.

Allerdings wurden Sessions zu einen Zeitpunkt erfunden als Browser keine Tabs kannten und anscheinend geht jeder Browser etwas anders damit um. Ich hatte zumindest massive Probleme mit dem FireFox wenn ich mehr als einen Tab zum gleichen Server offen hatte (z.B. die Startseite in den einen Tab, einen Artikel in der Einzelansicht in einen anderen). Chrome und Opera können das besser, aber ich kann meinen Besuchern ja schlecht vorschreiben welchen Browser sie zu benutzen haben oder wie viele Tabs sie öffnen dürfen.
WordPress bietet uns einen guten Ersatz für Sessions sofern man damit Probleme hat. Wir verwenden einfach Transients zum Speichern der Daten. Man muss hier abwägen ob die zusätzlichen DB-Zugriffe und vor allem die Menge an Daten die man speichern möchte dies rechtfertigt. Transients haben den Vorteil das man recht genau einstellen kann wie lange sie gültig sind. Das geht zwar mit Sessions auch, jedoch erlaubt nicht jeder Hoster das Verändern des Wertes für die Session-Lifetime. Transients sind sogar beständiger als Sessions, so kann man einen Transient an einen Login koppeln und somit eine Art von Server-Cookie umsetzen.

Fazit

Ich hoffe es ist deutlich geworden das der globale Namensraum eigentlich mehr Nach- als Vorteile hat und das man mit nur wenig Aufwand eine bessere Lösung bekommen kann die man nach seinen eigenen Bedürfnissen anpassen kann. Vor allem das automatische Speichern, ob in Sessions oder Transients, erleichtert die tägliche Arbeit enorm da man sich weniger Gedanken darum machen muss ob man nun alles gespeichert hat oder nicht. Der DataContainer lässt sich enorm ausbauen, denkbar wäre z.B. auch eine Methode die Werte aus einen Aufruf der Settings-API validiert und speichert. Am Ende des Skriptes werden die Daten automatisch in die Options geschrieben, stehen aber während der Laufzeit weiterhin zur Verfügung ohne das man sie sich extra aus den Options holen müsste.
Vor allem das man den DataContainer, und somit auch Zugriffe auf Transients und die Options-Tabelle, sehr einfach mocken kann, erleichtert das Unittesting ungemein wodurch man wiederum viel Zeit und Mühe beim Programmieren spart.
Alle in diesen Artikel verwendeten Skripte sind als Gist erhältlich für den Fall das sich jemand dazu ermuntert fühlt ein wenig mit DataContainern zu experimentieren.