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.


Empfehlung des Tages: Filter-Funktionen

Seit PHP5.2 sind die Filter-Funktionen fester Bestandteil von PHP geworden. Jedoch greifen immer noch sehr wenige Programmierer auf diese Filter-Funktionen zurück, was ein kleines bis großes Sicherheitsrisiko darstellt.

Der Normalfall sieht immer noch so aus, dass einfach abgefragt wird ob in $_GET, $_POST oder $_REQUEST ein Schlüssel vorhanden ist und dann der entsprechende Wert verwendet wird. Leider viel zu oft sieht man auch, dass erst gar nicht geprüft wird ob der Schlüssel vorhanden ist und einfach davon ausgegangen wird das dieser vorhanden ist und verwendet werden kann. Das ist nicht nur schlechter Stil, sondern eröffnet auch eine Sicherheitslücke. Denn die superglobalen Arrays $_GET, $_POST und $_REQUEST sind keine Einbahnstraße, man kann aus ihnen lesen und in sie hinein schreiben. Schauen wir uns das mal etwas näher an.

$_REQUEST lassen wir in der in diesem Artikel mal außen vor da es noch nicht von den Filter-Funktionen unterstützt wird. Zudem sollte man auf $_REQUEST auch nur in Ausnahmefällen zugreifen. $_REQUEST Enthält die Daten von $_GET und $_POST. Die Verwendung von $_REQUEST deutet also darauf hin, dass man nicht so wirklich weiß was man tut und einfach auf gut Glück auf ein superglobales Array zugreift in der Hoffnung die gesuchten Daten seien schon irgendwie da.
Es gibt einige wenige Fälle wo der Zugriff auf $_REQUEST sinnvoll sein kann, man sollte sich jedoch im klaren darüber sein das der Zugriff auf $_REQUEST eben nicht so sicher ist als wenn man $_GET und $_POST mit den Filter-Funktionen abfragt.

$_GET und $_POST sind keine Einbahnstraße

Das man auf die superglobalen Arrays $_GET und $_POST nicht nur lesend, sondern auch schreibend zugreifen kann, ist anscheinend nicht allen klar. Schauen wir uns dazu mal dieses Script an

$_POST['nonce'] = 'badnonce';
echo 'Was the nonce send with a POST request?<br>';
echo 'With <code>isset()</code>: ' . ( isset( $_POST['nonce'] ) ? 'yes' : 'no' ) . '<br>';
echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_POST, 'nonce' ) ? 'yes' : 'no' ) . '<br>';
echo 'Value of <code>nonce</code> with <code>$_POST[\'nonce\']</code>: ' . $_POST['nonce'] . '<br>';
echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_POST, \'nonce\' )</code>:' . filter_input( INPUT_POST, 'nonce' ) . '<br>';

echo '<hr>';

$_GET['nonce'] = 'badnonce';
echo 'Was the nonce send with a GET request?<br>';
echo 'With <code>isset()</code>: ' . ( isset( $_GET['nonce'] ) ? 'yes' : 'no' ) . '<br>';
echo 'With <code>filter_has_var()</code>' . ( filter_has_var( INPUT_GET, 'nonce' ) ? 'yes' : 'no' ) . '<br>';
echo 'Value of <code>nonce</code> with <code>$_GET[\'nonce\']</code>: ' . $_GET['nonce'] . '<br>';
echo 'Value of <code>nonce</code> with <code>filter_input( INPUT_GET, \'nonce\' )</code>:' . filter_input( INPUT_GET, 'nonce' ) . '<br>';

Die nonces sind in WordPress ein Sicherheitsmechanismus mit dem man prüfen kann ob eine Anfrage tatsächlich von dort kommt, von wo man sie erwartet. Verlassen wir uns darauf das der Schlüssel nonce vorhanden ist und verwenden diesen, gehen wir das Risiko ein das er an einer anderen Stelle einfach überschrieben wurde.

