Web-2.0-Anwendungen mit JSON und JSON-RPC
Kommentare

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

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.

JSON-RPC-Objekte Der JSON-RPC-Standard legt zurzeit genau zwei Objekte zur Kommunikation fest, jeweils eines zum Aufruf einer Funktion und eines für die Antwort. Die unterhalb von params (Aufruf) bzw. result und error (Antwort) definierten Elemente stehen dem Entwickler vollständig zur freien Gestaltung offen. Um standardkonform zu arbeiten, sollten sie jedoch für den Fall, dass sie nicht benötigt werden, explizit auf NULL gesetzt werden.

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  

Willkommen.

Willkommen

[ zurück ] [ weiter ]

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

?>
Zukunft

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.

Problemfall „Assoziative Arrays“ Die Behandlung der in PHP gerne verwendeten assoziativen Arrays stellt ein Problem dar: Aus Sicht von JavaScript gibt es zwischen einem assoziativen Array und einem Objekt kaum einen Unterschied, sodass ein Zugriff auf Objekt-Properties auch über die sonst für Arrays gewohnte Schreibweise (objekt[‚property‘]) möglich ist. Beim Serialisieren in JSON-Quelltext hingegen fällt das System auf die Nase, da zumindest der JSON-Encoder von json.org bei einer Variablen des Typs Array von einem indexbasierten Element ausgeht, folglich array.length auswertet und als Wert 0 zurückgeliefert bekommt. Die „Länge“ eines assoziativen Arrays ist in JavaScript schlicht nicht definiert.

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.

Arne Blankerts ist Leiter der Entwicklung bei der SalesEmotion AdSolutions GmbH in Hamburg.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -