Webservices mit PHP

Ich hab vor kurzem begonnen mich mit Webservices für PHP zu beschäftigen. Nun gibt es eine Vielzahl von interessanten Artikeln zu diesem Thema. Allerdings geht kaum einer der Artikel darauf ein, wie man ganze PHP Klassen zur Verfügung stellen kann. In den meisten Fällen wird eine einzelne Funktion mit Hilfe der in PHP5 zur Verfügung gestellten Funktion

$soapserver->addFunction("function");

eingebunden.

Nun kann man in PHP als Alternative auch noch eine Klasse zur Verfügung stellen. Sämtliche darin enthaltene Methoden werden dem Webservice zur Verfügung gestellt und können von einem Client genutzt werden, so sie in der WSDL (Web Sevice Description Language) Datei beschrieben werden. Allerdings lässt sich mit dieser Methode nur eine Klasse einbinden. Auch kann die WSDL Datei bei Bereitstellung von vielen Methoden relativ schnell unübersichtlich werden.

Nach einigem herumprobieren habe ich eine Möglichkeit gefunden das Ganze skalierbarer und übersichtlicher zu gestalten.

Um einen generellen Einstieg in die Webservice Programmierung mit PHP zu erhalten ist dieser (englischsprachige) Blog Artikel von JimmyZ sehr zu empfehlen.

Die WSDL Datei

Von dort habe ich auch die grundlegende Struktur für die hier verwendete WSDL Datei. WSDL ist eine Beschreibungssprache für Webservices. Mit ihr kann unter Anderem festgelegt werde mit welchem Server kommuniziert wird, welche Methoden von diesem bereitgestellt werden und welche Parameter übergeben und zurückgeliefert werden.

Der Aufbau einer solchen WSDL Datei mag zuerst etwas kompliziert erscheinen. In den meisten Fällen kann man aber die Grundstruktur einer einmal erstellten Datei wiederverwenden. So müssen nur Dinge wie die zur Verfügung gestellten Methoden und deren Paramter angepasst werden.

Eine WSDL Datei liest sich am Besten von unten nach oben. So ist es wesentlich einfacher die einzelnen Teile der Datei miteinander in Beziehung zu setzen.

Hier erst mal die verwendete WSDL Datei! Die einzelnen Teile erkläre ich dann anschließend.

<?xml version =‘1.0′ encoding =‘UTF-8′ ?>
<definitions
 targetNamespace=‘urn:Soap_Server’
 xmlns:tns=‘urn:Soap_Server’
 xmlns:soap=‘http://schemas.xmlsoap.org/wsdl/soap/’
 xmlns:xsd=‘http://www.w3.org/2001/XMLSchema’
 xmlns:soapenc=‘http://schemas.xmlsoap.org/soap/encoding/’
 xmlns:wsdl=‘http://schemas.xmlsoap.org/wsdl/’
 xmlns=‘http://schemas.xmlsoap.org/wsdl/’>

<types>
  <schema targetNamespace="urn:Soap_Server" xmlns="http://www.w3.org/2001/XMLSchema">
         <import namespace="http://schemas.xmlsoap.org/soap/encoding/" schemaLocation="http://schemas.xmlsoap.org/soap/encoding/"/>
         <complexType name="FixedArray">
                <complexContent>
                   <restriction base="soapenc:Array">
                          <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:anyType[]"/>
                   </restriction>
                </complexContent>
         </complexType>
  </schema>
</types>

<message name=‘callRequest’>
  <part name=‘apiPath’ type=‘xsd:string’/>
  <part name=‘args’ type=‘xsd:anyType’ />
</message>
<message name=‘callResponse’>
  <part name=‘callReturn’ type=‘xsd:anyType’/>
</message>

<portType name=‘Soap_ServerPortType’>
  <operation name=‘call’>
    <input message=‘tns:callRequest’/>
    <output message=‘tns:callResponse’/>
  </operation>
</portType>

<binding name=‘Soap_ServerBinding’ type=‘tns:Soap_ServerPortType’>
  <soap:binding style=‘rpc’
   transport=‘http://schemas.xmlsoap.org/soap/http’/>
  <operation name=‘call’>
    <soap:operation soapAction=‘urn:call’/>
    <input>
      <soap:body namespace="urn:Soap_Server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
    </input>
    <output>
      <soap:body namespace="urn:Soap_Server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
    </output>
  </operation>
</binding>

<service name=‘Soap_ServerService’>
  <port name=‘Soap_ServerPort’ binding=‘tns:Soap_ServerBinding’>
    <soap:address location=‘http://yourdomain/soap/server/server.php’/>
  </port>
</service>
</definitions>

Fangen wir also ganz unten an. In diesem Teil gibt man die Adresse des Servers an, mit dem kommuniziert wird.

<service name=‘Soap_ServerService’>
  <port name=‘Soap_ServerPort’ binding=‘tns:Soap_ServerBinding’>
    <soap:address location=‘http://yourdomain/soap/server/server.php’/>
  </port>
</service>

Der “Soap_Server” Teil der verschiedenen name-Attribute ist übrigens frei wählbar, sollte aber in der gesamten WSDL Datei einheitlich sein.
Das binding-Attribut verweist auf den darüberliegenden Teil mit den Bindings.
Der Teil schaut umfangreicher aus als er tatsächlich ist. Hier wird beschrieben wie der Client und der Server miteinander kommunizieren. Für jede zur Verfügung gestellte Methode ist ein eigener operation-Block notwendig. Ändern muss man nur den Namen der auszuführenden Methode. In diesem Fall gibt es nur eine Operation mit dem Namen “call”.

<binding name=‘Soap_ServerBinding’ type=‘tns:Soap_ServerPortType’>
  <soap:binding style=‘rpc’
   transport=‘http://schemas.xmlsoap.org/soap/http’/>
  <operation name=‘call’>
    <soap:operation soapAction=‘urn:call’/>
    <input>
      <soap:body namespace="urn:Soap_Server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
    </input>
    <output>
      <soap:body namespace="urn:Soap_Server" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
    </output>
  </operation>
</binding>

Der nächste Teil verbindet den Bindings-Block mit den Message-Blöcken. Also immer darauf achten die gleichen Namen zu verwenden, sodass die einzelnen Teile miteinander in Beziehung stehen. Wieder gilt: pro Methode ein operation-Block.

<portType name=‘Soap_ServerPortType’>
  <operation name=‘call’>
    <input message=‘tns:callRequest’/>
    <output message=‘tns:callResponse’/>
  </operation>
</portType>

In den Message-Blöcken werden die Parameter der angegebenen Methoden beschrieben. Für jedes message-Attribut der einzelnen Operationen, die wir im portType-Block definiert haben, wird ein Message-Block benötigt. In diesem Fall also zwei (einer für die zu übergebenden Parameter und einer für den Rückgabewert).
Für jeden zu übergebenden Parameter ist ein part-Element notwendig, in dem man den Namen des Parameters und den Datentyp beschreibt.

<message name=‘callRequest’>
  <part name=‘apiPath’ type=‘xsd:string’/>
  <part name=‘args’ type=‘xsd:anyType’ />
</message>
<message name=‘callResponse’>
  <part name=‘callReturn’ type=‘xsd:anyType’/>
</message>

Der Typ anyType ist ein eigens definierter Datentyp. Dies wird im (optinalen) types-Block gemacht. Wenn nur primitive Datentypen verwendet werden kann man ihn auch weglassen. Da in diesem Beispiel Arrays übertragen werden ist aber die Deklaration eines komplexen Datentypen erforderlich.

<types>
  <schema targetNamespace="urn:Soap_Server" xmlns="http://www.w3.org/2001/XMLSchema">
         <import namespace="http://schemas.xmlsoap.org/soap/encoding/" schemaLocation="http://schemas.xmlsoap.org/soap/encoding/"/>
         <complexType name="FixedArray">
                <complexContent>
                   <restriction base="soapenc:Array">
                          <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:anyType[]"/>
                   </restriction>
                </complexContent>
         </complexType>
  </schema>
</types>

Im definitions-Element werden noch die Namensräume deklariert. Bis auf einige Änderungen, die denke ich selbsterklärend sind, kann man diesen Teil in jeder WSDL Datei so lassen wie er ist.

<definitions
 targetNamespace=‘urn:Soap_Server’
 xmlns:tns=‘urn:Soap_Server’
 xmlns:soap=‘http://schemas.xmlsoap.org/wsdl/soap/’
 xmlns:xsd=‘http://www.w3.org/2001/XMLSchema’
 xmlns:soapenc=‘http://schemas.xmlsoap.org/soap/encoding/’
 xmlns:wsdl=‘http://schemas.xmlsoap.org/wsdl/’
 xmlns=‘http://schemas.xmlsoap.org/wsdl/’>