Das man die Werte der superglobalen Arrays einfach überschreiben kann, lässt sich recht einfach mit vielen WordPress Funktionen testen die Werte aus den superglobalen Arrays $_GET, $_POST und/oder $_REQUEST verwenden.

add_action(
	'admin_init',
	function(){
		if (
			( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] )
			||
			( isset( $_GET['action'] ) && 'trash' == $_GET['action'] )
		) {
			$_GET['action'] = $_POST['action'] = $_REQUEST['action'] = 'not-now';
		}
	}
);

Einfach mal diesen Code-Schnipsel in die functions.php einfügen und dann im Backend die Kommentarübersicht aufrufen. Dort dann wahllos ein paar Kommentare löschen. Entweder über die Checkboxen und eines der Bulk-Menüs oder über den Link der erscheint wenn man mit der Maus auf einen Kommentar zeigt.
Nutzt man die Checkboxen und das Bulk-Menü, passiert gar nichts. Beim Löschen über den Link verschwindet zwar der Kommentar und der Zähler hinter “Papierkorb” geht hoch, ein Klick auf “Alle Kommentare” genügt um die vermeintlich gelöschten Kommentare wieder anzuzeigen.

Es geht aber deutlich böser

add_action(
	'admin_init',
	function(){
		if (
			( isset( $_GET['action'] ) && 'delete-comment' == $_GET['action'] )
			||
			( isset( $_GET['action'] ) && 'trash' == $_GET['action'] )
		) {
			$a = array_fill( 0, 5, 0 );
			array_walk( $a, function( &$e ){ $e = rand( 1,999 ); } );
			$_GET['delete_comments'] = $_POST['delete_comments'] = $_REQUEST['delete_comments'] = $a;
			$_GET['c'] = $_POST['c'] = $_REQUEST['c'] = rand( 1, 999 );
		}
	}
);

Dieser Code überschreibt nicht einfach die action, sondern ersetzt die ausgewählten Kommentare durch zufällige Werte. Egal was man auswählt, welche Kommentaren gelöscht werden ist reiner Zufall. Im besten Fall passiert nichts, da die zufällig ausgewählten IDs nicht existieren. Im schlimmsten Fall werden ganz andere Kommentare gelöscht als ausgewählt. Man könnte auch über eine SQL-Abfrage alle vorhandenen Kommentar-IDS abfragen und dann zufällig IDs auswählen, dass ist dann richtig böse aber ein anderes Thema.

Mit filter_input() wäre dies nicht möglich, da filter_input() nur die Werte berücksichtigt, die auch tatsächlich über einen Request abgeschickt wurden (siehe erstes Beispiel). Nun fragt man sich vielleicht warum WordPress etwas so dummes macht wenn es doch eine solch einfache Lösung für das Problem gibt.
Nun die Antwort liegt in meinen ersten Satz dieses Artkels. Die Filter-Funktionen sind erst ab PHP5.2 verfügbar, WordPress wurde jedoch zu PHP4 Zeiten entwickelt und enthält auch noch unendlich viel PHP4-Code. Wer das ändern möchte, kann gerne mal eine Suche nach $_GET, $_POST und $_REQUEST über die WordPress Dateien laufen lassen und die entsprechenden Stellen anpassen. Die Suche nach $_GET liefert aktuell 594 Treffer in 104 Dateien. Soviel zu diesen Thema.

Ich hoffe es ist klar geworden warum es nicht ganz unerheblich ist seinen Code mindestens auf PHP5.2 Niveau zu bringen und warum man in seinen Themes und Plugins unbedingt auf die Filter-Funktionen setzen sollte anstatt direkt auf die superglobalen Arrays $_GET, $_POST und $_REQUEST zuzugreifen.

Zum Schluss noch etwas Mehrwert. Manchmal kann es vorkommen das Werte sowohl per GET als auch POST Request übergeben werden (meist im Zusammenhang mit Ajax Requests), in diesen Fall greift man gerne auf $_REQUEST zurück. Da die Filter-Funktionen von PHP $_REQUEST (noch) nicht unterstützen, müsste man jedes mal umständlich $_GET und $_POST abfragen ob der benötigte Schlüssel vorhanden ist. Das kann man sich mit einer kleinen Funktion vereinfachen und den Mangel von PHP gleich etwas ausbügeln.

