Symfony2 SOAP support
Where RESTful API’s are the standard in modern day web applications, SOAP (Simple Object Access Protocol) used to be a very popular protocol for RPC calls, and in some circles it might still be popular: SOAP is the form of RPC that Microsoft supports (best) in their .net framework. It consists of a WSDL file and a HTTP POST request with a “SOAPAction” header set, when invoking a remote procedure. It can be found in “$_SERVER['HTTP_SOAPACTION']” in vanilla PHP and in “$this->getRequest()->server->get(‘HTTP_SOAPACTION’)” in a Symfony2 controller.
PHP had no support for SOAP for a long time and since this was not an easy protocol to implement, this effectively meant you couldn’t use SOAP in PHP based applications. Symfony1 used to have SOAP support and in Symfony2 SOAP support can be added by installing a bundle. This bundle is called “BeSimpleSoapBundle” and is based on some BeSimple and the Zend Framework SOAP classes. It has a really nice WSDL generator and it works very good (we tried it). Installing the BeSimpleSoapBundle is not hard (and is well documented), but the project hasn’t been updated in the last two years. So if you want to play it safe you might want to refrain from using it.
You may want to choose the new (PHP >5.2) SOAP Server implementation as suggested by the official Symfony2 documentation. It consists (like BeSimpleSoapBundle) of two parts: a client and a server. The server takes care of decoding the WSDL file, parsing this incoming XML and calling a method in a said object. The client takes care of parsing the WSDL file and being able to call the methods with the right parameters. The only thing you are missing when not using BeSimpleSoapBundle is a WSDL generator. Fortunately there are many WSDL generators online.
But the problem persists that every time you run your application on a different machine you have to regenerate the WSDL file. Changing this hostname when moving from one development VM to another was to cumbersome for us so we came up with another solution: let PHP (the Symfony2 project) generate the WSDL file and fill in the “base” URL wherever nessecary. We altered the book example by adding a conditonal statement, so that if the SOAPAction header is not present it just serves the WSDL file. In the controller we also added a client implementation for debugging purposes.
We used the WSDL generator by nbsoftware to generate the WSDL file and used “webservice service(“http://localhost/soap/server”)” as the webservice definition. The resulting WDSL file was put into the “Resources/views” directory of the Bundle as a twig template called “wsdl.xml.twig” and we replaced “###SERVER_ADDRESS###service” with “http://localhost/soap/server” and all the occurences of with “http://localhost” with “{{ base_uri }}”. We serve the file conditionally by using the “$this->render()” call with the template identifier and the “base_uri” as arguments.
When you want to call another controller from that controller you can instantiate the controller by using the “new” keyword and after that call the “setContainer” method with “$this->container” as argument. After that you can set the newly created controller as the object that handles the SOAP calls. If you create a public method with a name that is also present in the WSDL file you can call that method using the SOAP client.
This method makes offering SOAP functionality really easy and with a little effort you can offer your RESTful calls via SOAP to .net applications as well. Below you find the full controller code that we use:
class SoapController extends Controller
{
/**
* Function to retrieve current applications base URI for WSDL
*/
private function getBaseUri()
{ // get the router context to retrieve URI information
$context = $this->get('router')->getContext();
// return scheme, host and base URL
return $context->getScheme().'://'.$context->getHost().$context->getBaseUrl();
}
/**
* Serves WSDL on GET and handles SOAP calls when given
*/
public function serverAction()
{
// init data format
$this->getRequest()->attributes->set('_format', 'xml');
// retrieve base URI for WSDL file location
$base_uri = $this->getBaseUri();
$request = $this->getRequest();
// if this is not a SOAP request, serve the WSDL file
if (!$request->server->has('HTTP_SOAPACTION'))
{
return $this->render('LswSoapBundle:Soap:wsdl.xml.twig', compact('base_uri'));
}
// load the mapping from the config file
$mapping = $this->container->getParameter('soap.mapping');
// get rid of the extra quotes around the soap action name
$soapAction = array_pop(explode('/',trim($request->server->get('HTTP_SOAPACTION'),'"')));
// intiantiate the (controller) class that is mapped
$object = new $mapping[$soapAction]();
// make sure the controller has access to the container
$object->setContainer($this->container);
// initialize the SOAP server
$server = new \SoapServer($base_uri.'/soap/server.xml');
// point to the controller that has a method with the same name as the soap action
$server->setObject($object);
try
{ // try to execute the method
$data = $server->handle();
}
catch (\Exception $e)
{ // catch any exception and make a proper SOAP error response
$code = $e->getCode();
$message = $e->getMessage();
return $this->render('LswSoapBundle:Soap:error.xml.twig', compact('code', 'message'));
}
// on success render the response
return compact('data');
}
/**
* Client for WSDL calls, for debugging only
*/
public function clientAction()
{
$base_uri = $this->getBaseUri();
try {
$client = new \SoapClient($base_uri.'/soap/server.xml', array('trace' => 1));
$a = array('message'=>'hello world!');
$response = $client->__soapCall('helloWorld', array($a));
var_dump($response);
die();
} catch (\SoapFault $e) {
var_dump($e->getMessage(), $client->__getLastResponse()); die();
}
}
}
NB: One of the ceveats is that the wsdl file will be cached (by PHP) and that you may want to run “rm /tmp/wsdl-*” before debugging a nasty problem. This could save you a lot of time.
