19. Oktober 2012

SOAP Webservices mit TYPO3 Flow

Dieses Tutorial gibt einen Einblick in die Erstellung von SOAP Webservices für TYPO3 Flow Anwendungen auf Basis des Flow Pakets TYPO3.Soap. Neben der Erstellung von speziellen Service-Klassen geht es außerdem um automatische Generierung von WSDL für Service Definitionen und das Testing der Services mit einem SOAP Client. SOAP Services wirken vielleicht auf den ersten Blick ein wenig veraltet, aber es ist eine bekannte und sehr verbreitete Technik. Hält man sich dabei an das WS-I Basis Profil (http://ws-i.org/Profiles/BasicProfile-1.2-2010-11-09.html) sind sie außerdem direkt vollständig kompatibel zu vielen Sprachen und Umgebungen (.NET, Java, usw.).

Voraussetzungen

Für dieses Tutorial benötigst Du eine lauffähige aktuelle Version von Flow (aktuell 1.1). Eine fertige Applikation ist allerdings nicht notwendig, denn wir werden zu Demonstrationszwecken ein einfaches Paket erstellen. In Deiner PHP Installation sollte allerdings die soap Erweiterung installiert und aktiviert sein.

Das TYPO3.Soap Paket installieren

Öffne ein Terminal (Kommandozeile) und gehe in das Root-Verzeichnis der Flow Applikation. Ein Paket kann mit dempackage:import Kommando durch folgenden Aufruf installiert werden:

./flow3 package:import TYPO3.Soap

Nach dem Import solltest Du das neue Paket sehen, wenn du den Befehl package:list aufrufst.

Damit die WSDL Generierung funktioniert (dazu später mehr) müssen noch die Sub-Routen des Pakets TYPO3.Soap unter Configuration/Routes.yaml in Deine globalen Routen eingefügt werden:

...

##
# TYPO3.Soap subroutes
#

-
  name: 'TYPO3.Soap'
  uriPattern: '<SoapSubroutes>'
  subRoutes:
    SoapSubroutes:
      package: TYPO3.Soap

##
# FLOW3 subroutes
#

-
  name: 'FLOW3'
  uriPattern: '<FLOW3Subroutes>'
  defaults:
    '@format': 'html'
  subRoutes:
    FLOW3Subroutes:
      package: TYPO3.FLOW3

Ein Beispiel-Paket erstellen

Nutze den package:create Befehl, um ein Beispiel-Paket zu erstellen:

./flow3 package:create Test.Soap

Der komplette Code des Tutorials wird in diesem Paket abgelegt. Es sollte sich innerhalb Deines Anwendungs Root Pfades befinden, in: Packages/Application/Test.Soap.

Eine einfache Service-Klasse

Zur Bereitstellung eines SOAP Webservices kann ein beliebiges Singleton-Objekt aus Flow genutzt werden. Per Default werden alle Klassen-Namen, die nach dem Namens-Schema [VendorName]\[PackageName]\Service\Soap\[ServiceName]Service benannt sind, automatisiert in einen SOAP Webservice exportiert. Erstellen wir also ein Service-Objekt und exportieren eine einfache Methode für ein SOAP-basiertes "Hello World":

Classes/Service/Soap/TestService.php

<?php
namespace Test\Soap\Service\Soap;

use TYPO3\FLOW3\Annotations as FLOW3;

/**
 * A simple SOAP test service
 *
 * @FLOW3\Scope("singleton")
 */
class TestService {

	/**
	 * Greet someone
	 *
	 * @param string $name The name to be greeted
	 * @return string A nice welcome message
	 */
	public function hello($name) {
		return 'Hello, ' . $name;
	}

}
?>

Wie man dabei sieht, ist es nicht notwendig eine spezielle Super-Klasse zu erweitern. Ein SOAP Service-Objekt ist lediglich ein einfaches Singleton innerhalb des Service\Soap Namespaces dessen Klassen-Name mit ...Serviceendet. Alle Webservice-Operationen werden durch Methoden in der Service-Klasse implementiert. Das SOAP-Paket untersucht automatisch die PHPDoc der Methoden und generiert daraus die entsprechenden Service Definitionen (WSDL). Es ist daher wichtig, dass Du darauf achtest, Kommentare mit korrekte Typ-Definitionen für Parameter und Rückgabewerte zu erstellen.

Das TestService Singleton definiert eine hello Methode die einen Parameter für den Namen entgegen nimmt und"Hello, [name]" als Rückgabewert liefert. Das ist natürlich ein kleines bisschen mehr, als das klassische "Hello World", aber es wäre auch ein wenig zu langweilig einfach nur einen statischen Wert zurück zu geben.

Du kannst Dir die WSDL des exportierten Webservices anschauen, indem Du die folgende URL im Browser öffnest:localhost. Die SOAP-Adresse für den Port des Services wird automatisch an folgende Position exportiert: localhost. Die meisten SOAP-Tools oder -Clients können aus der WSDL die korrekte Anfrage für den Aufruf der Service-Operationen erstellen.

Erhältst Du im Browser eine vergleichbare Ausgabe, wie die folgende verkürzte WSDL, sind wir so weit, dass wir den Service über einen Aufruf mit einem SOAP-Client testen können.

<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
	xmlns:tns="http://tempuri.org/service/soap/test.soap/test"
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	name="TestService"
	targetNamespace="http://tempuri.org/service/soap/test.soap/test">

	...

	<wsdl:message name="helloRequest">
		<wsdl:part name="name" type="xsd:string">
			<wsdl:documentation>The name to be greeted</wsdl:documentation>
		</wsdl:part>
	</wsdl:message>

	<wsdl:message name="helloResponse">
		<wsdl:part name="returnValue" type="xsd:string">
			<wsdl:documentation>A nice welcome message</wsdl:documentation>
		</wsdl:part>
	</wsdl:message>


	<wsdl:portType name="TestServiceSoapPort">
		<wsdl:documentation>Interface for TestService</wsdl:documentation>
		<wsdl:operation name="hello">
			<wsdl:documentation>Greet someone</wsdl:documentation>
			<wsdl:input message="tns:helloRequest" />
			<wsdl:output message="tns:helloResponse" />
		</wsdl:operation>

	</wsdl:portType>

	...

	<wsdl:service name="TestService">
		<wsdl:port binding="tns:TestServiceSoapBinding" name="TestServiceSoapPort">
			<soap:address location="http://soap-demo.dev/service/soap/test.soap/test" />
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>

Um den Service aufzurufen, kannst Du den SOAP-Client nutzen den PHP mitliefert oder ein Tool wie SoapUI, das einen etwas besseren Überblick über den SOAP Webservice liefert und zudem automatisch generierte Anfragen für Operationen mitliefert. In diesem Tutorial nutzen wir für die Webservice-Tests SoapUI. Erstelle dafür einfach ein neues Projekt in dem Programm (File -> New Project) und definiere dort die WSDL URI. Mit den Standard Einstellungen wird SoapUI für jede Operation Beispiel-Requests erstellen.

Ein neues SoapUI Projekt erstellen

Lade den Beispiel-Request durch einen Doppelklick und gebe einen Namen ein:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:test="http://tempuri.org/service/soap/test.soap/test">
	<soapenv:Header/>
	<soapenv:Body>
		<test:hello>
			<name>Christopher</name>
		</test:hello>
	</soapenv:Body>
</soapenv:Envelope>

Jetzt kann der Request über das grüne Icon ausgeführt werden. Das Feld mit der Response sollte dann das Ergebnis anzeigen:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tempuri.org/service/soap/test.soap/test">
   <SOAP-ENV:Body>
      <ns1:helloResponse>
         <returnValue>Hello, Christopher</returnValue>
      </ns1:helloResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Glückwunsch! Du hast einen funktionierenden SOAP-Webservice mit Flow.

Eine SOAP-Anfrage starten

Mit komplexere Daten arbeiten

Strings sind natürlich einfach zu nutzen, aber alle normalen Service-Klassen in Flow werden in irgendeiner Form mit Objekten arbeiten. Wir nutzen für die Nachrichten Argumente daher Data Transfer Objects (DTO):

Classes/Service/Soap/TestService.php

<?php
namespace Test\Soap\Service\Soap;

use TYPO3\FLOW3\Annotations as FLOW3;

/**
 * A simple SOAP test service
 *
 * @FLOW3\Scope("singleton")
 */
class TestService {

	...

	/**
	 * Greet a person
	 *
	 * @param \Test\Soap\Service\Soap\Dto\Person $person The personto be greeted
	 * @return string A welcome message
	 */
	public function helloPerson(\Test\Soap\Service\Soap\Dto\Person $person) {
		return 'Hello, ' . $person->getFirstname() . ' ' . $person->getLastname();
	}

}
?>

Hinweis: Erhälst Du bei dem Aufruf der helloPerson-Operation einen Fehler, solltest Du Deinen lokalen WSDL-Cache von PHP prüfen.

Bei einem Blick auf die generierte WSDL fällt auf, dass der WSDL-Generator für das Person DTO einen komplexen Typ, mit allen verfügbaren Eigenschaften, erstellt hat.

<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
	xmlns:tns="http://tempuri.org/service/soap/test.soap/test"
	xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
	xmlns:xsd="http://www.w3.org/2001/XMLSchema"
	name="TestService"
	targetNamespace="http://tempuri.org/service/soap/test.soap/test">

	...

	<wsdl:types>
		<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://tempuri.org/service/soap/test.soap/test">
			<xsd:complexType name="Person">
				<xsd:sequence>
					<xsd:element name="firstname" type="xsd:string" minOccurs="0" maxOccurs="1" >
					</xsd:element>
					<xsd:element name="lastname" type="xsd:string" minOccurs="0" maxOccurs="1" >
					</xsd:element>
					<xsd:element name="salutation" type="xsd:string" minOccurs="0" maxOccurs="1" >
					</xsd:element>
				</xsd:sequence>
			</xsd:complexType>
		</xsd:schema>
	</wsdl:types>

	...
</wsdl:definitions>

Der Abschnitt wsdl:types enthält ein Schema (Du kannst Deine eigene targetNamespace URI definieren) mit komplexen Typ-Definitionen für die generierten komplexen Typen.

SOAP-Anfrage mit komplexen Daten

Das Domain-Model anbinden

In den meisten Fällen wirst Du bereits ein Domain-Model mit Entities haben und möchtest über einen Webservice bestimmte Operationen ansprechen. Um dein Domain-Model von dem Webservice getrennt zu halten (um spätere Anpassungen zu ermöglichen) ist es sinnvoll auf Deinen Models mit Data Transfer Objects und einem seperaten Domain Service für die Operationen zu arbeiten.

Damit dieses Tutorial nicht ausufert, schauen wir uns eine einfache Service-Klasse an, die direkt auf einem Entity arbeitet und eine Repository-Abhängigkeit hat. Wir starten mit dem Erstellen einer einfachen Entity Book mit einem Repository:

./flow3 kickstart:model Test.Soap Book title:string isbn:string description:string
./flow3 kickstart:repository Test.Soap Book

Wir fügen der Book-Entity ein paar Annotationen hinzu, um Validierungs-Eigenschaften für den Titel und die ISBN-Nummer zu definieren. Die Annotation @Identity markiert die Eigenschaft isbn als eindeutig und ermöglicht so einen guten Entity-Identifier für Service-Operationen (in den meisten Fällen möchte man die Interne UUID nicht ansprechen). Anschließend sollte eine solche Klasse zur Verfügung stehen (getters und setters ausgelassen):

<?php
namespace Test\Soap\Domain\Model;

/*                                                                        *
 * This script belongs to the FLOW3 package "Test.Soap".                  *
 *                                                                        *
 *                                                                        */

use TYPO3\FLOW3\Annotations as FLOW3;
use Doctrine\ORM\Mapping as ORM;

/**
 * A Book
 *
 * @FLOW3\Entity
 */
class Book {

	/**
	 * The title
	 * @var string
	 * @FLOW3\Validate(type="NotEmpty")
	 */
	protected $title;

	/**
	 * The isbn
	 * @var string
	 * @FLOW3\Identity
	 * @FLOW3\Validate(type="NotEmpty")
	 */
	protected $isbn;

	/**
	 * The description
	 * @var string
	 * @ORM\Column(type="text")
	 */
	protected $description;

	...

}
?>

Vergiss nicht ./flow3 doctrine:update aufzurufen, um das Datenbank-Schema zu aktualisieren (und konfiguriere Deine Datenbank korrekt in der Settings.yaml).

Ein einfacher Service, um die Bücher über einen SOAP Webservice zu verwalten, sollte das Anlegen und Anzeigen von Büchern unterstützen, wodurch auch die wichtigsten SOAP-Features gezeigt werden. Wir erstellen also zunächst ein Buch, denn unsere Buch-Tabelle sollte momentan noch leer sein:

<?php
namespace Test\Soap\Service\Soap;

use TYPO3\FLOW3\Annotations as FLOW3;

/**
 * A SOAP service for books
 *
 * @FLOW3\Scope("singleton")
 */
class BookService {

	/**
	 * @var \Test\Soap\Domain\Repository\BookRepository
	 * @FLOW3\Inject
	 */
	protected $bookRepository;

	/**
	 * @var \TYPO3\FLOW3\Persistence\PersistenceManagerInterface
	 * @FLOW3\Inject
	 */
	protected $persistenceManager;

	/**
	 * Create a book
	 *
	 * @param \Test\Soap\Domain\Model\Book $book The new book
	 * @return boolean TRUE if the book was created successfully
	 */
	public function create(\Test\Soap\Domain\Model\Book $book) {
		$this->bookRepository->add($book);
		$this->persistenceManager->persistAll();
		return TRUE;
	}

	...

?>

Wie schon zuvor nutzen wir einfach den PHP-Typ der create-Methode und das SOAP-Paket wird sich um das Mapping auf eine frische Book-Instanz kümmern. Im Vergleich zu einem Controller, müssen wir uns selber um den Aufruf vonpersistAll auf dem PersistenceManager kümmern. Das Gute daran ist, dass wir jegliche Exception hier selber anfangen und in ein übliches Webservice-Ergebnis konvertieren können.

Wenn Du nun den neuen Service in SoapUI oder einen anderen Client einfügst und die create-Operation testest, solltest Du ein Buch in der Datenbank vorfinden. Einfach, oder?

19. Oktober 2012
Christopher Hlubek

Christopher Hlubek