define( RACE_CONDITION, TRUE ) – Das Rennen um die Konstanten

Leider noch viel zu oft sieht man in Plugins den teilweise massenhaften Gebrauch von define(). Selbst wenn mit Klassen gearbeitet wird, wird reichlich Gebrauch von define() gemacht. Daran lässt sich gut erkennen ob der Programmierer die Klasse lediglich zur Kapselung seines Codes oder tatsächlich wegen OOP erstellt hat.
Aber was ist nun so doof an define() das man es möglichst sparsam, am besten gar nicht, benutzen sollte? Die Überschrift sagt es ja schon: Race Conditions.

Zunächst einmal ein Stück PHP-Code wie er lange Zeit für WordPress-Plugins benutzt wurde. Unter anderem, weil der Codex es so vorgeschlagen hatte (mittlerweile wird von der Verwendung dieses Codes dringendst abgeraten):

	// Pre-2.6 compatibility
	if ( ! defined( 'WP_CONTENT_URL' ) )
		define( 'WP_CONTENT_URL', get_option( 'siteurl' ) . '/wp-content' );
	if ( ! defined( 'WP_CONTENT_DIR' ) )
		define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' );
	if ( ! defined( 'WP_PLUGIN_URL' ) )
		define( 'WP_PLUGIN_URL', WP_CONTENT_URL. '/plugins' );
	if ( ! defined( 'WP_PLUGIN_DIR' ) )
		define( 'WP_PLUGIN_DIR', WP_CONTENT_DIR . '/plugins' );

Der Code ist relativ logisch. Sollten bestimmte Konstanten nicht definiert sein, so werden sie definiert. So weit, so unschlimm. Aber der Teufel steckt, wie so oft, im Detail.
Bedenkt man das alle Plugins nacheinander geladen werden, wird schnell klar dass das erste Plugin welches diese Konstanten definiert gewinnt. Typische Race Condition: Wer zuerst kommt, mahlt zuerst.
Was passierte nun wenn das erste Plugin das geladen wurde WP_CONTENT_URL nicht dem Codex entsprechend mit get_option( 'siteurl' ) . '/wp-content' ) definierte, sondern z.B. mit define( 'WP_CONTENT_URL', '/my_bad_content/wp-content/' ); ? Ganz klar, alle anderen Plugins die auf WP_CONTENT_URL zurückgriffen versuchten aus dem falschen Verzeichnis zu lesen. Da define() nicht “überschreibbare globale Variablen”, auch bekannt als Konstanten, definiert, konnte auch kein anderes Plugin den Pfad zum wp-content-Verzeichnis wieder grade biegen.
Im einfachen Fall führt das zu nicht mehr funktionierenden Plugins. Im schlimmeren Fall deutet WP_CONTENT_URL auf einen entfernten Server und könnte Schadcode nachladen. Zugegeben, ein sehr konstruierter Fall da WordPress in der Regel diese Konstanten selber definiert hatte. Aber es ist nun einmal ein mögliches Szenario was man im Hinterkopf haben sollte.

Nun haben wir die 2.6er Zeit in WordPress ja hoffentlich lange hinter uns gelassen und niemand verbreitet mehr Plugins die solche Code-Zeilen enthalten. Oder? Ich schau mal böse zu den Kollegen rüber und schweige mich aus wo ich den Code her habe. Ich müsste auch einige böse anschauen, dazu fehlt mir die Zeit.