function read_superglobals( $vars ) {
	
	if ( is_string( $vars ) )
		$vars = (array) $vars;
	
	$return = array();
	
	foreach ( $vars as $variable_name ) {
		if ( ! filter_has_var( INPUT_POST, $variable_name ) ) {
			if ( ! filter_has_var( INPUT_GET, $variable_name ) ) {
				$return[$variable_name] = '';
			} else {
				$return[$variable_name] = filter_input( INPUT_GET, $variable_name );
			}
		} else {
			$return[$variable_name] = filter_input( INPUT_GET, $variable_name );
		}
	}

	return $return;
}

Die Funktion erwartet als Parameter ein Array oder einen String mit den jeweiligen Schlüssel(n) den/die man abfragen möchte. Die Funktion prüft dann mit filter_input() ob in $_GET oder $_POST der Schlüssel vorhanden ist. Ist er vorhanden, kopiert sie den Wert mit den zugehörigen Schlüssel in ein Array und gibt anschließend das Array zurück. Die Funktion arbeitet in etwa so wie wp_reset_var(), jedoch mit den Unterschied das sie keine globalen Variablen erzeugt und ausschließlich die Schlüssel zurück gibt, die auch tatsächlich mittels eines GET oder POST Requests abgesendet wurden.
Natürlich kann man die Funktion auch dann verwenden, wenn man genau weiß ob die Werte mittels GET oder POST Request gesendet wurden. Dann bekommt man ein Array zurück mit exakt den Schlüssel-Werte Paaren die man benötigt.


Linktitel automatisch ausfüllen lassen

Bitte beachten!
Das Plugin steht nun auch auf WordPress.org zum Download bereit. Bitte alle Supportanfragen und Fehlermeldungen entweder im Support auf WordPress.org oder im Issue Tracker auf GitHubBitte keine Support-Anfragen in den Kommentaren! Danke für das Verständnis.


Derzeit schreibe ich anAutoinsert Linktitle ein paar Artikeln und ärgere mich immer das ich, wenn ich einen Link erstelle, den Titel für den Link manuell ausfüllen muss. Wahrscheinlich bin ich der einzige auf diesen Planeten mit diesen Problem, deswegen habe ich mir schnell mal ein kleines Plugin geschrieben.

Gibt man im URL-Feld eine gültige URL ein und verlässt dieses Feld, dann wird ein Ajax-Request abgesetzt der versucht aus der verlinkten Webseite den title-Tag zu extrahieren. Gelingt dies, wird der Webseitentitel in das Feld für den Linktitel eingefügt. Wenn nicht, passiert nix. Den Titel kann man bei Bedarf dann noch anpassen oder wieder löschen.

Vielleicht nimmt sich ja mal jemand etwas Zeit und gibt ein wenig Feedback, wäre schön und hilfreich.

Nachtrag

Ich hatte nicht damit gerechnet das dass Plugin ein so hohes Interesse wecken würde. Github, dort wo der Plugin-Code derzeit liegt, ist nicht jedem bekannt und auch nicht jeder weiß wie man aus dem Repository ein Zip bekommt. Deswegen hier eine kleine Anleitung dazu.

#111 Als erstes also mal das Github-Repository öffnen. Sollte euer Bildschirm anders aussehen als im Screenshot links, dann klickt auf Code (zwischen der Pulskurve und Network (die rote Markierung habe allerdings ich eingefügt)). Rechts neben Clone in Windows ist ein Button mit einer kleinen Wolke und dem Wort ZIP (siehe Screenshot). Klickt ihr da drauf, dann sollte eine Aufforderung erscheinen eine Zip-Datei zu speichern.
Diese Zip-Datei speichert ihr auf eurem Computer und kann dann bereits aus dem Backend von WordPress heraus installiert werden.
Dazu im Backend auf die Plugins-Seite gehen, dort oben neben Plugins auf Installieren klicken, im nächsten Bildschirm auf Hochladen (oder in der englischen Version Upload), die zuvor gespeicherte Zip-Datei auswählen und fertig. Nun noch das Plugin aktivieren und das war es.


