REST-Services sind ja nicht mehr neu und haben gerade die klassischen WebServices (SOAP) an vielen Stellen ersetzt. Wer mal mit SOAP, auch auf einen niedrigeren Level, gearbeitet hat, wird dies nicht als Verlust sehen. Es gibt viele Frameworks für REST-Services und auch lassen sich die meisten anderen Frameworks ohne Probleme so anpassen, dass sie keine HTML-Seite zurück liefern sondern das Ergebnis als JSON. XML wäre auch möglich als Ausgabeformat, aber ist sehr sehr wenig verbreitet. XML ist meistens sehr viel größer, da "name":"value" viel kürzer ist als <name>value</name> oder <attribute name="name" value="value"/>.
Meine Erfahrungen mit dem Parsen von XML und JSON in PHP ist aber, dass beide eigentlich gleich schnell sind. Ein Wechsel bei Config-Files von XML auf JSON hat keine Verbesserung der Ladezeiten ergeben. Auch die Serialisierung von Objekten bei Java war mit JSON nur minimal schneller als mit XML, wobei es dort auch sehr vom Verwendeten Framework abhängig war. Also kann es sein, das es in neueren PHP-Versionen doch Unterschiede geben wird, wenn dort noch mehr optimiert wird.
Mit das größte Problem auf dass ich bei REST-Services in PHP gestoßen bin ist das Parsen der URLs. Hier besteht einfach das Problem, dass es immer viele Methoden pro Service gibt und das einlesen der Mapping-Patterns somit doch relativ aufwendig sein kann. Während man in einem Java-Servlet nur einmal am Anfang alles parsen muss, muss man in PHP bei jeden Request von vorne beginnen. Eine Lösung wäre das Speichern der Ergebnisse in der Session, wobei hier dann aber das Problem bleibt, dass man bei Änderungen diese erkennen und den Inhalt der Session erneuern muss. Man kann das XML als String lesen und einen MD5-Hash bilden. Damit könnte man verschiedene Versionen des Services in der Session identifizieren. Am schönsten wäre natürlich den Zugriff auf das Dateisystem komplett einsparen zu können, aber dass scheint erstmal so nicht machbar zu sein.
Das Gleiche gilt auch für das Objekt, dass die Methoden enthält auf die die URLs gemappt werden. Man muss sich auch deutlich machen, dass ein REST-Service im Grunde kein Konstrukt der objekt-orientierten Programmierung ist, sondern die Facade im Grund klassischen prozedualen Prinzipen folgt. Gerade wenn man Sicherheits-Token und so verwendet und nicht die Session für die Speicherung des eingelogten Benutzer verwendet, merkt man dass man auch ohne Klasse mit einfachen functions hier genau so gut arbeiten könnte. Aber wir verwenden eine Klasse, die den Service beschreibt, um auch keinen Stilbruch zu haben. Ein Objekt zu erzeugen ist teuer und theoretisch könnte man sich dieses hier ersparen.
Mein AFP2-Framework benutzt die selben Klassen wie mein älteres aoop-Framework. Es implementiert momentan noch kein wirkliches Routing sondern nutzt einfach Regex-Ausdrücke um die gesamte URL mit einem Match auf die richtige Methode zu mappen. Hier wird also einfach von oben nach unten die Liste durch laufen und immer ein preg_match durchgeführt. Routing mit beliebig tiefer Verschachtelung und Sub-Routen, würde hier viel Zeit ersparen, da damit die Anfangs-Liste relativ klein wäre und schnell abzuprüfen wäre und erst dann in der nächsten Liste weiter geprüft wird. Das kommt dann später nochmal um etwas schneller zu werden.
Ein ganz einfacher REST-Service kann so aussehen:
rest.xml
<service name="walls" class="PostWallREST">
<method name="loadWall" pattern="/^\/rest\/\w+\/wall\/(\d+)(\?.+)?$/">
<argument type="int" requestvalue="false" group="1"/>
<argument type="int" pattern="userId" requestvalue="true"/>
<argument type="string" pattern="_XW_SESSION_SEC" requestvalue="true"/>
</method>
</service>
PostWallREST.php
class PostWallREST{
private $security=null;
private $conv=null;
private $am=null;
public function PostWallREST(){
$this->security=new XWSearchStringParser();
$this->conv=new PWDTOConverter();
$this->am=XWAddonManager::instance();
}
public function loadWall($id,$userId,$secToken){
$resp=new XWRESTResponse();
$resp->singleResult=true;
if(isset($_SESSION["XWUSER"]) && $this->am->getAddonByName("XWUserSession")->checkSessionSecToken($secToken)){
if($_SESSION["XWUSER"]->getId()==intval($userId) && intval($id)>0){
$wall=new PWWall();
$wall->load($id);
if($wall->checkRight($_SESSION["XWUSER"])){
$dto=$this->conv->convertWall($wall,$_SESSION["XWUSER"],true);
$resp->results[0]=$dto;
}
else{
$resp->restMethodSuccess=false;
}
}
else{
$resp->restMethodSuccess=false;
}
}
else{
$resp->error=true;
$resp->errorMessage="no valid session";
}
$resp->newSecToken=$this->am->getAddonByName("XWUserSession")->getURLParameterWithSessionSecTokenValueOnly();
return $resp;
}
}
Wie man erkennt wird im
service-Tag die Klasse angeben, auf die die URLs gemappt werden sollen. Das Mapping der einzelnen Methoden wird dann innerhalb genauer definiert. Jede Method einen grundlegenden Pattern, der für ein Match auf die URL verwendet wird. Wenn dieser passt wird versucht die Argument (die Reihenfolge in der XML muss der Reihenfolge in PHP entsprechen!) aus der URL oder dem Request zu rekonstruieren. Jedes Argument kann ein eigenes Pattern haben. Wenn es ein Request-Value ist, entspricht das Pattern dem Namen im Request. Sollte kein Pattern angeben sein, wird dass Pattern der Method verwendet. Das erspart viel Text, weil man so nur ein allgemein gültiges Pattern schreiben muss. Mit
group wird die Gruppe beim preg_replace angeben. Also die die den benötigten Wert enthält, der als Argument übergeben werden soll. Im Pattern der Method können also schon alle Werte in Gruppen markiert werden und bei den Arguments muss immer nur noch angegeben werden, welche Gruppe verwendet werden soll.
URLs zu REST-Services folgen in aoop und APF2 folgender Regel:
http://..../rest/{modulename}/.....
Über den Modul-Namen wird gesteuert welche rest.xml geladen wird.
/rest/ ist die Markierung, dass hier ein REST-Service angesprochen werden soll. Der Rest der URL kann nach Belieben definiert werden.
Für unser Beispiel wäre z.B. folgende URL lauffähig:
http://..../rest/PostWall/wall/1?userId=1&_XW_SESSION_SEC=XXXXX
Das Ergebnis der Methode wird dann in JSON umgewandelt und auch der Header entsprechend auf "text/json" gesetzt.
Damit lässen sich nun pro Modul REST-Services definieren und deployen. Bei PHP hat man zum Glück nicht das Problem, dass sich ändernde Klasse von anderen REST-Services anderer Module zu Laufzeitfehlern führen können. Wenn man ein Modul ändert, wird die Klasse ja beim nächsten Request erneut geladen und nicht wie bei Java in der VM gehalten. Probleme kann es nur geben, wenn man die Session verwendet und versucht dort gespeicherte serialisierte Daten einer alten Version in ein Objekt der neuen Version der Klasse zu deserialisieren.
Das macht diese Umgebung in PHP für Microservices interessant. Da man hier wirklich einzelne Module zur Laufzeit austauschen kann, ohne dabei die anderen Module zu beeinflussen (wenn man natürlich vorher drauf geachtet hat, dass Klassen, die auch von anderen Modulen verwendet werden, nicht zu Fehlern bei diesen führen). Zu überlegen wäre auch diese Klassen zwischen den Services als JSON und nur über eine URL dem anderen Service verfügbar zu machen. Also die URL auch zu Intra-Service Kommunikation zu verwenden.
Also sowas wie:
$req=$_REQUEST;
$req["userId"]=0;
$req["_XW_SESSION_SEC"]="XXXXX";
$json=XWRESTServicesLocal::call("/rest/PostWall/wall/1",$req);
Und den JSON-String dann auf eine eigene Klasse zu mappen. So etwas werde ich für die nächste Version dann einbauen.
Das war jetzt ein grober Überblick wie meine Frameworks REST-Services implementieren und versuchen dabei möglichst flexibel zu sein, aber auch nicht dabei zu sehr auf Kosten der Performance zu arbeiten. Die Arbeiten an APF2 sind noch in Gange und deswegen kann sich da auch noch einiges Ändern und vielleicht lerne ich von anderer Seite noch einiges, wie sich auch die letzten Probleme lösen lassen, um so REST-Services noch besser implementieren zu können.