Im Vollbesitz unserer geistigen Kräfte, sprich, allem was wir zur Verarbeitung an und für sich brauchen, fehlt uns jetzt nur noch eine standardisierte Kommunikation mit dem Server. Natürlich könnten wir hier auch unsere eigene Aufruflogik entwickeln, aber da proprietäre Lösungen zum einen unschön und zum anderen fast immer mit erheblichem Mehraufwand zur Überbrückung von Inkompatibilitäten verbunden sind, sollte von dieser Idee schnellstens Abstand genommen werden. Ganz besonders dann, wenn es doch bereits einen zwar noch recht jungen, dafür aber immerhin existierenden Standard gibt: JSON-RPC. Idealerweise wird über die POST-Methode ein standardisiertes JSON-Objekt an den Server übergeben und von diesem nach Ausführung der gewünschten Funktion zurückgeliefert.
Fehlt bei einem Funktionsaufruf das Property id, so spricht der Standard von einer „Notification“, was einem Funktionsaufruf ohne Rückgabewert in PHP gleichkommt. Da eine HTTP-Anfrage jedoch immer etwas zurückliefern sollte, dürfte das bewusste Auslassen der ID im Webumfeld nur in den seltensten Fällen Sinn machen.
method | Name der aufzurufenden Funktion |
params | Array mit Parametern für die Funktion |
id | Eindeutige ID zur Zuordnung der späteren Antwort, auf NULL zu setzen, wenn keine Antwort erwartet/gewünscht wird |
result | Ergebnis-Objekt oder NULL bei Fehler |
error | Fehler-Objekt oder NULL, wenn kein Fehler |
id | Die ID der Anfrage (siehe Funktionsaufruf) |
Der Aufbau dieser Objekte ist sehr übersichtlich und schlicht gehalten (siehe Kasten „JSON-RPC-Objekte“), beinhaltet aber alles Notwendige, um eine reibungslose Kommunikation sicherzustellen. Ganz im Sinne der Wiederverwendbarkeit von Quellcode zeigt Listing 7a daher eine generische Implementierung eines einfachen JSON-RPC-Servers unter Verwendung der PECL-Extension und in PHP-5-Notation. Natürlich ließe sich ein derartiger Service auch mit älteren PHP-Versionen realisieren, die neuen Funktionen in der aktuellen PHP-Serie machen das Leben jedoch um einiges leichter.
Listing 7a -------------------------------------------------------------------- method) || empty($payload->method)) { throw new Exception('Keine Methode definiert...'); } // Wir verwenden :: als Trenner zwischen Klasse und Methode... if (strpos($payload->method,'::')===false) { throw new Exception('Keine Klasse definiert...'); } // JSON-Methode in Zielklasse und Methode aufspalten list($class,$method)=explode('::',$payload->method,2); // Klasse instanzieren - das Laden übernimmt __autoload() $obj = new $class(); if (!$obj) { throw new Exception("Fehler beim Instanziieren der Klasse '$class'"); } // Kennt unsere Klasse die gewünschte Methode? if (!is_callable( array($obj,$method))) { throw new Exception("Die Methode '$method' existiert nicht in der Klasse '$class'"); } // Methode aufrufen, ggfs. vorhandene Parameter weiterreichen ... if ( !call_user_func_array(array($obj, $method),$payload->params) ) { throw new Exception("Die Ausführung der Methode '$method' ist fehlgeschlagen!"); } // Das Ergebnis aus dem Puffer im result-Property sichern $response->result=$obj->buffer; } catch (Exception $e) { // Irgendwas ist schiefgelaufen .. // Fehler-Objekt vorbereiten .. $err=new stdClass; // Daten aus der Exception übernehmen $err->message = $e->getMessage(); $err->line = $e->getLine(); $err->file = $e->getFile(); $err->trace = $e->getTraceAsString(); // und das Error-Property setzen $response->error=$err; } // Request-ID durchschleifen $response->id=$payload->id; // Gzip-Kompression aktivieren ob_start("ob_gzhandler"); // Und unsere JSON-Antwort ausgeben echo json_encode($response); ?>
Da JSON-RPC zurzeit noch keinerlei Vorgabe zur Behandlung von klassenbasierten Aufrufen vorschreibt, verwenden wir einfach den aus der OOP bekannten Operator :: zur Trennung zwischen Klasse und Methode. Der Server-Code teilt daher den via method-Property übergebenen Funktionsnamen entsprechend auf, lädt – dank _autoload für uns automatisch – die benötigte Klasse nach und instanziiert diese. Ist die gewünschte Methode in der Klasse vorhanden und auch aufrufbar, führen wir den Funktionsaufruf durch, wobei die ggfs. von JSON-RPC gelieferten Parameter durchgereicht werden. Sollte bei diesem ganzen Procedere etwas schief laufen, fangen wir die geworfenen Exceptions in einer JSON-RPC verträglichen Weise ab: Es wird ein Error-Objekt erzeugt und die aus der Exception bekannten Properties und Methoden zur Informationsgewinnung aufgerufen bzw. eingebunden. Auch ohne großen Aufwand lassen sich so schnell und effektiv kleine oder große Web Services aufsetzen. Um das Ganze etwas zu verdeutlichen, werden wir uns das anhand einer kleinen Galerie noch einmal näher anschauen. In einem bewusst einfach gehaltenen HTML-Dokument (Listing 6a) befinden sich neben den für AJAX notwendigen Skripten eigentlich nur noch zwei Objekte, die von Interesse sind: das Bild (id: image) und die Überschrift (id:desc).
Listing 6a ------------------------------------------------------------------PHPMagazin JSON Gallerie
Um die in JavaScript erzeugten Objekte serialisieren zu können, greifen wir auf die Stringify-Methode der JSON-Klasse zurück, deren Quelltext man kostenfrei von json.org beziehen und verwenden kann. Will der geneigte Besucher unserer kleinen Galerie nun zum nächsten Bild wechseln, erfragen wir uns via JSON-RPC vom Server die für uns relevanten Informationen (Listing 6b), in diesem Fall den Dateinamen und eine kurze Beschreibung des Bildes.
Listing 6b -------------------------------------------------------------------- // xmlHttpRequest-Objektinstanz var xhttp=null; var Position=-1; // onLoad-Funktion function init(){ [ ... siehe Listing 1 ... ] } function gallery_callback() { if (xhttp.readyState==4) { var res=eval('('+xhttp.responseText+')'); if (res.error) { alert(res.error.message); return; } document.getElementById('desc').innerHTML=res.result.desc; document.getElementById('image').alt=res.result.desc; document.getElementById('image').src=res.result.src; Position=res.result.position; } } function prevImage() { return loadImage(Position - 1); } function nextImage() { return loadImage(Position + 1); } function loadImage(Ziel) { if (!xhttp) return false; // onReadyStateChange Handler xhttp.onreadystatechange=gallery_callback; // Request-Objekt zusammenbauen var request={ method: 'jsonGallery::getImage', params: [Ziel], id: new Date().getTime() }; // JSON enkodieren var payload=JSON.stringify(request); // asynchronen GET Request vorbereiten und abschicken xhttp.open('POST', 'server.php', true); xhttp.send(payload); return true; }
Die Rückantwort des Servers liegt natürlich im JSON-Quelltext vor, sodass wir alles mit einem einfachen eval() in reale JavaScript-Variablen umwandeln und unsere HTML-Elemente aktualisieren können. Doch Vorsicht: Stammen die Daten aus einer nicht vertrauenswürdigen Quelle, sollte anstelle von eval() auf die parse()-Methode der JSON-Klasse zurückgegriffen werden, damit sichergestellt ist, dass keine XSS-Angriffe durchgeführt werden können. Die einzige von außen via JSON-RPC aufrufbare Methode unseres Backend-Moduls (Listing 7b) ist schnell erklärt, es handelt sich um die Funktion getImage: Basierend auf der übergebenen Index-Position wird ein dynamisch erzeugtes Objekt mit den Daten des Bildes sowie der neuen Position zunächst im Puffer zwischengespeichert und später an den Client übertragen. Ist unter der angegebenen Index-Position kein Eintrag vorhanden, teilen wir dies dem Benutzer mit, wenngleich dies erst als Fehlermeldung im Frontend sichtbar wird.
Listing 7b --------------------------------------------------------------------- imageList=Array(); $this->imageList[]=Array('file' => 'pic1.png', 'desc' => 'JSON Beispiel Gallerie'); $this->imageList[]=Array('file' => 'pic2.png', 'desc' => 'Das 2te Bild'); $this->imageList[]=Array('file' => 'pic3.png', 'desc' => 'Alle guten Dinge...'); // Puffer vorbereiten $this->buffer = new stdClass; } // Die durch JSON-RPC aufzurufende Methode public function getImage($pos) { // $pos validieren if ($pos = count($this->imageList)) { throw new Exception("'$pos' ist ungültig"); } // Ergebnis-Objekt befüllen $this->buffer->position=$pos; $this->buffer->src=$this->imageList[$pos]['file']; $this->buffer->desc=$this->imageList[$pos]['desc']; return true; } } // class ?>
Auch wenn JavaScript natürlich alles andere als neu ist und folglich auch seine syntaktischen Möglichkeiten seit langem bekannt sind, bietet sich JSON als Übertragungsformat neben XML geradezu an. Im Zusammenspiel mit dem noch jungen JSON-RPC wird dies, angetrieben vom Marketinghype um Web 2.0, den etablierten Web-Services-Formaten XML-RPC und SOAP sicher einiges an Marktanteil abnehmen. Die Tatsache, dass es Überlegungen gibt, PHP standardmäßig mit Unterstützung zur En- und Dekodierung von JSON-Quellcode auszuliefern, sowie die rasant zunehmende Zahl an Frameworks mit JSON Support sprechen eine deutliche Sprache. Auch die Anzahl der im Netz verfügbaren Web Services auf JSON-Basis nimmt zu: So hat beispielsweise Google seit einiger Zeit ebenfalls JSON im Programm, wenn auch leider bisher mit einem zum Standard inkompatiblen RPC-Ansatz. Zurzeit befindet sich im Übrigen die Version 1.1 von JSON-RPC in Planung. Wer sich nach diesem Artikel daran beteiligen möchte, ist herzlich eingeladen. Der Workingdraft zur neuen Version enthält schon einige sehr interessante Neuerungen und Verbesserungen, sodass diese Fassung wohl den endgültigen und webweiten Durchbruch dieses Protokolls sicherstellen wird.
Auf JavaScript-Seite lässt sich das Ganze leicht auflösen: einfach ein Objekt anstelle des Arrays verwenden und die Welt ist wieder in Ordnung. Doch leider sieht PHP das natürlich anders, Objekte sind halt eben doch keine Arrays. Wer PHP 5.1 einsetzt, kann dies durch das Verwenden der durch die SPL-Erweiterung eingeführten Iteratoren, im Speziellen des Array-Objekts, etwas entschärfen oder dem JSON-Decoder mitteilen, dass anstelle von Objekten immer assoziative Arrays erzeugt werden sollen.
Für die Zukunft, gerade wenn man seine Programmierung objektorientiert gestaltet, sollte jedoch mit der von JSON beim Decode-Vorgang verwendeten stdClass gearbeitet und zumindest für die Übergabe von Daten von und nach JavaScript auf assoziative Arrays verzichtet werden.
Hinterlasse einen Kommentar
Hinterlasse den ersten Kommentar!