Der Server

Nun komme ich zum PHP Teil. Der Server wird mit der in PHP5 integrierten Soap Unterstützung aufgebaut. Der Soap-Server macht nichts anderes als eintreffende Anfragen zu behandeln und eine Antwort zurückzugeben. Die WSDL Datei stellt hier die Schnittstelle zwischen Server und Client dar.
Der folgende Code erstellt einen neuen Soap-Server.

<?php

ini_set("soap.wsdl_cache_enabled", "0");        // Schaltet den WSDL Cache aus.

// Ein neuer Soap Server dem die WSDL Datei übergeben wird. Diese liegt im selben Verzeichnis wie diese Datei.
$server = new SoapServer(‘mywsdl.wsdl’);

// Setzt die zu verwendende Klasse. Wenn sich diese in einer seperaten Datei befindet muss sie vorher mittels include inkludiert werden.
$server->setClass(‘Soap_Server’);
$server->handle();                              // startet den Soap Server

In der hier eingebundenen Klasse könnten jetzt verschiedenste Methoden stehen, die alle in der WSDL Datei beschrieben werden können und so dem Client verfügbar gemacht werden.
Ich mache es aber so, dass ich nur eine Methode bereitstelle, die mir je nach übergebenen Parametern eine Klasse instanziert und eine bestimmte Funktion aufruft.
Der Vorteil dieser Variante liegt eigentlich auf der Hand. Ich muss bei einer Erweiterung meines Webservices keine Änderungen an der WSDL Datei durchführen. Im Grunde muss ich nur bekannt geben mit welchem Aufruf meine neu eingebundenen Klassen und Methoden erreichbar sind.

Hier also der zweite Teil der Datei server.php

class Soap_Server
{      
        public function call($apiPath, $args = array())
        {
                // Der API Pfad besteht aus dem Namen der aufzurufenden Klasse und der Methode, getrennt durch einen Punkt. Die Werte werden in den beiden Variablen gespeichert.
                list($resourceName, $methodName) = explode(‘.’, $apiPath);
               
                // War der übergebene API Pfad falsch wird ein Soap Fehler ausgelöst. Dieser kann vom Client abgefangen und ausgewertet werden.
                if (empty($resourceName) || empty($methodName)) {
                         throw new SoapFault(‘Client’, ‘API Path incorrect’, ’server.php’, );
                }
               
                // Exisitiert die Datei nicht, wird ebenfalls ein Soap Fehler ausgelöst
                if (!file_exists("api/".$resourceName.".php")) {
                         throw new SoapFault(‘Client’, ‘API Path incorrect’, ’server.php’, );
                }
               
                // Datei wird eingebunden…
                require_once("api/".$resourceName.".php");
               
                // … und instanziert.
                $classInstance = new $resourceName();
               
                // Und noch eine Fehlerüberprüfung ob die Methode auch aufrufbar ist.
                if (is_callable(array($classInstance, $methodName)))
                {
                        // Wenn ja, dann wird sie aufgerufen und das Ergebnis zurückgeschickt
                        return $classInstance->$methodName((is_array($args) ? $args : array($args)));
               
                } else {
                        // Wenn nicht… dann lösen wir wieder einen Soap Fehler aus.
                        throw new SoapFault(‘Client’, ‘API Path incorrect’, ’server.php’, );
                }
        }
}

?>

So oder ähnlich könnte man die eigentlich gewünschte Methode aufrufen. In der Praxis bietet sich an das Ganze über eine XML-Konfigurationsdatei zu mappen, sodass der API Pfad nicht unbedingt dem Speicherort oder Namen der Klasse entsprechen muss.

Die ganze Arbeit erledigt hier die call-Methode. Im Grunde kann ich so die verschiedensten Klassen bereitstellen. Ein Beispielklasse könnte so aussehen. Die bereitgestellten Methoden sollen hierbei nur das Prinzip verdeutlichen.

<?php

class Books
{

        // Diese Daten würden normalerweise aus einer Datenbank kommen.
       