Der kleine aber feine Unterschied zwischen is_email() und sanitize_email()

Wer mit E-Mails in WordPress arbeitet, sollte den Unterschied zwischen is_email() und sanitize_email() kennen. Er ist zwar klein, aber fein.

Der offensichtlichste Unterschied ist erst einmal der Rückgabewert. is_email() prüft ob eine gegebene E-Mail Adresse überhaupt den Kriterien entspricht und gibt einen String zurück wenn dem so ist. Andernfalls gibt is_email() false zurück. sanitize_email() hingegen gibt entweder einen String mit einer gültigen E-Mail Adresse oder einen leeren String zurück.
is_email() gibt also einen boolschen Wert oder einen String, sanitize_email() immer einen String zurück.

Nun könnte man mit einer einfachen Typumwandlung is_email() dazu bringen ebenfalls immer einen String zurück zu geben. (string) is_email( $email ) würde sich nahezu gleich verhalten wie sanitize_email(). Im Erfolgsfall eine gültige E-Mail Adresse, im Fehlerfall einen leeren String. Stimmt das? Überprüfen wir es:

$emails = array( 'foo@bar.com', 'baz-at-example-org', 'öttö@wördpäss.com', 'meh@cöm.com' );
echo '<ol>';
array_walk(
	$emails,
	function ($email) {
		$empty = 'an empty string';
		$is = (string) is_email( $email );
		$se = sanitize_email( $email );

		printf(
			'<li>%s - %s (%s)</li>',
			( '' != $is ) ? $is : $empty,
			( '' != $se ) ? $se : $empty,
             $email
		);
	}
);
echo '</ol>';

Die Ausgabe sieht in etwa so aus:

  1. foo@bar.com – foo@bar.com (foo@bar.com)
  2. an empty string – an empty string (baz-at-example-org)
  3. an empty string – tt@wrdpss.com (öttö@wördpäss.com)
  4. an empty string – meh@cm.com (meh@cöm.com)

Wie man sehen kann, gibt (string) is_email( $email ) wie erwartet bei ungültigen E-Mail Adressen einen leeren String zurück. Das WordPress glaubt das Umlaute in E-Mail Adressen nicht erlaubt seien, lass wir an dieser Stelle mal dahingestellt. WordPress kann halt (noch) nicht mit Umlautdomains umgehen.
Viel interessanter ist die Ausgabe von sanitize_email(). Auch sanitize_email() kann nicht mit Umlauten umgehen, streicht sie aber einfach aus der E-Mail Adresse anstatt sie z.B. zu kodieren.

Das führt nun natürlich zu Problemen wenn man es nicht weiß. Sollte mal eine größere Anzahl an E-Mails nicht versendet werden, kann das mitunter daran liegen das man vergessen hat vor dem Speichern zu prüfen ob WordPress damit umgehen kann.

// FALSCH
$user = array(
  'name' => 'Hans'
  'email' => sanitize_email( $email )
);
update_option( 'awesome_options', $user );

