SOAP Server and Test Client with Zend_Soap, Symfony2, Doctrine2 and PostgreSQL

This is a story about the short project where interesting set of tools is used and many problems solved, so I wanted to share it. It is a SOAP server where each service can be tested from simple test client web interface.

I always try to use the best tool for the job, and for this project my weapons of choice were:

SOAP Server

For SOAP server I have created separated bundle, one controller with two actions.

First action handles all SOAP requests, set SOAP server URL to /soap_api/wsdl route and sets soap_api (defined in config and taken from dependency injection container) service as class to handle SOAP requests. Then assign response and renders as XML in the view.

$soap = new \Zend_Soap_Server($this->get('request')->getUriForPath('/soap_api/wsdl'));
$soap->setObject($this->get('soap_api'));
$soap->setReturnResponse(true);

return $this->render('SoapBundle:Default:index.xml.twig', array('response' => $soap->handle()));

Similar to this, in second action we dynamically generate WSDL definition for our SOAP server with Zend_Soap_AutoDiscover. We use Zend_Soap_Wsdl_Strategy_ArrayOfTypeComplex mode so we can later define array of complex type response in our SOAP class with simple phpdoc trick: @return OurCustomResponse[].

$autodiscover = new \Zend_Soap_AutoDiscover('Zend_Soap_Wsdl_Strategy_ArrayOfTypeComplex');
$autodiscover->setUri($this->get('request')->getUriForPath('/soap_api'));
$autodiscover->setClass(self::SOAP_SERVER_CLASS);
ob_start();
$autodiscover->handle();
return $this->render('SoapBundle:Default:wsdl.xml.twig', array('wsdl' => ob_get_clean()));

Zends auto-discovery saves you a lot of time by generating wsdl for you. You just need to write good doc-blocks in attached class and everything will work out perfect. Also take care to disable wsdl_cache on your development server, so when you change doc-blocks it regenerates the wsdl. That is pretty much it on the server, we attached class which will implement our SOAP interface and just take care that this actions are rendered in XML format (_format: xml in routing.yml).

Test Client

My plan was to dynamically create page with link to each service function using reflection and then for each function generate test page with dynamically generated form which reflects function interface. Let’s take a look at the code.

$server = \Zend_Server_Reflection::reflectClass(self::SOAP_SERVER_CLASS);
foreach ($server->getMethods() as $method) {
	if ($function != $method->getName()) {
		continue;
	}
	foreach ($method->getParameters() as $parameter) {
		$form->add(
			new \Symfony\Component\Form\TextField(
				$parameter->name,
				array(
					'required' => !$parameter->isOptional()
				)
			)
		);
	}
	break;
}

So, I used Symfony form component and generated form dynamically according to given function from our service class. Then, after the submit, collect parameters and just call given function.

$response = $this->getSoapClient()->__call($function, $arguments);

Where getSoapClient just instantiates Zend_Soap_Client. Then I displayed response on the result page as well as last request and response from client (can be easily retrieved from SOAP client) and catch eventual SoapFaults.

For debugging purposes I have added simple checkbox to each form which can disable SOAP and send plain PHP calls.

if (isset($_POST['disable_soap_server']) && $_POST['disable_soap_server']) {
    $response = call_user_func_array(array($this->get('soap_api'), $function), $arguments);
} else {
    $response = $this->getSoapClient()->__call($function, $arguments);
}

That was enough to have test client interface working. Now when I alter my service definition I don’t need to touch my test client code, it will always reflect the service.

Doctrine2 and PostgreSQL

This was probably the most tricky part. It turned out that Doctrine2 is not playing very nice with PostgreSQL.

I had PostgreSQL schema and wanted to generate Doctrine entities from database to get started. When I run import, an error occurred.

console doctrine:mapping:import  SoapBundle annotation
[Doctrine\DBAL\DBALException]
Unknown database type  requested, Doctrine\DBAL\Platforms\PostgreSqlPlatform may not support it.

After looking at Doctrines source code and googling around, I find out that other people have problems with some PostgreSQL data types (like cidr and inet) too.  I patched the source and generated entities. But when I tried to run some CRUD, I got:

SQLSTATE[3F000]: Invalid schema name: 7 ERROR: schema "main" does not exist

Since my schema name is “Main” I realized that I’m facing the case sensitivity problem. I was despaired and tried everything that came to me. At the end of the day, I have posted it on stackoverflow.

Luckily, mstrzele answered and thanks to his PgSqlBundle it finally worked. Well, after a few patches. :) I just enabled PgSqlBundle, set autoloader and configured Doctrine to use driver from PgSqlBundle.

One thing you should take care of there if you work with Symfony2 is to always test your app in production mode before release. It turned out that in production mode all entities are cached with proxy objects in cache folder, no matter you use them in your code or not. I had some invalid entities which caused app to fail in production mode only.

Finish it

After I figured it all out, it was easy to implement SOAP interface and use Doctrine entities and repositories to implement the CRUD. It was important to convert all exceptions to SoapFault at the top and document all error codes and messages, so they can be easily caught by the client.

That’s it. Have any tip to share? Feel free to post comments. I’m sure there is a space for improvements. ;)

  • Michael Tacelosky

    Thanks for writing this up. Would you consider sharing the completed package with some dummy data on github? I’m new to Symfony, PostgreSQL and Doctine and am looking at porting a CodeIgniter+MySQL project to the exact configuration you describe, so any working example would be most appreciated.

    • Anonymous

      Well, both Symfony2 and Doctrine2 evolved last few months, so most of it wouldn’t work out of the box. Configuration is changed, annotations, almost everything (https://github.com/symfony/symfony/blob/master/UPDATE.md), so my concrete example would bring more problems then it solves.

      If you have existing PosgreSql database, I suggest you to download latest Symfony2 (beta5) and try to reverse engineer it. I’m also not sure PgSqlBundle is upgraded to work with latest Symfony2 and Doctrine2, but you can fork it and work it out with mstrzel. If you face any problems, I’m always there to help.

      Thanks for reading.

  • http://www.facebook.com/dan.klasson Daniel Klasson

    You might want to check out: http://besim.pl/SoapBundle/

    • Anonymous

      Wow, very cool, thanks.

  • Golifter Golifter

    I have a problem with following line:
    $autodiscover->setClass(self::SOAP_SERVER_CLASS);

    What is the proper value of that passing const ?

    I have tried with ‘GSoapBundleServicesHelloService’ and also with ‘HelloService’ but I’m receiving something like
    “WSDLSOAP-ERROR: Parsing WSDL: Couldn’t load from ‘http://g.localhost:8080/app_dev.php/soap_api/wsdl‘ : failed to load external entity “http://g.localhost:8080/app_dev.php/soap_api/wsdl”

    • umpirsky

      Try with ‘G\SoapBundle\Services\HelloService’.