Allgemein (69)
Rating: 0 (0)


[b]PHP OOP: Dynamisch generierte Accessor-Methoden mit Exception-Handling[/b] Für ein aktuelles kommerzielles Projekt, welches sowohl das PHP- als auch das JS-Framework des GCv5 benutzt, wollte ich die Business Logic noch stärker als bisher objektorientieren. D.h. jede Entität der sollte (ähnlich den User-Objekten im GCv5) über eine zugehörige Klasse mit standardisierten Interfaces und Methoden dargestellt werden können. Nachdem ich diesen Ansatz schon länger im GCv5 verfolgte und dadurch einen sehr strukturierten und erstaunlich flexiblen Code erzeugen konnte, wollte ich diesmal noch ein Stück weiter gehen. Ein grundsätzliches Problem bei der Entwicklung mit PHP ist es, dass die Sprache es einem zu leicht macht. Wo Java einen in ein objektorientiertes Korsett zwängt (und dadurch das Verhältnis Codemenge - Nutzen total ad absurdum führt) und Perl für jede nur erdenkliche Aufgabe mindestens zehn verschiedene Wege mit unterschiedlicher Syntax kennt (und es somit schlicht und einfach unmöglich macht, fremden Code zu verstehen und modifizieren), ist PHP "einfach einfach" und benötigt kein durchgehendes Konzept. "Jeder Depp" kann mit PHP einigermaßen brauchbare Ergebnisse (nach aussen hin) liefern. Genau dadurch aber hat PHP einen so schlechten Ruf bekommen: Vereinfachung führt zu grausigen Hacks, Sicherheitsproblemen, unstrukturiertem, schwert wartbaren Code usw. Lange Rede, kurzer Sinn: Bis jetzt hatte ich es mir mit meinen Klassen einfach gemacht. Member Variables wurden einfach deklariert oder auch zur Laufzeit aus Datenbankrückgaben erzeugt, abgefragt und modifiziert. An sich war das schon ein brauchbarer Aufbau, die Struktur selbst war gut. Allerdings auch fehleranfällig: Durch die fehlende Typisierung von PHP passierte es schnell, dass falsche Werte in Member Variables landeten und dadurch auch in der Datenbank... bzw. nicht ganz, da derartige Vorkommnisse gern SQL-Fehler produzieren. Umgehen lässt sich das - zumindest teilweise - durch das saubere Benutzen von Get- und Set-, sogenannten Accessor-Methoden. Wer sich etwas mit OOP auskennt wird sagen, Accessor-Methoden sind eh Standard und sollten sowieso benutzt werden. Allerdings ist es natürlich ein gewisser Aufwand, sie zu schreiben und die Klassen werden dadurch auch nicht übersichtlicher. PHP liefert allerdings ein paar schöne Möglichkeiten, in diesem Fall das Runkit. Dieses PECL-Lib kann unter FreeBSD einfach über die Ports installiert werden und bindet sich als Shared Library in PHP ein. Es ermöglicht das dynamische Verändern von Klassen zur Laufzeit. Damit ist es möglich, die Accessor-Methoden automatisiert zu erstellen, ohne dass unnötiger Code die Klassen aufbläht. Zuerst wird in der entsprechenden Klasse (um Beispiel Class User) ein Template für die Variablen angelegt: [pre] protected static $typemap = array ( "id" => false, "login" => "str", "passwd" => "str", "firstname" => "str", "lastname" => "str", "email" => "str", "clientid" => "int", "status" => "int" ); protected $data = array (); private $rel = array (); [/pre] $typemap definiert, welche Variablen später gehandlet werden sollen inklusive deren Typs. Sie entsprechen den Spalten eines User-Datensatzes in der Datenbank. Damit die Beschreibung nicht zur Laufzeit von anderen Komponenten der Applikation geändert werden kann wird $template als "private" deklariert, zudem noch als "static", um den Inhalt auslesen zu können, ohne eine Instanz der Klasse erzeugen zu müssen. $data und $rel halten später die tatsächlichen Daten, wobei $data die Eigenschaften des User-Objekts selbst enthält und $rel ein nützlicher kleiner Hack ist: Oft benötige ich für Listen von Objekten mehr als nur deren eigene Eigenschaften. Ein User ist z.B. über den Foreign Key "clientid" mit einem Client-Objekt verbunden, welches die Eigenschaft "client_name" besitzt. Ein einfacher INNER JOIN verknüpft die Eigenschaften des Clients mit denen des Users. Aus einem ausgelesenen Datensatz kann direkt ein User-Objekt erzeugt werden (bzw. die User-Klasse besitzt eine Methode, welche den Datensatz in $data einfügt). Dabei werden alle aus weiteren Tabellen ausgelesenen Daten mit deren Schlüssel in das assoziative Array $rel eingetragen und können später direkt über das User-Objekt bezogen werden. Anyway, wie entstehen nun die Accessor-Methoden für die Variablen? Hier kommt ein bisschen Magie des Runkit ins Spiel. Nachdem die Includes mit den Klassen eingelesen wurden, werden diesen die gewünschten Methoden hinzugefügt: [pre] $classes = array ("User"); foreach ($classes AS $class) { runkit_method_add ($class, "typemap", "", 'return self::$typemap;', RUNKIT_ACC_PUBLIC); eval ('$typemap = ' . $class . '::typemap ();'); foreach ($typemap AS $key => $value) { $func = 'if (!array_key_exists ("' . $key . '", $this->data)) throw new Exception (E_KEYNOTFOUND); if (is_null ($arg)) { return $this->data["' . $key . '"]; } else {'; if (!$value) { $func.= 'throw new Exception (E_CANTSET);'; } elseif ($value == "str") { $func.= 'if (!is_string ($arg)) throw new Exception (E_TYPE_STRING);'; } elseif ($value == "int") { $func.= 'if (!is_int ($arg)) throw new Exception (E_TYPE_INT);'; } elseif ($value == "float") { $func.= 'if (!is_float ($arg)) throw new Exception (E_TYPE_FLOAT);'; } $func.= '$this->data["' . $key . '"] = $arg; }'; runkit_method_add ($class, $key, '$arg = NULL', $func, RUNKIT_ACC_PUBLIC); } $func = 'if (!is_string ($arg)) throw new Exception (E_TYPE_STRING); if (!array_key_exists ($arg, $this->rel)) throw new Exception (E_KEYNOTFOUND); return $this->rel[$arg];'; runkit_method_add ($class, "rel", '$arg', $func, RUNKIT_ACC_PUBLIC); } [/pre] Wie man sieht, wird einfach eine Liste der gewünschten Klassen durchiteriert. Jeder wird dann zuerst über Runkit eine Methode hinzugefügt, um die als private deklarierte Eigenschaft $typemap auszulesen. Für jeden Schlüssel aus $typemap wird dann eine Accessor-Methode erzeugt. Der Code dafür wird einfach in eine Variable geschrieben, die man so z.B. auch evaluieren könnte. $key enthält den Namen der Variablen und bestimmt somit, wo in $data der Inhalt zu finden ist, $value enthält den Typ und kann benutzt werden, um bei falschen Zuweisungen passende Exceptions zu werfen. Der Funktions-String wird dann per Runkit der Klasse hinzugefügt. $key bestimmt den Namen der neuen Methode. Bei der Planung muss man natürlich aufpassen, dass keine Eigenschaft den selben Namen wie eine normale Methode der Klasse hat, sollte allerdings nicht so schwierig sein. ;) Die erzeugten Methoden sind sogar noch etwas eleganter als Set- und Get-Methoden und wirklich integrierte Accessor-Methoden: Wird als Argument nichts oder NULL übergeben, wird der aktuelle Inhalt zurückgegeben. Wird ein Argument übergeben (auch das boolsche false oder ein leerer String) wird diese auf ihren Typ überprüft und gesetzt. Benutzen lässt es sich z.B. so: [pre] $user = new User (1); $user->login ("foo"); echo "login is " . $user->login (); -> login is foo [/pre] Mehr über die Runkit-Funktionen erfährt man im PHP-Manual in der Rubrik [url=http://www.php.net/manual/en/ref.runkit.php]runkit Functions[/url]