Aber was für WordPress-Konstanten gilt, gilt leider auch für unsere eigenen Konstanten. Schnell mal mit define() eine Konstante definieren weil die Verwendung von Konstanten ja so einfach ist. “Ich definiere in Datei A eine Variable die ich in Datei B brauche? Nimm eine Konstante, die sind überall verfügbar.” Dies ist ein Gedankengang, von dem wir uns schleunigst trennen sollten.
Das fängt damit an, dass Konstanten “überall” verfügbar sind. Es interessiert kein anderes Script wo ich z.B. meine JavaScript-Dateien ablege, deswegen braucht es auch kein define( 'SCRIPT_DIR', '/my_ugly_dir/ );. Mal davon abgesehen das ich damit Informationen “nach außen” trage die andere nicht interessieren, verseuche ich damit auch den globalen Namensraum. Alle anderen Scripte können nun die Konstante SCRIPT_DIR nicht mehr verwenden da sie bereits belegt ist.
Um diesen Umstand zu umgehen, werden Konstanten gerne mit Prefixen versehen. define( 'MY_UGLY_SCRIPT_DIR', '/my_ugly_script_dir' ); ist auch nicht wesentlich besser. Je mehr Plugins auf den Markt kommen, desto einfallsreicher muss ich meine Prefixe aussuchen. Das endet in immer längeren Prefixe und macht den Code am Ende nur unübersichtlicher. Zudem löst es das Problem von Namenskollisionen nicht komplett.

Denn was ist denn wenn ich mehr als ein Plugin schreibe? Hand auf’s Herz, wir sind doch alles faule Säue. Also kopieren wir einfach Code den wir bereits geschrieben haben um ihn wiederzuverwenden. So gesehen nicht schlimm, ist ja sogar quasi ein empfohlenes Vorgehen. Nur blöd wenn ich in zwei Plugins die gleiche Konstante verwende, sie jedoch in beiden Plugins unterschiedliche Werte enthält (z.B. __FILE__.
Race Condition. Das zuerst geladene Plugin gewinnt. Ich mache mir das Leben also unnötig schwer. Denn Code den ich einmal geschrieben habe, kann ich nicht gefahrlos wiederverwenden ohne ihn erneut anzufassen.

Ewig nur meckern kann aber auch nicht die Lösung sein. Eine Alternative zu define() kommt aus dem OOP-Bereich und ist ein einfacher Datencontainer.

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

public static function __set( $name, $value ){
if( ! isset( self::$data[$name] ) )
self::$data[$name] = $value;
else
return FALSE;

return TRUE;
}

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

$dc = new DataContainer;
$dc->FILE = basename( __FILE__ );

var_dump( $dc->FILE );

$dc_two = new DataContainer;
$dc_two->FILE = 'mettigel';

var_dump( $dc->FILE );
view raw file1.php This Gist is brought to you using Simple Gist Embed.

Das Prinzip sollte schnell klar sein. Es wird eine Klasse erzeugt die nichts anderes macht als Variablen über die magischen Methoden __set() und __get() entgegen zu nehmen und zu speichern. Da das Array $data als statisch deklariert wurde, wird es bei neuen Instanz der Klasse nicht verändert.
Der Trick an der ganzen Sache besteht darin, in der magischen Methode __set() zu überprüfen ob die Variable $name bereits gesetzt ist. Ist sie es nicht, wird sie gesetzt. Ist sie es, wird FALSE zurück gegeben. So sind einmal gesetzte Variablen vor dem Überschreiben geschützt.
Damit kann man die Klasse DataContainer genauso verwenden wie ein define(). Darüber hinaus kann man sich auch noch eine Methode reset( $name ) schreiben, mit der man im Notfall eine einmal gesetzte Pseudo-Konstante wieder löschen kann. Selbst ein redefine(), das es in PHP nicht gibt, wäre damit möglich. Mit so einem Datencontainer sind wir also sogar flexibler als mit define().

Jetzt stellt sich natürlich die Frage wenn ich den Datencontainer in Datei a.php habe, aber in Datei b.php darauf zugreifen will, wie macht man das?
Da gibt es elegante und weniger elegante Wege. Ein etwas weniger eleganter Weg wäre es den Datencontainer mittels define()verfügbar zu machen.

$dc = new DataContainer;
$dc->FILE = basename( __FILE__ );
define( 'DATACONTAINER', serialize( $dc ) );

// somewhere over the rainbow ... ehmm ... in another file
$e = unserialize( DATACONTAINER );
var_dump( $e->FILE );

Bitte schlagt mich nicht für diesen Code-Schnipsel. Kann man machen, muss man aber nicht. Denn deutlich eleganter geht es mit einem Autoloader, darüber schreibe ich aber demnächst etwas (Cliffhänger ;) )

Ich hoffe es ist klar geworden das die Verwendung von Konstanten relativ viele Probleme mit sich bringt die man relativ einfach beseitigen kann. Überlassen wir die Verwendung von Konstanten besser WordPress und widmen uns lieber einfacheren und eleganteren Methoden.

1 Trackbacks

  1. Von WordPress Version testen – Yoda Condition am 11. Januar 2012 um 08:14

    [...] Yoda Condition Debuggen du musst Gehe zum Beitrag AboutKontaktImpressum « define( RACE_CONDITION, TRUE ) – Das Rennen um die Konstanten [...]