// Richtig
$user = array(
  'name' => 'Hans'
  'email' => sanitize_email( (is_email( $email ) )
);
update_option( 'awesome_options', $user );

Merke!

Jede E-Mail Adresse sollte bevor sie mit sanitize_email() verarbeitet wird erst mit is_email() validiert werden. Ansonsten bekommt man E-Mail Adressen die vielleicht lustig aussehen, es aber definitiv nicht sind.


Gimme-Gimme-Gimme

ABBA suchte dereinst nach einen Mann für die Nachtschicht. PHP kann damit wenig anfangen, genauso wenig wie mit den falschen Typen, wobei mit “Typen” jetzt nicht der Mann gemeint ist.

Oft schreibt man Funktionen oder Methoden und macht sich nur wenig Gedanken über den Rückgabewert im Fehlerfall, also dann, wenn die Funktion nicht die Aktion durchführen konnte wofür sie geschrieben wurde. Ich selber habe es bisher auch so gehandhabt das ich im Fehlerfall einen boolschen Wert oder vergleichbares zurück gegeben habe. Das Ergebnis ist, ich muss bei jeden Funktionsaufruf prüfen ob auch der Variablen-Typ zurück kommt den ich erwarte. Erwarte ich z.B. ein Array, kommt im Fehlkerfall aber ein boolsches False zurück, muss ich das abfangen.

Warum? Weil es ansonsten unter Umständen Fehlermeldungen hagelt und das Script abbricht. Im Backend kann man es noch verschmerzen, ich nenne das immer den “Weisste bescheid” Zustand. Im Frontend ist es aber mehr als peinlich und sogar kontraproduktiv wenn der Seitenaufbau nach dem Blogheader mit einer Fehlermeldung abbricht.

Stellen wir uns mal vor wir schreiben ein Widget für die Sidebar. Im Code für das Widget sind folgende zwei Funktionen:

/**
 * Create a ordered- or unordered list from values
 *
 * @param	string	$type	ol for ordered lists or ul for unordered list
 * @param	array	$values	Array with list items
 * @return	string			HTML list
 */
function get_list( $type = 'ol', $values = array() ) {

	$type = ( in_array( $type, array( 'ol', 'ul' ) ) ) ?
		$type : 'ol';

	foreach ( $values as &$val )
		$val .= ' item';

	$format = "<{$type}>" . str_repeat( '<li>%s</li>', sizeof( $values ) ) . "</{$type}>";

	return vsprintf( $format, $values );

}

/**
 * Returns an array or boolean depending on parameter
 *
 * @param	boolean	$fail	Returns an array if set to true, else returns boolean false.
 * @return Ambigous <boolean, multitype:string >
 */
function get_list_values( $fail = false ) {

	return ( true == $fail ) ?
		false : array( 'A', 'B', 'C' );

}

Die eine Funktion (get_list_values()) erzeugt ein Array mit Elementen, die andere Funktion (get_list()) erzeugt aus einem Array mit Werten eine HTML-Liste. Der Aufruf beider Funktionen könnte exemplarisch so aussehen:

$vals = get_list_values();
$list = get_list( 'ol', $vals );

echo $list;

Soweit funktioniert das ganze recht ordentlich. Nun provozieren wir jedoch mal einen Fehler und rufen die Funktion get_list_values() mit den Parameter 1 auf. Jetzt werden wir mit einen Warning: Invalid argument supplied for foreach() in ... begrüßt und das Script bricht an dieser Stelle ab.

Um solche Fehlermeldungen zu vermeiden, hätte ich vorher prüfen müssen ob $vals ein Array ist.

$vals = get_list_values(1);

if ( is_array( $vals ) ) {
  $list = get_list( 'ol', $vals );
  echo $list;
} else {
  // do some error handling
}

Das erscheint bisher alles recht logisch. Die frage ist nur, was soll man an dieser Stelle für ein Error-Handling machen? Einen Log-Eintrag? Eine eigene Fehlermeldung ausgeben? Nichts?
Nichts scheint mir eigentlich die beste Lösung zu sein. Mit einer eigenen Fehlermeldung kann der Besucher nichts anfangen.Und ein Log-Eintrag hilft dem Besucher auch nicht weiter. Ein Log-Eintrag hilft dem Programmierer bzw. den Admin an dieser Stelle ebenso wenig, da es unklar ist warum die Funktion get_list_values() gescheitert ist. Das kann uns nur die Funktion selber sagen, daher wäre ein Log-Eintrag innerhalb der Funktion sinnvoller untergebracht.

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return false;
	}

	return array( 'A', 'B', 'C' );

}

