zurück
News

Gewusst wie: W3C, PHP und BiPRO Norm 480

zeitsprung-BLOG
|
Marcel Maas
|
26.8.2019

Unsere Softwareentwickler standen in einem Projekt jüngst vor der Herausforderung einen BiPRO Server mit der BiPRO Norm 480 (Such- und Listenservices) zu erstellen und geben einen kleinen Einblick in die gemeisterten Schwierigkeiten bei der Implementierung als PHP Server.

Marcel Maaß über die Implementierungshürden von BiPRO Norm 480 mit PHP als Webservice Server

BiPRO
BiPRO Norm 480
Digitalisierung
PHP
Knowledge-Base
Tech
Interview

Für die angekündigte Orchestrierung von Webservices nach BiPRO Norm gestaltete sich eine Umsetzung als kleine Herausforderung. Im Detail ging es hier um die Umsetzung einer PHP Soap Server Implementierung nach BiPRO Norm 480 (Listenservice). Dieser Service richtet sich strikt nach den W3C Vorgaben für einen Enumeration Webservice und verwendet die dort definierten Elemente. Unter anderem gibt die Definition für diesen Webservice vor, dass diverese Angaben im Header eines Requests gemacht werden müssen.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ws="http://www.w3.org/2011/03/ws-enu" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:allg="http://www.bipro.net/namespace/allgemein" xmlns:basis="http://www.bipro.net/namespace/basis" xmlns:kt-partner="http://www.bipro.net/namespace/kontext/partner" xmlns:provider="http://www.zeitsprung.de/namespace" xmlns:vtrg="http://www.bipro.net/namespace/vertrag">
  <soapenv:Header>
     <wsa:Action>http://www.w3.org/2011/03/ws-enu/Enumerate</wsa:Action>
     <wsa:MessageID>urn:uuid:e36457ff-d0f1-4c85-abe6-6cdf4bd511e9</wsa:MessageID>
     <ws:ReplyTo>
        <wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
     </ws:ReplyTo>
  </soapenv:Header>
  <soapenv:Body>
     <ws:Enumerate>
        <ws:NewContext>
           <ws:Filter Dialect="http://www.bipro.net/namespace/kontext/partner">
              <kt-partner:Filter xsi:type="kt-partner:CT_Partnersuche">
                 <kt-partner:Name>Maaß</kt-partner:Name>
                 <kt-partner:Vorname>Marcel</kt-partner:Vorname>
                 <kt-partner:Unternehmen>
                    <allg:Nummer>1234</allg:Nummer>
                    <allg:Nummernart>BaFin</allg:Nummernart>
                 </kt-partner:Unternehmen>
              </kt-partner:Filter>
           </ws:Filter>
        </ws:NewContext>
     </ws:Enumerate>
  </soapenv:Body>
</soapenv:Envelope>
Beispiel 1: Header für Enumerate Request

Einfache Umsetzung mit dem PHP Soap Server ohne Decorator

Für die Implementierung der PHP Soap Server Klasse, die eingehende SOAP XML Requests verarbeitet, benutzen wir das Decorator Pattern, um diverse Validierungen durchzuführen, bevor der eigentliche Service initialisiert wird. Im Normalfall übernimmt dieser Dekorierer die Validierung eines angegebenen Security Tokens und prüft diesen gegen diverse Merkmale. Erst wenn ein Security Token valide ist, wird der eigentliche Webservice initialisiert.

Als unerwartet problematisch stellten sich bei dieser Umsetzung aber die zusätzlichen Header Angaben Action, MessageID und ReplyTo heraus. Ein Consumer muss diese Angaben nach Vorgaben der W3C liefern. zeitsprung als Service Provider ist angehalten die gelieferten Daten zu prüfen. Sobald die genannten Elemente im Header vorhanden sind, möchte der PHP Soap Server Methoden aufrufen, die genau diesen Elementen entsprechen. Unsere Erwartungshaltung war eigentlich, dass direkt die Enumerate Funktion aufgerufen wird. Dies ist aber nicht so. Wie sich heraus stellte, sucht der PHP Soap Server zunächst nach Funktionen, die nach den Elementen im Header benannt sind.

Schauen wir uns an, was hier auf PHP Seite passiert. Zunächst einmal ein vereinfachtes Beispiel unserer Service Klasse für unseren Listenservice. Um in das Thema einzusteigen, lassen wir den Decorator erst einmal weg. Dazu später mehr.

<?php
namespace Zeitsprung\Norm480\Partner\Soap;

use Zeitsprung\Norm480\Entity\Enumerate\EnumerateRequest;
use Zeitsprung\Norm480\Entity\Enumerate\EnumerateResponse;

use SoapFault;

class ListenService
{
   /**
    * Verarbeitung des Enumerate Requests
    *
    * @param EnumerateRequest $request Request als Klassenstruktur
    * @throws SoapFault
    * @return EnumerateResponse
    */
   public function EnumerateOp(EnumerateRequest $request) : EnumerateResponse
   {
       // Verarbeitung des Enumerate Requests und Lieferung der entsprechenden Response
   }
}
?>
Beispiel 2: Beispielhafte PHP Umsetzung eines Listen Service

