In this tutorial I'll show you how to create SOAP webservice for your FLOW3 application using the TYPO3.Soap package. It covers the creation of special service classes, the automatic generation of WSDL for service description and how to test the service with a SOAP client.

SOAP services might look a little bit antiquated but it's a well known and common technique. And if you stick to a WS-I basic profile (http://ws-i.org/Profiles/BasicProfile-1.2-2010-11-09.html) it's also very interoperable out of the box.

Requirements

You should have a running, recent FLOW3 installation (1.1) for this tutorial. You do not need an existing application to follow the tutorial, since we will create a simple package for demonstration. You PHP installation should have the soap extension installed and activated.

Installing the TYPO3.Soap package

Open a terminal and go to the root directory of your FLOW3 application. A package can be installed with the package:import command by running:

./flow3 package:import TYPO3.Soap

After the import you should see the new package if you run the package:list command.

For the WSDL generation to work (more about that later), you have to add the subroutes of the TYPO3.Soap package to your global routes in Configuration/Routes.yaml:

...

##
# 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

Create a demo package

Use the package:create command to create a demo package:

./flow3 package:create Test.Soap

All code created in this tutorial will go into this package. It should be located under your application root path in Packages/Application/Test.Soap.

A simple service class

Any singleton object in FLOW3 can be used to provide a SOAP webservice. As a default all classes following the naming schema [VendorName]\[PackageName]\Service\Soap\[ServiceName]Service will be exported automatically as SOAP webservices. So let's create a service object and export a simple method for a SOAP driven "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;
	}

}
?>

As you can see we don't need to extend any special superclass. A SOAP service object is just a simple singleton under the Service\Soap namespace with a class name ending in ...Service. All webservice operations are implemented by methods in the service. The SOAP package automatically inspects the PHPDoc of the methods to generate the correct service definition (WSDL). So make sure to write comments with correct type definitions for parameters and return types.

The TestService singleton defines a hello method which takes a name parameter and returns "Hello, [name]" as the return value. A little bit more than the classic Hello World, but a static return value would be somehow boring.

You can look at the WSDL of the exported webservice by opening http://localhost/service/soap/test.soap/test.wsdl in your browser. The SOAP address for the port of the service is automatically exported at http://localhost/service/soap/test.soap/test. Most SOAP tools or clients can take the WSDL and create the correct request to call the operations of a service.

If you get something like the following, shortened WSDL in your browser we are ready to test the service by calling it with a SOAP client.

<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>

You can use the SoapClient that comes with PHP to call the service or use a tool like SoapUI that gives a better overview over a SOAP webservice and can auto-generate requests for the operations. For this tutorial we will use SoapUI to test the webservice. Create a new project in SoapUI (File -> New Project) and specify the WSDL URI. With the default settings, SoapUI will generate sample requests for every operation.

Create new project in SoapUI

Load the sample request by double clicking it and enter a name in the request:

<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>

Run the request with the green arrow icon. The response pane should show the webservice result:

<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>

Gratulations! You wrote a working SOAP webservice.

Execute SOAP request in SoapUI

Dealing with complex data

Of course strings are easy to use but any decent service class in FLOW3 will somehow deal with objects. So let's use data transfer objects (DTO) for the message arguments: 

Classes/Service/Soap/Dto/Person.php

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

use TYPO3\FLOW3\Annotations as FLOW3;

/**
 * A simple person DTO
 */
class Person {

	/**
	 * @var string
	 */
	protected $salutation;

	/**
	 * @var string
	 */
	protected $firstname;

	/**
	 * @var string
	 */
	protected $lastname;

	/**
	 * @param string $firstname
	 */
	public function setFirstname($firstname) {
		$this->firstname = $firstname;
	}

	/**
	 * @return string
	 */
	public function getFirstname() {
		return $this->firstname;
	}

	/**
	 * @param string $lastname
	 */
	public function setLastname($lastname) {
		$this->lastname = $lastname;
	}

	/**
	 * @return string
	 */
	public function getLastname() {
		return $this->lastname;
	}

	/**
	 * @param string $salutation
	 */
	public function setSalutation($salutation) {
		$this->salutation = $salutation;
	}

	/**
	 * @return string
	 */
	public function getSalutation() {
		return $this->salutation;
	}

}
?>

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();
	}

}
?>

Note: If you should get any error calling the helloPerson operation, you should check your local WSDL cache of PHP. The SOAP server caches the WSDL per default and rejects unknown operations or undefined types.

If you have a look at the generated WSDL, you will notice that the WSDL generator created a complex type for the Person DTO with all gettable properties:

<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>

The section wsdl:types contains a schema (you can declare your own targetNamespace URI) with complex type definitions for the generated complex types.

Connecting your domain model

Most of the time you already have a domain model with entities and want to expose certain operations through a web service. To separate your domain model from the web service (for future changes) it's a good idea to use data transfer objects on top of your models and a separate domain service for the operations.

To keep this tutorial focussed, we'll have a look at a simple service class which directly operates on an entity and has a repository as dependency. We start by kickstarting a simple entity Book with a repository:

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

Let's add some annotations to the Book entity for setting validation options on the title and ISBN number. The Identity annotation will mark the isbn property as unique and allows for a great entity identifier for service operations (we don't want to expose the internal UUID here in most cases). After that we should have a class like this (leaving out the getters and setters):

<?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;

	...

}
?>


To update your database schema don't forget to run ./flow3 doctrine:update (and configure your database properly in the Settings.yaml).

A simple service for managing the books through a SOAP webservice should offer creation and listing of books to show the relevant SOAP features. Let's start by creating a book since our books table should be empty right now:

<?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;
	}

	...

?>

Like before we just use the PHP type in the create method and the SOAP package will take care of mapping to a _fresh_ Book instance. In order to persist the book, we add it to the BookRepository. In contrast to a controller we have to take care of the persistAll call on the PersistenceManager. The good thing is, that we can catch any exception here and convert it to a custom result of the webservice.

If you add the new service to SoapUI or another client and test the create operation you should have a book in the database table. Simple, isn't it?