Dadurch sind wir schon mal einen kleinen Schritt weiter. Der Fehler wird da geloggt, wo er tatsächlich auftritt, nicht da wo er zu einen weiteren Fehler führt. Das eigentliche Problem besteht jedoch weiterhin. Denn die Funktion gibt ja noch einen boolschen Wert zurück. Wir müssen also weiterhin prüfen ob das Ergebnis des Funktionsaufrufes tatsächlich von den erwarteten Typs ist. Das ist hinderlich, denn dadurch ist folgendes Konstrukt nicht möglich:

echo get_list( 'ol', get_list_values( 1 ) );

Würde die Funktion nun einen Wert vom erwarteten Typ zurück geben, könnten wir obiges Konstrukt ohne weiteres verwenden. Im Fehlerfall erhalten wir dann anstatt einer Liste lediglich ein leeres Ergebnis. Also ändern wir die Funktion ein klein wenig:

function get_list_values( $fail = false ) {

	if ( true == $fail ) {
		write_to_error_log( sprintf( 'Function %s was called with parameter $fail set to %s', __FUNCTION__, $fail ) );
		return array();
	}

	return array( 'A', 'B', 'C' );

}

Das Logging des Fehlers bleibt, er geht also nicht verloren. Dafür haben wir im Frontend keine Fehlermeldung mehr und der Seitenaufbau bricht auch nicht mehr ab.

Dies können wir nun auf alle Funktionen übertragen. Wenn die Funktion im Erfolgsfall ein Array zurück gibt, sollte sie dies auch im Fehlerfall machen, dann halt ein leeres Array. Erwarten wir ein Objekt als Rückgabewert, so sollte im Fehlerfall ein leeres Objekt zurück kommen. Allerdings sollte man speziell bei Objekten die Eigenschaften vorbelegen und nicht einfach nur ein leeres Objekt zurück geben da oft direkt auf die Objekteigenschaften zugegriffen wird und man, anders als bei Arrays, nicht mit empty() prüfen kann ob das Objekt überhaupt irgendwelche Eigenschaften enthält. Eine Abfrage mit empty() liefert bei Objekten immer false zurück, da ein Objekt niemals “leer” ist.

function get_some_object( $fail = false ) {

	$object = new stdClass();
	$object->foo = false;
	$object->bar = '';
	$object->baz = null;

	if ( true == $fail )
		return $object;

	$object->foo = true;
	$object->bar = 'FooBarBaz';
	$object->baz = $object;

	return $object;

}
$objects = array();
var_dump( $objects );

$objects[] = get_some_object();
$objects[] = get_some_object( true );
$objects[] = new stdClass();

array_walk( $objects, function( $object ) { var_dump( empty( $object ) ); } );

/*
Ausgabe:
array (size=0)
  empty

boolean false
boolean false
boolean false
*/

Der Grund warum man sich damit beschäftigen sollte ist jedoch nicht nur eine saubere Ausgabe im Frontend, sondern auch das Debuggen. Im ersten Beispiel wurde der Fehler nämlich still und heimlich an einen ganz anderen Ort verlagert. Obwohl er in der Funktion get_list_values() aufgetreten ist, wird er erst in get_list() beim Aufruf der foreach-Schleife wirksam. In Beispielen ist es immer einfach den Fehler zu finden. Sie sind von Natur aus kurz und übersichtlich. In größeren und längeren Scripten ist es jedoch oft nicht so einfach heraus zu finden wann und wo eine Variable durch einen Funktionsaufruf einen Wert zugewiesen bekommen hat. Liegen zwischen der Wertzuweisung und der Verarbeitung des Wertes noch andere Zwischenschritte in der der fehlerhafte Wert nicht zu einen Fehler führt, wird die Suche noch umständlicher.
Deshalb sollte man Fehlerfälle immer sofort dort behandeln, wo sie auftreten anstatt sie in einen Pseudozustand umzuwandeln und sie ggf. auch noch als Rückgabewert zurück zu geben.