Das oben gezeigte Beispiel zeigt die Klasse ListenService , die die vereinfachte PHP Umsetzung des Listenservice darstellt. Die native PHP SoapServer Klasse ruft diese Klasse automatisch auf, wenn ein valides XML Request eingeht. Wie soetwas aussehen kann, wird im folgenden Beispiel deutlich.

<?php
use Zeitsprung\Norm480\Partner\Soap\ListenService;

$wsdl = 'http://www.example.com/listenservice?wsdl';
$options = [];

$service = new ListenService();

$server = new SoapServer($wsdl, $options);
$server->setObject($service);
$server->handle();
Beispiel 3: Initialisierung der SoapServer Klasse mit der Klasse ListenService

Im Grunde genommen würde diese Implementierung vollkommen ausreichen. Mittels SoapServer::setObject() wird die Service Klasse als Handler festgelegt, die dann die Enumeration Requests verarbeiten würde. Schon hier würde man aber merken, dass der Server versucht eine Action Methode aufzurufen, die wir gar nicht implementieren. Auch die Webservice Definition der W3C sieht keine Implementierung einer Action Methode vor. Klingt schwer nach einem Thema für Galileo Mystery oder dem üblichen Tagesablauf eines PHP Entwicklers mit BiPRO-Services.

Einfache Umsetzung mit dem PHP Soap Server mit Decorator

Um den Service nicht direkt zugänglich zu machen, verwenden wir den oben kurz erwähnten Decorator. Man kann sich so einen Decorator wie einen Türsteher vorstellen, der den Eingang des Webservices kontrolliert und nur die Consumer hinein lässt, die wirklich exakt das liefern, was wir haben wollen. Mit PHP könnte eine Decorator Klasse wie folgt aussehen.

<?php
namespace Zeitsprung\Norm480\Soap;

use Zeitsprung\Norm480\Partner\Soap\ListenService;
use SoapFault;

class Decorator
{
   protected string $service;

   public function __construct(string $service)
   {
       $this->service = $service;
   }

   public function __call(string $method, array $arguments = [])
   {
       if (!method_exists($this->service, $method)) {
           throw new SoapFault('Server', sprintf(
               'Die Webservice Funktion "%s" existiert nicht.'
               $method
           ));
       }

       $service = new $this->service();

       $response = call_user_func_array(
           [ $service, $method ],
           $arguments
       );

       return $response;
   }
}
Beispiel 4: PHP Decorator Klasse

Um den Spannungsbogen ein wenig zu halten, ist unsere Dekorator Klasse noch nicht komplett. Die Klasse besteht aus einem Konstruktor, die den Namen der Service Klasse entgegen nimmt. Weiterhin benutzen wir die magische PHP Methode __call(), die immer dann aufgerufen wird, wenn unser Soap Server eine Webservice Funktion ausführen möchte. Die __call() Methode prüft, ob die aufgerufene Webservice Methode $method in der Service Klasse enthalten ist. Ist sie nicht enthalten, wird ein Soap Fault geworfen. PHP hat hier bei der Umsetzung der Soap Server Klasse im objektorientierten Kontext wirklich gute Arbeit geleistet. Man sieht an diesem Beispiel, wie einfach ein Soap Server hergestellt werden kann.

Der Start des Soap Servers sieht nicht viel anders aus, als das oben gezeigte Beispiel 3.

use Zeitsprung\Norm480\Partner\Soap\ListenService;
use Zeitsprung\Norm480\Soap\Decorator;

$wsdl = 'http://www.example.com/listenservice?wsdl';
$options = [];

$service = ListenService::class;
$decorator = new Decorator($service);

$server = new SoapServer($wsdl, $options);
$server->setObject($decorator);
$server->handle();
Beispiel 5: Initialisierung der SoapServer Klasse mit der Decorator Klasse

Der einzige Unterschied bei der Benutzung eines Decorators ist, dass wir die Service Klasse ListenService nicht direkt an die SoapServer Klasse übergeben, sondern in diesem Fall den initialisierten Decorator. Letztendlich verwaltet unser Decorator jetzt, welcher Request bis zum Service durch kommt und welcher nicht.

Da wir jetzt unseren Decorator implementiert haben, wird auch sofort deutlich, was das Problem mit dem W3C Enumeration Webservice und den Header Angaben ist. Bei einem jetzt durchgeführten Request würden wir einen Fehler produzieren.

Die Webservice Funktion "Action" existiert nicht.

Obwohl wir mit dem oben gezeigten XML Request gar keine Action Funktion des Webservices aufrufen, scheint der PHP Soap Server eine Action Methode ausführen zu wollen. Eine solche Funktion ist aber nirgends definiert. Logischerweise endet das immer in einem Fehler. Warum ist das so?

Die Lösung des Phänomens

Der Enumeration Webservice der W3C implementiert das Webservice Adressing der W3C. Das Action Attribut legt somit die auszuführende Funktion fest. Zwar würde der Webservice auch funktionieren, wenn wir das Action Element im Header weglassen und lediglich <wsen:Enumerate> im Body des Request XML empfangen würden. Aber dann würde der Webservice nicht mehr den BiPRO und W3C Vorgaben entsprechen.

