Quicktipp: Checkboxen auswerten

Vor kurzem hatte ich das Problem das ich mehrere Checkboxen bzw. deren Kombination auswerten musste. Normalerweise eine Arbeit für Leute die Mutter & Vadder erschlagen haben. Dazu gleich mal die Bedingungen:
Wir haben drei Checkboxen (a, b und c) in einem Formular und müssen auswerten welche Checkbox ausgewählt wurde bzw. nicht ausgewählt wurde. Wenn das Formular abgeschickt wird, bekommen wir ein POST- bzw. GET-Array mit den Werten (ich gehe im weiteren mal von einem POST-Array aus).
Das Böse an Checkboxen ist, wenn eine Checkbox nicht ausgewählt wurde, wird auch kein Wert im POST- bzw. GET-Array gesetzt.
Nach dem Absenden des Formulars bekommen wir also in etwa so etwas:

$mPost = array(
		'action'	=> 'send',
		'user'		=> 'Horst',
		'a'		=> 'On',
		'c'		=> 'On',
		'foo'		=> 'bar',
		'baz'		=> '',
	);

Dies ist nun eine Simulation des POST-Arrays und man kann sehen das für ‘b’ kein Wert gesetzt wurde sofern die Checkbox ‘b’ nicht ausgewählt wurde.
Wie wertet man nun üblicherweise die drei Checkboxen aus? Wahrscheinlich mit vielen If-ElseIf-Else Konstrukten und isset():