        private $booklist = array(
       
                array(
                        ‘title’ => ‘Lord of the Rings’,
                        ‘genre’ => ‘Fantasy’,
                        ‘author’ => ‘J.R.R. Tolkien’
                ),
               
                array(
                        ‘title’ => ‘1984′,
                        ‘genre’ => ‘Science Fiction’,
                        ‘author’ => ‘George Orwell’
                ),
               
                array(
                        ‘title’ => ‘Brave new World’,
                        ‘genre’ => ‘Science Fiction’,
                        ‘author’ => ‘Aldous Huxley’
                )
        );

        // Liefert Bücher zurück die den Suchkriterien entsprechen.
        public function items($filters)
        {
                if (is_array($filters))
                {
                        $list = array();
                       
                        foreach($this->booklist as $key => $book)
                        {
                                $inc = true;
                                foreach($filters as $filtername => $value)
                                {
                                        if($book[$filtername] !== $value)
                                        {
                                                $inc = false;                                          
                                        }
                                }
                               
                                if ($inc) $list[] = $book;
                        }
                        return $list;
                }
                else
                {
                        return $this->booklist;
                }
        }
       
        // Liefert genau ein Buch, dass dem übergebenen Titel entspricht.
        public function info($args)
        {
                if (is_array($args) && isset($args[‘title’]))
                {
                        foreach($this->booklist as $book)
                        {
                                if($book[‘title’] === $args[‘title’])
                                {
                                        return $book;
                                }
                        }
                }
                return null;
        }
}
?>

Der Client

Der Client ist eigentlich recht einfach umgesetzt. Es bedarf eigentlich nur einer Zeile um einen SoapClient zu starten. Diesem wird der URL der WSDL Datei übergeben.

<?php

ini_set("soap.wsdl_cache_enabled", "0"); // Schaltet den WSDL Cache aus.

// Ein neuer Soap Client dem die WSDL Datei übergeben wird.
$proxy          = new SoapClient(‘http://localhost/soap/server/mywsdl.wsdl’);

// Und so schauen die Aufrufe aus. Die Ausgaben von var_dump habe ich in Kommentaren darunter geschrieben.
var_dump($proxy->call("Books.items", array(‘genre’ => ‘Science Fiction’)));
echo "<br /><br />";
// array(2) { [0]=> array(3) { ["title"]=> string(4) "1984" ["genre"]=> string(15) "Science Fiction" ["author"]=> string(13) "George Orwell" } [1]=> array(3) { ["title"]=> string(15) "Brave new World" ["genre"]=> string(15) "Science Fiction" ["author"]=> string(13) "Aldous Huxley" } }

var_dump($proxy->call("Books.items", array(‘genre’ => ‘Fantasy’, ‘author’ => ‘J.R.R. Tolkien’)));
echo "<br /><br />";
// array(1) { [0]=> array(3) { ["title"]=> string(17) "Lord of the Rings" ["genre"]=> string(7) "Fantasy" ["author"]=> string(14) "J.R.R. Tolkien" } }

var_dump($proxy->call("Books.items", array(‘author’ => ‘George Orwell’)));
echo "<br /><br />";
// array(1) { [0]=> array(3) { ["title"]=> string(4) "1984" ["genre"]=> string(15) "Science Fiction" ["author"]=> string(13) "George Orwell" } }

var_dump($proxy->call("Books.info", array(‘title’ => ‘Brave new World’)));
//array(3) { ["title"]=> string(15) "Brave new World" ["genre"]=> string(15) "Science Fiction" ["author"]=> string(13) "Aldous Huxley" }

?>

Abschließend möchte ich noch kurz auf die Fehlerbehandlung eingehen, auf die ich im voranstehenden Client Beispiel verzichtet habe. Da der Server ja Fehler auslöst, falls zum Beispiel ein falscher API Pfad übergeben wird, gilt es diese auch abzufangen. Am Besten macht man das mit einem try/catch Block.
Im fogenden Code Beispiel wurde der erste Aufruf verändert. Hier wird durch einen Schreibfehler die Klasse Book anstatt Books aufgerufen. Da es diese nicht gibt, wird ein Fehler ausgelöst, den wir aber abfangen.

try {
        $return = $proxy->call("Book.items", array(‘genre’ => ‘Science Fiction’));
        var_dump($return);
} catch(SoapFault $ex) {
        echo "Fehler: ".$ex;
}

Kommentieren ist momentan nicht möglich.