Wie funktioniert das Action Attribut im Zusammenhang mit der auszuführenden Methode? In der WSDL Datei ist dieser Zusammenhang klarer zu sehen.

<wsdl:operation name="EnumerateOp">
<soapbind:operation soapAction="http://www.w3.org/2011/03/ws-enu/Enumerate"/>
   ...  
</wsdl:operation>
Beispiel 6: WSDL Definition der Webservice Funktion Enumerate

Hier wird klar, dass anhand des gelieferten Action Attributes die auszuführende Webservice Funktion definiert wird. Es existiert eine direkte Bindung zwischen dem Action Element und der auszuführenden Operation. Erst anhand des angegebenen Action Elements im Header ist klar, welche Funktion des Webservices eigentlich ausgeführt werden soll.

Wie bilden wir das in unserem Decorator ab? Wenn der Soap Server eine Action, MessageID und ReplyTo Funktion haben möchte, geben wir ihm diese einfach. Wir binden diese einfach als Validierungen ein. Wir erweitern also unsere PHP Decorator Klasse.

<?php
namespace Zeitsprung\Norm480\Soap;

use Zeitsprung\Norm480\Partner\Soap\ListenService;
use SoapFault;

class Decorator
{
   protected string $service;

   public function __construct(string $service)
   {
       $this->service = $service;
   }

   public function __call(string $method, array $arguments = [])
   {
       if (!method_exists($this->service, $method)) {
           throw new SoapFault('Server', sprintf(
               'Die Webservice Funktion "%s" existiert nicht.'
               $method
           ));
       }

       $service = new $this->service();

       $response = call_user_func_array(
           [ $service, $method ],
           $arguments
       );

       return $response;
   }

   /**
    * Validiert das Action Element aus dem Header
    *
    * @param string $value
    * @throws SoapFault
    * @return void
    */
   public function Action(string $value) : void
   {
       $valid = [
           'http://www.w3.org/2011/03/ws-enu/Enumerate',
       ];

       if (!in_array($value, $valid)) {
           throw new SoapFault('Server', sprintf(
               'Schema Validierungsfehler. Die aufgerufene Webservice Methode "%s" wird nicht unterstützt.',
               $value
           ));
       }
   }

   /**
    * Validiert die angegebene Message ID
    *
    * @param string $value
    * @throws SoapFault
    * @return void
    */
   public function MessageID(string $value) : void
   {
       if (empty($value) || !preg_match('/^urn:uuid:[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value)) {
           throw new SoapFault('Server', sprintf(
               'Schema Validierungsfehler. Die angegebene Message ID "%s" ist ungültig.',
               $value
           ));
       }
   }

   /**
    * Validiert die Reply To Adresse
    *
    * @param string $value
    * @throws SoapFault
    * @return void
    */
   public function ReplyTo(string $value) : void
   {
       if (!is_object($value) || !property_exists($value, 'Address') || $value->Address != 'http://www.w3.org/2005/08/addressing/anonymous') {
           throw new SoapFault('Server', sprintf(
               'Schema Validierungsfehler. Die im Header angegebene ReplyTo Adresse ist falsch.'    
           ));
       }
   }
}
Beispiel 7: Vollständige beispielhafte PHP Decorator Klasse für den Listenservice

Im oben gezeigten Beispiel wurden drei Methoden hinzugefügt, die wie die drei Elemente im Soap Header des XML Requests lauten. Diese Methoden werden komplett automatisch vom Soap Server in der Reihenfolge der Elemente aufgerufen. Zunächst die Action Methode, dann die MessageID Methode und am Ende die ReplyTo Methode. Erst wenn diese Methoden abgearbeitet wurden, führt die __call() Methode die eignetlich angefragte Funktion Enumerate aus.

Fazit

Glücklicherweise führt die PHP SoapServer Klasse die WS-Addressing Definitionen automatisch aus, sofern diese in der Webservice Defintion angegeben sind. Wir müssen uns also nicht mehr um den eigentlich Ablauf und die Reihenfolge der aufgerufenen Funktionen kümmern. Es wäre weitaus aufwendiger, wenn wir das Action Attribut erst aufwendig auswerten müssten und auf Basis der angegebenen URI erst eine Funktion des Webservices ermitteln müssten. Glücklicherweise funktioniert das automatisch. Das war für uns zunächst sehr überraschend.

Weniger gut ist aber die mangelhafte PHP Dokumentation in diesem Bereich. Bis heute war es uns nicht bekannt, dass die Soap Server Klasse derart selbstständig agieren kann. Man muss eigentlich nur wissen, dass die Header Elemente als Methoden in der verwendeten Decorator Klasse vorhanden sein müssen. Benutzt man keinen Decorator, müssen diese Funktionen zwingend in der Service Klasse vorhanden sein.  Dann werden sie nacheinander abgearbeitet und der Prozess läuft.

Lesen Sie auch