if( isset( $_POST['a'] ) )
...
elseif( isset( $_POST['a'] ) && isset( $_POST['b'] ) )
...
elseif( isset( $_POST['a' ) && ! isset( $_POST['b' ) )
...

if( isset( $_POST['b'] ) && isset( $_POST['c'] ) )
...

So wühlt man sich Zeile für Zeile durch alle möglichen Kombinationen und versucht keine zu vergessen. Das. Ist. Blöd! Und nicht nur blöd, sondern auch verdammt unübersichtlich. Bei drei Checkboxen mag es noch machbar sein, aber wenn man fünf, acht oder mehr Checkboxen hat, dann wird es fast schon unmöglich alle Kombinationen von ausgewählten und nicht ausgewählten Checkboxen zu berücksichtigen.

Da eine Checkbox nur ein Ja-Nein-Wert (Wahr oder Falsch) darstellt, habe ich mich recht schnell an die Zeit erinnert als ich mit der Computerei angefangen habe. Damals hatte ich einen C16 mit 16kB RAM. Dort war jedes Bit kostbarer als Gold und durfte nicht verschwendet werden. Deswegen hatte man oft mit Bitmasken gearbeitet. In einer Bitmaske steht jedes Bit für einen Ja oder einen Nein-Wert. Das war und ist recht effizient, denn in einem Byte kann ich so 8 Werte speichern. Und natürlich auch wieder abfragen.

Nun stehen wir vor dem Problem unsere drei Checkboxen in eine Bitmaske zu bekommen. Dazu benötigen wir erst einmal eine entsprechende Maske. Mit printf() bzw. sprintf() können wir eine solche Maske leicht erstellen:

$bin = sprintf( '%b%b%b', $mPost['a'], $mPost['b'], $mPost['c'] );

Das PHP-Manual sagt zum Platzhalter %b folgendes:

das Argument wird als Integer angesehen und als Binär-Wert ausgegeben

%b wird also immer zu 1 oder 0 umgewandelt. Wandeln wir nun unsere Variablen (a, b und c) in Boolsche Werte um, so würde PHP %b durch 1 bzw. 0 ersetzen, je nachdem ob die Variable TRUE oder FALSE ist. Denn zuerst würde der boolsche Wert in einen Integer (1 bzw. 0) umgewandelt und dann in einen einstelligen Binärwert. Dies erreichen wir dadurch, dass wir mit isset() abfragen ob die Variable überhaupt existiert. Somit umschiffen wir gleichzeitig das Problem das nicht ausgewählte Checkboxen keinen Wert übertragen.

$bin = printf( '%b%b%b', isset( $mPost['a'] ), isset( $mPost['b'] ), isset( $mPost['c'] ) );

Das klappt soweit schon wunderbar und bei drei Checkboxen ist es auch noch recht übersichtlich.
Als Ergebnis erhalten wir einen String der z.B. “101″ enthält. Je nachdem welche Variable gesetzt ist und welche nicht. Diesen String können wir nun ganz einfach mit einem switch-case abfragen und darauf reagieren:

switch( $bin ){
	case '000':
		//nichts ausgewaehlt
	break;

	case '100':
		// nur checkbox 'a' wurde ausgewählt
	break;

	case '101':
		// checkbox 'a' wurde ausgewaehlt, checkbox 'b' jedoch NICHT, checkbox 'c' ist uns egal
	break;

	default:
		// alle anderen Faelle
	break;
}

Bei drei Checkboxen ist das schon eine ganz brauchbare Lösung. Aber was ist wenn wir jetzt, sagen wir mal, 20 Checkboxen haben. Zum Beispiel eine Umfrage oder ähnliches?
Hier stehen wir vor zwei Problemen. Zum einen wäre der sprintf() nicht mehr übersichtlich und recht unbrauchbar. Zum anderen müssen wir ja gewähren das jede Variable an ihren Platz ist und nicht z.B. ‘a’ und ‘g’ vertauscht sind.

Als erstes legen wir uns mal ein Muster fest welche Variablen uns in welcher Reihenfolge interessieren. Das machen wir einfach mit einem Array:

$defaults = array(
		'a'	=> '',
		'b'	=> '',
		'c'	=> '',
	);

Damit wir nicht mit uninitialisierten Variablen arbeiten müssen, mischen wir das POST-Array mit unserer Vorgabe. Dadurch gehen wir sicher das alle benötigten Variablen auch mit einem (leeren) Wert initialisiert sind.

$data = array_merge( $defaults, $mPost );

Nun müssen wir nur noch über unser Vorgabe-Array ($defaults) laufen und abfragen ob die entsprechenden Schlüssel im Daten-Array ($data) gesetzt sind.

$bin = '';
foreach( $defaults as $key => $val )
	$bin .= sprintf( '%b', !!$data[$key] );

Dies kann man auch anders lösen, z.B. mit einem Trinären Operator. Je nach Geschmack des Programmierers halt:

$bin .= ! empty( $data[$key] ) ? '1' : '0';

Wenn der entsprechende Wert im Daten-Array nicht leer ist, wird eine 1 ausgegeben, ansonsten eine 0.
Als Ergebnis erhalten wir wieder einen String mit vielen Einsen und Nullen den wir im Switch-Case abfragen können. Wandelt man den String mit bindec() in eine Dezimalzahl um, kann man die Auswahl schön platzsparend speichern. Wenn man mit Cookies arbeitet, wird man nämlich schnell wieder daran erinnert das Speicherplatz Mangelware ist. Odre man nutzt den Dezimalwert um eine statistische Auswertung durchzuführen. Oder verwendet ihn im Switch-Case. Oder mit If-Abfragen. Oder, oder, oder…

Und hier das ganze dann  noch mal als komplettes Script:

$mPost = array(
'action' => 'send',
'user' => 'Horst',
'a' => 'On',
'c' => 'On',
'foo' => 'bar',
'baz' => '',
);

$defaults = array(
'a' => '',
'b' => '',
'c' => '',
);

$data = array_merge( $defaults, $mPost );

$bin = '';

foreach( $defaults as $key => $val ){
$bin .= sprintf( '%b', !!$data[$key] );
//$bin .= ! empty( $data[$key] ) ? '1' : '0';
}

$dec = bindec( $bin );

var_dump( $bin );
var_dump( $dec );

switch( $bin ){
case '000':
//nichts ausgewaehlt
break;
case '100':
// nur checkbox 'a' wurde ausgewählt
break;
case '101':
// checkbox 'a' wurde ausgewaehlt, checkbox 'b' jedoch NICHT
break;
default:
// alle anderen Faelle
break;
}
view raw file1.php This Gist is brought to you using Simple Gist Embed.

Zum Schluss noch etwas “dreckiges” PHP. Hier möchte ich mal schnell den NOTNOT-Operator einwerfen auf den ich bei der Suche nach der Lösung gestoßen bin. !$data[$key] wird Wahr (TRUE) wenn $data[$key] leer ist. Ich möchte aber wissen ob eine Variable nicht leer ist, denn printf() soll ja aus ‘%b’ eine ’1′ machen wenn die Variable gesetzt ist. Also muss ich die Bedingung !$data[$key] noch einmal negieren. Aus !$data[$key] wird also !!$data[$key].
Dies ist also im Grunde genommen nur eine fürchterliche Kurzschreibweise für ! empty( $data[$key] ). Für das schnelle Programmieren sind solche Kurzschreibweisen ganz nützlich. Sie sparen eine Menge Tipperei wenn man viel Code schreiben muss. Für die Lesbarkeit des Codes sind sie jedoch sehr kontraproduktiv. Man muss schon genau überlegen was da gerade passiert und sicher gehen das die Variable auch initialisiert ist, ansonsten hagelt es Notices.


Quick-Tipp: Schnell-Login

Hin und wieder gibt es das Bedürfnis sich ohne Angabe von Benutzername und Passwort einzuloggen. Das könnte z.B. der Fall sein wenn man automatisierte Tests durchführen möchte die einen Login benötigen. Oder aber man muss zum Testen immer wieder den Benutzer wechseln um verschiedene Szenarien durchzuspielen.
Aber auch wenn man sich in einer eher “unsicheren Umgebung” befindet (z.B. schlecht abgesichertes öffentliches Netzwerk) möchte man vielleicht nicht so gerne seine Login-Daten eintippen. Es wäre also ganz praktisch wenn man sich (automatisiert) einloggen kann ohne ständig Login-Daten einzutippen.

WordPress lässt sich relativ einfach dazu bringen Login-Daten automatisiert anzunehmen. In erster Linie ist die Funktion wp_signon() dafür zuständig den Login durchzuführen. Dazu übergibt man ihr Benutzername und Passwort, die Funktion gibt daraufhin true zurück bzw. ein WordPress-Fehler-Objekt. Dies kann man bequem mit is_wp_error() abfragen und so mit nur wenigen Zeilen einen Login durchführen.

<?php
require( dirname(__FILE__) . '/wp-load.php' );
is_wp_error(
wp_signon(
array(
'user_login'=>'YourLoginName',
'user_password'=>'YoUrAw3s0M3P455W0rD'
)
)
) ? die('Mooo... :(') : wp_safe_redirect( admin_url() );

Speichert man den Code in einer separaten Datei ab, so muss zuerst wp-load.php eingebunden werden um die WordPress-Funktionen verfügbar zu machen. Hier bitte auf den Pfad achten, iom Gist liegt die Datei im gleichen Verzeichnis wie wp-load.php.  Danach wird direkt wp_signon() mit einem Array aus Benutzername und Passwort gefüttert, welches wiederum direkt als Parameter an is_wp_error() übergeben wird.
Durch Aufruf der Datei ist man direkt eingeloggt und wird ins Backend umgeleitet. Möchte man lieber ins Frontend umgeleitet werden, so ersetzt man einfach admin_url() durch site_url().

Ich habe mir für meine Entwicklungsarbeit ein kleines Plugin geschrieben mit dem ich recht schnell zwischen verschiedenen Benutzern hin- und her wechseln kann. Dazu listet mir das Plugin auf der Login-Seite die Test-User auf, welche ich zuvor angelegt habe. Durch einen Klick auf einen entsprechenden User-Namen kann ich mich dann ohne Eingabe von Benutzername und Passwort anmelden. Auf Optik habe ich verzichtet, da es ein Werkzeug bei der Entwicklung ist. Wer mag, kann dem ganzen ja noch ein bisschen optischen Feinschliff verpassen.
Das Plugin WP-Quicklogin ist auf Github zu finden.


WordPress Version testen

Momentan habe ich es mir zur Aufgabe gemacht ein veraltetes Plugin auf eine neue Codebasis zu setzen (Refactoring). Um welches Plugin es sich dabei handelt, werde ich verraten wenn es in einem vorzeigbaren Zustand ist. Derzeit braucht es noch recht viel Arbeit.
Dabei fallen aber immer wieder ein paar Sachen ab die ich für erwähnenswert halte. Mein letzter Artikel war übrigens auch das Ergebnis dieser Arbeit. Diesmal ist es eine Klasse mit der man die minimale WordPress- und PHP-Version testen kann (MySQL könnte man auch testen, ich denke aber das wird kaum jemand machen da es eher selten der Fall ist das man eine bestimmte MySQL-Version voraussetzt). Minimale Version bedeutet hierbei, man testet ob WordPress bzw. PHP mindestens Version X hat.
Bei dem Tempo das Automattic bei der Entwickelung von WordPress an den Tag legt (alleine für dieses Jahr sind 3 Versionen geplant), wird es immer wichtiger zumindest zu  prüfen unter welcher WP-Version ein Plugin oder Theme aktiviert wird. Kann man natürlich auch sein lassen, wenn man den Anwender lieber mit Fehlermeldungen bzw. nicht funktionierenden Funktionen beglücken möchte. Ich persönlich halte es aber für empfehlenswert zu prüfen und ggf. auf eine zu niedrige Version hinzuweisen.

Die Klasse lässt sich vielfältig konfigurieren und flexibel einsetzen. Mit der statischen Methode is_WP() lässt sich z.B. prüfen ob WordPress bereits gestartet wurde. Ist dies nicht der Fall, werden 403-Header (forbidden) gesendet und das Script beendet. Dies ist z.B. nützlich um ein Script gegen direktes Aufrufen zu schützen.
Bsp.:

// beendet das Script falls WordPress nicht zuvor gestartet wurde
WP_Environment_Check::is_WP();

Kern der Klasse sind allerdings die drei Methoden check_wp(), check_php() und run_all_tests() (die Methode check_mysql() ist, wie erwähnt, nur der Vollständigkeit halber dabei und dürfte eher selten Anwendung finden). Konfiguriert man ein Array oder ein Objekt mit den entsprechenden Werten und übergibt das Array bzw. Object der Methode run_all_tests(), so werden die entsprechenden Versionen geprüft und ggf. das Script beendet. Natürlich kann man auch nur einzelne Komponenten testen. Im Gist ist noch eine Datei mit einigen Beispielen, ich denke dadurch wird klar wie man die Klasse verwenden kann.
Im Normalfall ruft man die Klasse beim Aktivieren des Plugins auf, so dass das Plugin erst gar nicht aktiviert wird.

Nun möchte man vielleicht nicht gleich mit dem Hammer zuschlagen und das Script beenden weil es sich um ein Theme und nicht um ein Plugin handelt. Dafür ist die Methode set_die_on_fail() nützlich. Übergibt man ihr den Wert TRUE, so wird das Script bei einen fehlerhaften Test nicht beendet, sondern FALSE als Rückgabewert zurück gegeben. Die Rückgabewerte kann man dann entsprechend auswerten und Meldungen ausgeben.

Falls man mit den Standardmeldungen nicht glücklich ist, kann man diese ebenfalls recht einfach anpassen. Einfach der Klasse einen neuen String übergeben. Das macht z.B. Übersetzungen der Fehlermeldungen recht einfach. Auch hierzu gibt es ein Beispiel im Gist.

Vielleicht findet der eine oder andere die Klasse ganz nützlich oder erweitert sie sogar. In beiden Fällen würde ich mich über Feedback natürlich freuen. Und hier noch der Gist zur Klasse und den Beispielen:

<?php
/**
*
* Class to check the environment (WordPress-, PHP and MySQL-version)
* Test only on minimum or equal version
*
* @author Ralf Albert
* @version 1.0
*
* @var array|object $versions (optional) Array with key=>val or object $version->wp|php|mysql; what to test => minimum version
*
*/
class WP_Environment_Check
{
/**
*
* WP version
* @access public
* @var string minimum or equal version of WordPress
*/
public $wp = '3.2';

/**
*
* PHP version
* @access public
* @var string minimum or equal version of PHP
*/
public $php = '5.2';

/**
*
* MySQL version
* @access public
* @var string minimum or equal version of MySQL
*/
public $mysql = '5.0';

/**
*
* Exit message if WordPress test failed
* @access public
* @var string
*/
public $exit_msg_wp = '';

/**
*
* Exit message if PHP test failed
* @access public
* @var string
*/
public $exit_msg_php = '';

/**
*
* Exit message if MySQL test failed
* @access public
* @var string
*/
public $exit_msg_mysql = '';

/**
*
* If set to true, the class will die with a message if a WP|PHP|MySQL test fail.
* Does not affect is_WP() or if forbidden_headers() is called withot a message
* @access public static
* @var bool true (default)|false
*/
public static $die_on_fail = TRUE;

/**
*
* Constructor
* Run all test that are defined in $version
* @access public
* @param array|object $versions
*/
public function __construct( $versions = NULL ){
self::is_WP();

if( ! empty( $versions ) || ( is_array( $versions ) || is_object( $versions ) ) )
$this->run_all_tests( $versions );
}

/**
*
* Set $die_on_fail
* @param bool $status True exits the script with a message
*/
public function set_die_on_fail( $status = TRUE ){
if( ! is_bool( $status ) )
$status = (bool) $status;

self::$die_on_fail = $status;
}

/**
*
* Check if WordPress is active (if $wp is an object of class wp() )
* @access public static
* @return bool true|die with message and send forbidden-headers if WP is not active
*/
public static function is_WP(){
global $wp;

if( ! $wp instanceof WP )
self::forbidden_header();
else
return TRUE;
}

/**
*
* Run all tests
* @access public
* @param array|object $versions
* @return bool true if all tests passed successfully
*/
public function run_all_tests( $versions = NULL ){
if( empty( $versions ) || ( ! is_array( $versions ) && ! is_object( $versions ) ) )
return FALSE;

$tests = array( 'wp', 'php', 'mysql' );

foreach( $versions as $test => $version ){
// check if the wanted test is available (means: is the test x a method 'check_x')
if( in_array( strtolower( $test ), $tests ) ){
$method = strtolower( $test );
$func = 'check_' . $test; // create the method (check_wp|check_php|check_mysql)
$this->$method = $version; // set $this->wp|php|mysql to version x

if( ! call_user_func( array( &$this, $func ) ) )
die( 'Test ' . __CLASS__ . '::' . $func . ' failed!' ); // this should never happen...
}
}

return TRUE;
}

/**
*
* Check WordPress version
* @access public
* @return bool true returns true if the test passed successfully. Die with a message if not.
*/
public function check_wp(){
if( empty( $this->wp ) )
return FALSE;

if( empty( $this->exit_msg_wp ) )
$this->exit_msg_wp = 'This plugin requires WordPress ' . $this->wp . ' or newer. <a href="http://codex.wordpress.org/Upgrading_WordPress">Please update WordPress</a> or delete the plugin.';

global $wp_version;
if( ! version_compare( $wp_version, $this->wp, '>=' ) ){
return self::forbidden_header( $this->exit_msg_wp );
}

return TRUE;
}

/**
*
* Check PHP version
* @access public
* @return bool true|die with message
*/
public function check_php(){
if( empty( $this->php ) )
return FALSE;

if( empty( $this->exit_msg_php ) )
$this->exit_msg_php = 'This plugin requires at least PHP version <strong>' . $this->php . '</strong>';

if( ! version_compare( PHP_VERSION, $this->php, '>=' ) ){
return self::forbidden_header( $this->exit_msg_php );
}

return TRUE;
}

/**
*
* Check MYSQL version
* @access public
* @return bool true|die with message
*/
public function check_mysql(){
if( empty( $this->mysql ) )
return FALSE;

if( empty( $this->exit_msg_mysql ) )
$this->exit_msg_mysql = 'This plugin requires at least MySQL version <strong>' . $this->mysql . '</strong>';

global $wpdb;
if( ! version_compare( $wpdb->db_version(), $this->mysql, '>=' ) ){
return self::forbidden_header( $this->exit_msg_mysql );
}

return TRUE;
}

/**
*
* Send forbidden-headers (403) if no message is set. Only dies if a message is set
* @access public static
* @param string (optional) $exit_msg
*/
public static function forbidden_header( $exit_msg = '' ){

if( empty( $exit_msg ) ){
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
die( "I'm sorry Dave, I'm afraid I can't do that." );
} else {
if( FALSE === self::$die_on_fail )
return FALSE;
else
die( $exit_msg );
}
}
}

<?php
/*
* Mocking
*/
class WP{}
$wp = new WP;

class WPDB
{
public function db_version(){
return '5.1';
}
}
$wpdb = new WPDB();

$wp_version = '3.3.1';
/* end mocking */

/*
* Creating an instance ov WP_Environment_Check
* Setup the minimum versions
* Setup an exit-message if the WordPress test fail
* Performs every single test one by one
*/

$a = new WP_Environment_Check();
$a->wp = '3.3.1';
$a->php = '5.2';
$a->mysql = '5.1';
$a->exit_msg_wp = 'The plugin <em><a href="http://example.com/my_plugin/">Acme Plugin</a></em> requires WordPress ' . $a->wp . ' or newer. <a href="http://codex.wordpress.org/Upgrading_WordPress">Please update WordPress</a> or delete the plugin.';
$a->check_wp();
$a->check_php();
$a->check_mysql();

/*
* Creating an object with the minimum versions
* Create an instance of WP_Environment_Check
* Performs all tests at once
*/

$v = new stdClass();
$v->wp = '3.3.1';
$v->php = '5.2';
$v->mysql = '5.0';
$a = new WP_Environment_Check();

$a->run_all_tests( $v );


/*
* Setup an array with the minimum versions
* Performs all test by creating an instance of WP_Environment_Check
*/

$v = array( 'wp' => '3.0', 'php' => '5.2', 'MySQL' => '5.1' );
$a = new WP_Environment_Check( $v );

/*
* Store testresult in an object. Disable dying if tests failed
*/

$a = new WP_Environment_Check();
$a->set_die_on_fail( FALSE );
$a->wp = '3.3.1';
$a->php = '5.2';
$a->mysql = '7.0';

$r = new stdClass();

$r->wp = $a->check_wp();
$r->php = $a->check_php();
$r->mysql = $a->check_mysql(); // fail on MYSQL -> 'mysql' => false

var_dump( $r );


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.


Performance ist nicht gleich Performance

Ich bin kein großer Fan von Performance-Tests, dass sage ich gleich vorweg. Der Grund ist schlichtweg der, dass man vergleichbare Umgebungen voraussetzen muss um die Ergebnisse übertragbar zu machen. Wenn ich Plugin auf System A teste, rennt es noch wie doof. Teste ich es aber auf System B, hinkt und hakt es wie ein lahmer Gaul.
Der Grund dafür liegt in der Optimierung der Systeme, sofern sie optimiert sind. Als Beispiel möchte ich nur die Anbindung an den MySQL-Server nennen. Hier gibt es etliche verschiedene Konfigurationen für die unterschiedlichsten Anwendungsfälle. Werden viele kleine Datensätze abgefragt, optimiert man den MySQL-Server anders als wenn man ihn für wenige große Datensätze benötigt. Somit würde das gleiche Plugin auf beiden Systemen unterschiedlich performant sein, alleine deshalb weil die Anbindung an die Datenbank anders ist.
Nicht viel anders sieht es beim Server selber aus. Auch hier gibt es unzählige Optimierungsvarianten. Hinzu kommt noch unterschiedliche Server-Software, Betriebssysteme auf denen der Server läuft bis hin zu der Hardware die einen sehr wesentlichen Teil der Performance ausmacht. Wie soll man da einen gemeinsamen Nenner finden den man als Grundlage des Vergleichs heranziehen kann?

Viele Tests messen einfach das, was man einfach messen kann. Das wäre dann z.B. der Speicherverbrauch. Dann wird auch gerne die Ausführungszeit gemessen, was aber aufgrund der unterschiedlichen Systeme schon kaum noch Aussagekraft hat. Reicht das alles nicht, kommen gerne noch so obskure Faktoren wie Codezeilen oder Anzahl der verwendeten Hooks und Filter hinzu.
Selten bis gar nicht wird die Code-Qualität bewertet. Dies ist für mich aber ein sehr wichtiges Kriterium. Denn ein schlampig programmiertes Plugin läuft oft auch schlampig, wirft Fehlermeldungen und ist nicht zukunftssicher. Häufig sieht man Plugins die über die Jahre gewachsen sind bis der Autor selber den Überblick verliert. Entweder wird das Plugin dann von Grund auf neu programmiert und weiter gepflegt. Oder aber, was häufiger vorkommt, man lässt das Plugin so wie es ist und kümmert sich nicht mehr darum.
Nun ist Code-Qualität nicht ganz so einfach zu “messen”. Man muss den Code bewerten und hierzu eine Reihe von Kriterien heranziehen. das kostet Zeit und Mühe, weshalb es wohl niemand macht. Ein Kriterium wären zum Beispiel die Übersichtlichkeit. Das kann eigentlich jeder bewerten der sich den Code anschaut. Oder aber auch ob der Code ein Wust aus wilden Zeilen darstellt die kaum entzifferbar sind. Ist der Code gut kommentiert damit auch jemand der nicht so viel Ahnung von Programmierung hat versteht was da vor sich geht. Hält der Code sich an gewisse Programmierrichtlinien. Nur um mal einige wenige Kriterien zu nenne die man verwenden kann.
All das kann man mit Noten bewerten, sogar relativ objektiv. Nun werden einige sagen “Hauptsache es rennt!“. Das ist natürlich auch ein Argument. Also zieht man noch die Anzahl an Fehlern, Notices usw. heran die das Plugin wirft wenn man es einsetzt. Auch grausam geschriebener Code kann fehlerfrei laufen. So kommt man schon zu einen recht guten und nachvollziehbaren Ergebnis.
Kennt man sich bei der Programmierung ein wenig besser aus, dann kann man auch noch einzelne Code-Abschnitte genauer unter die Lupe nehmen. Ist der Code gemäß den empfohlenen Vorgaben geschrieben? Wenn nicht, macht es Sinn von den Vorgaben abzuweichen? Gibt es “merkwürdige” Programmiertechniken im Code oder ist alles auf den ersten Blick verständlich?

Für mich sind dies deutlich wichtigere Faktoren als Millisekunden Laufzeit und Megabyte Speicherverbrauch. Denn dies sind Faktoren die ich erklären kann, so dass auch jemand der weniger Ahnung hat versteht wie ich zu meinen Testergebnis gekommen bin. Und vor allem wird schnell deutlich wo noch Potenzial für Verbesserungen besteht. Code der leicht verständlich ist, wird auch gerne von anderen gepflegt, so dass er sich recht schnell verbreitet und mit der Zeit (hoffentlich) besser wird.

Ein ziemlich merkwürdiger Test macht hingegen gerade auf Google+ die Runde. Es geht um den Plugin-Test von MillaN. Sergej Müller hat sich wohl zurecht gefragt wie MillaN auf seine Ergebnisse kommt. Getestet wurden 35 Plugins aus den unterschiedlichsten Kategorien und mit einem Wert von 1 (schlecht) bis 5 (sehr gut) bewertet. Nur wie dieser Wert zustande kommt, bleibt vielen vollkommen unergründlich.
Vielleicht ist weniger Speicherverbrauch besser als mehr? Also FPW Post Instructions bekommt eine 4 bei einem konstanten Speicherverbrauch von 0,1MB. GD Press Tools Pro verbraucht zwar 29 mal so viel Speicher (2,9MB), bekommt aber die Bestnote 5. Es scheint also etwas mit dem Speicherverbrauch im Frontend bzw. Backend zu tun zu haben. Denn GD Press Tools Pro verbraucht im Frontend “nur” 2MB Speicher, während FPW Post Instructions sowohl vorne wie hinten den gleichen Speicherverbrauch hat. So wirklich schlüssig ist das Konzept jedoch nicht. Denn Event Manager bekommt die schlechteste Note 1 bei einem Speicherverbrauch von 5 bzw 3,7MB (Front-/Backend). GD Custom Posts And Taxonomies Tools Lite bekommt für die gleiche Leistung (1,5/1,3MB) eine noch ganz gute 4 verpasst. Event Manager ist also schlechter obwohl es mehr Speicher einspart?
Ich könnte jetzt noch weiter rätseln ob es vielleicht an der Ausführungszeit liegt oder an der Anzahl der verwendeten Hooks. Vielleicht auch am Mondstand beim Test des Plugins. Man weiß es nicht und der Autor trägt auch nicht zur Aufklärung bei.

Grade 2 plugins have only some elements of optimization, but they are close to grade 1. Adminimize plugin shouldn’t even load on the front end except for some elements, and that needs to be optimized.

Das soll also der Grund sein warum einige Plugins schlechter sind als andere? Adminimize soll z.B. im Frontend nur das laden was es benötigt, was es im Prinzip auch macht, dies sollte aber optimiert werden? Hö? Es scheint also darum zu gehen was ein Plugin im Front- bzw. Backend lädt. Da ist es wohl egal welche Aufgabe es hat, wenn es nicht den geheimen Richtlinien des Autors entspricht, ist es schlecht.

Na dann schauen wir uns doch mal eins dieser supertollen Grade-5 Plugins an. Ich habe mich für GD Unit Converter entschieden und es mal unter die Lupe genommen. Was mir beim ersten Blick auffiel, waren folgende Codezeilen:

$this->script = $_SERVER["PHP_SELF"];
$this->script = end(explode("/", $this->script));

Und? Sinn erkannt? Also ich habe erst mal längere Zeit gerätselt was da passiert bis ich den Code kopiert und getestet habe. OK, ich schreibe es mal ein wenig um:

$this->script = basename(__FILE__);

So einfach kann PHP sein. Der nächste Horror folgt sofort:

    private function plugin_path_url() {
        $this->plugin_url = plugins_url("/gd-unit-converter/");
        $this->plugin_path = dirname(dirname(__FILE__))."/";

        define("GDUNITCONVERTER_URL", $this->plugin_url);
        define("GDUNITCONVERTER_PATH", $this->plugin_path);
    }

Da werden also zwei globale Variablen definiert die zwanzig Zeilen später bei einem wp_enqueue_script() wieder verwendet werden. Klar, man hätte auch einfach auf $this->plugin_url zurück greifen können (OOP, u know!?). Aber warum sparsam mit dem Speicher umgehen wenn man doch so schön sinnlos welchen durch Verwendung globaler Variablen verschwenden kann?
Ach ja. Weil man anderen gerne vorschreibt sie sollen doch bitte schön sparsam mit dem Speicher umgehen:

I am sure that many developers will say that these results are not important, but considering that most of the WordPress users are on shared hosting with limited memory and resources available to them, this is most important thing to have plugins they need and still have server running fine.

Also Wasser predigen und Wein saufen. Contactform 7 bekommt übrigens nur eine schwache 2, dafür aber eine extra Erwähnung:

All tested plugins, but one, used the internal WordPress AJAX handling. Contact Form 7 uses own handler and that is not something I can recommend. Using WP handler is best solution considering that it is already written with security concerns in mind and it is very easy to use, making plugin fit better with WP development concepts.

Muss ich extra erwähnen das GD Unit Converter seinen eigenen Caching-Mechanismus mitbringt und mal ganz elegant auf den in WordPress bereits eingebauten Caching-Mechanismus pfeift?
Natürlich darf auch der eigene Logger nicht fehlen, der zwar nicht genutzt wird aber dennoch geladen werden muss (require_once). Über solch lustige Konstrukte wie folgenden wundert man sich dann schon gar nicht mehr.

$js_url = defined("SCRIPT_DEBUG") && SCRIPT_DEBUG ? "js/src/unit-converter.js" : "js/unit-converter.js";

Wenn SCRIPT_DEBUG definiert ist, sollen die JavaScripte aus dem nicht vorhandenen Verzeichnis js/src/ geladen werden. Das gibt lustige Fehlermeldungen wenn man sein Blog im Debug-Modus startet. So lustig, das man das Plugin sofort deaktiviert.

Es ist einfach schlau daher zu reden und dann selber solch einen Mist zu veröffentlichen. Gefühlt die Hälfte des Codes die das Plugin umfasst ist sinnlos, wird nicht benötigt, verstößt gegen Coding Guidelines oder ist schlichtweg Speicherverschwendung. Hier sollte der Herr MilanN vielleicht selber noch mal Hand anlegen und das Plugin um fluffige 90% Code reduzieren bevor er ihm die Bestnote 5 verpasst. Ich habe mich vorhin mal eine Stunde lang hingesetzt und versucht das Plugin von Scratch (also ohne Vorlage) nachzubauen. Nach einer Stunde hatte ich bereits brauchbare Ergebnisse und kam mit einem Zehntel an Code aus. Der Speicherverbrauch dürfte noch sparsamer sein, da ich nicht total unnützes Zeug lade, sondern mich darauf beschränke das zu nutzen, was ich benötige.

Solche Tests die nicht klar machen wie eine Bewertung zu Stande kommt und bei denen der Autor sich die Testbedingungen so zurecht biegt das er sich selbst Bestnoten verpassen kann, kann man getrost außer Acht lassen. Hier muss sich jeder selber fragen wie objektiv der Test sein kann wenn die eigenen Plugins den Maßstab vorgeben. Da sollte man sich als Autor nicht wundern wenn andere einen auf den Zahn fühlen und zeigen wo der Mops die Locken hat.

Fazit

Gute Tests sind objektiv und vergleichen nicht eigene Arbeit mit der von Fremden. Wird die eigene Arbeit dennoch im Test mit einbezogen, dürfen die Testkriterien nicht auf die eigene Optimierung hin zugeschnitten sein.
Die Testkriterien sollten klar und verständlich sein. Was man als Leser nicht nachvollziehen kann, ist nichts wert. Ist es fraglich wie eine Bewertung zu Stande gekommen ist, ist der ganze Test wertlos. Moinka Thon-Soun hat auf Texto in einen Artikel beschrieben wie ein schlecht gemachter Test auf einen Laien wirkt. Nämlich verwirrend, sonst nichts.
Die Tests sollten nachprüfbar sein. Dazu müssen Testscripte zugänglich sein und die Testumgebung erläutert werden.
Test sollten nicht auf einen einzelnen Aspekt fokussiert sein. Jeder Test hat einen Schwerpunkt, dies rechtfertigt jedoch nicht eine schlechte Bewertung weil ein Testkandidat eben nicht genau den Schwerpunkt trifft. Ein guter Tests umfasst mehrere Aspekte und bildet aus den Einzelergebnissen eine Bewertung. Jedes Plugin hat seine Stärken und Schwächen. Ein Plugin das optisch gut gestylt daher kommt, verbraucht eben mehr Speicher (mehr Grafiken, mehr CSS, mehr Code) als ein Plugin welches sehr spartanisch gestaltet ist. Was nützt einem aber ein Plugin das am falschen Ende spart, wenn es durch seinen Purismus nur schwer bedienbar ist?
Tests sollten sich auf das beschränken, was unter anderen Systemen ähnliche (vergleichbare) Ergebnisse liefert. Eine systemunabhängiger Wert sind z.B. Fehlermeldungen (auch Notices und Deprecated Meldungen) . Die werden auf jeden System gleich sein, denn falsch ist hier wie da falsch.
Test sollten realitätsnah sein. Im Labor gelten andere Regeln als in der Wildnis. Selbst wenn ein Plugin im Test wunderbar abschneidet, kann es dadurch versagen das es andere Plugins behindert oder gar komplett ausschaltet. Man kann nicht alle Kombinationen von Plugins prüfen, jedoch gibt es eine Reihe von Regeln für ein friedliches Miteinander die man beachten sollte.

Also bitte nicht jeden Test blauäugig Glauben schenken. Lieber einmal mehr als einmal zu wenig an den Ergebnissen zweifeln. Wir wissen doch alle: Die Statistik die man selber gefälscht hat, ist immer noch die beste ;)