SOAP WebService in Symfony

November 16th, 2008 by Carlos Barros

One interesting topic on web development is webservice development. There are several techniques to implement a webservice out there, and today I’ll talk about one technique that I worked in the recent past that I really like: SOAP. As per wikipedia:

SOAP, originally defined as Simple Object Access Protocol, is a protocol specification for exchanging structured information in the implementation of Web Services in computer networks. It relies on Extensible Markup Language (XML) as its message format and usually relies on other Application Layer protocols, most notably Remote Procedure Call (RPC) and HTTP for message negotiation and transmission. SOAP forms the foundation layer of the web services protocol stack providing a basic messaging framework upon which abstract layers can be built.

The plan for this tutorial is to build a complete set of webservice methods to interact with the citypicker, built in a previous post. For this, I’ll use a great symfony plugin called ckWebService. This plugin enables the developer to expose your actions as a SOAP webservices. Another great functionality is the built-in WSDL generator, that parses module’s doc comment in order to identify which actions should be exposed and it’s input/output parameters.

Let’s start by installing ckWebService plugin in our symfony project. I’ll not install the latest release, instead I’ll checkout from trunk svn, as it contains some nice improvements if compared to latest release:

barrosws@barros.ws [~/symfony/blog]# cd plugins/
barrosws@barros.ws [~/symfony/blog/plugins]# svn co http://svn.symfony-project.com/plugins/ckWebServicePlugin/trunk ckWebServicePlugin

OBSERVATION: Current trunk version has a small bug (actually a wrong variable name) that must be fixed before continuing:

  public function getResultProperty()
  {
    - return $this->resultMember;
    + return $this->resultProperty;
  }

Now we need to configure the plugin in order to make it work. The read-me located at plugin page provides a complete guide to configure it. For this project we use a basic configuration:

apps/frontend/config/app.yml:

soap:
  enable_soap_parameter: on
  ck_web_service_plugin:
    wsdl: soap.wsdl 
    handler: ckSoapHandler
    persist: %SOAP_PERSISTENCE_SESSION%
    render: off
    result_callback: getSoapResult
    soap_options:
      encoding: utf-8
      soap_version: %SOAP_1_2%

apps/frontend/config/filters.yml:

soap_parameter:
  class: ckSoapParameterFilter
  param:
    condition: %APP_ENABLE_SOAP_PARAMETER%

apps/frondend/config/factories.yml:

soap:
  controller:
    class: ckWebServiceController

Done! That’s all we need to start exposing actions as SOAP webservices. For now on we can expose any of our previously created action by adding a special tag to the doc comment, like this:

  /**
  * Action description
  * @ws-enable
  *
  * @param string $name
  * @return boolean
  */
  public function executeSomeAction($request)
  {
  	/* action here */
  }

This doc comment will expose the action and instruct the WSDL generator that this action expects a string input parameter, called $name and that it will return a boolean value. An interesting thing about this plugin is that it will place all input parameters in the $request object, so the action can access it as if it was called from a browser, passing name as a query string or a post value:

    ...
    $name = $request->getParameter('name');
    ...

Also, notice that $request parameter was removed from the doc comment. This is necessary because if we keep it, the WSDL generator will add $request as a parameter to the webservice, what is not the case here.

Let’s start the implementation for this project. We have three actions that will be exposed:

  • executeIndex: to list users;
  • executeEdit: to insert/edit users;
  • executeDel: to delete users.

One might think that we will need to add @ws-enable to these actions doc comment… well, yes, that’s the original idea, but I prefer using a different approach. My approach is to create a new module, called soap (or whatever you want) and create wrappers to actual actions. This will reduce the number changes needed to be done in the actual actions (sometimes it won’t require any change at all) and will make it possible for the developer to code the entire system without even caring about webservice, all adjustments can be easily made only when actually implementing the webservice. In some emails I exchanged with Christian Kerl, the plugin author, he said me that this is not the best way to achieve this result. The correct way to do this is to create a custom SoapHandler, but this will kill WSDL generator (he said work is in progress), so I’ll stick to my way by now (trunk version has all the necessary changes to make this possible – it’s not the case with latest release).

So, let’s create our new module:

barrosws@barros.ws [~/symfony/blog]# symfony generate:module frontend soap
>> dir+      /home/barrosws/symfony/blog/apps/frontend/modules/soap/templates
>> file+     /home/barrosws/symfony/blog/app...soap/templates/indexSuccess.php
>> dir+      /home/barrosws/symfony/blog/apps/frontend/modules/soap/actions
>> file+     /home/barrosws/symfony/blog/app.../soap/actions/actions.class.php
>> file+     /home/barrosws/symfony/blog/tes...al/frontend/soapActionsTest.php
>> tokens    /home/barrosws/symfony/blog/tes...al/frontend/soapActionsTest.php
>> tokens    /home/barrosws/symfony/blog/app...soap/templates/indexSuccess.php
>> tokens    /home/barrosws/symfony/blog/app.../soap/actions/actions.class.php

The first action will expose is executeIndex, that will return a list of all users registered in the system. This is the simplest one and I’ll use to explain some important points:

apps/frontend/modules/soap/actions/actions.class.php:

  /**
  * Get users
  *
  * @ws-enable
  * 
  * @return SoapUser[]
  */
  public function executeGetUsers($request)
  {
  	// call actual action
  	$this->getController()->forward('citypicker','index');
 
  	// set result
  	$actionInstance = $this->getLastActionInstance();
  	$actionInstance->result = $actionInstance->users;
  }

As I said before, we will create wrappers to actual actions. For this action, we don’t have any input parameter, so we don’t need any extra processing. First thing the action does is a forward to actual action. Note that I use the forward method from the controller instead of forward method from sfAction. This is necessary because we need continue our execution flow AFTER actual action returns (sfAction’s forward won’t return control to us). Return value is expected to be located in the deepest action instance, in our case, citypicker/index action, in a property called result (in our case, we store the result of a UserPeer::doSelect() call – made in citypicker/index action and stored in users property). In order to do this we need to get this action’s instance and that’s what getLastActionInstance method do:

apps/frontend/modules/soap/actions/actions.class.php:

  /**
   * Get last action instance
   * 
   * @return sfActionInstance
   */  
  private function getLastActionInstance()
  {
  	return $this->getController()->getActionStack()->getLastEntry()->getActionInstance();
  }

This method will simply return last actions instance from the action stack, and we will use it in all of our wrappers. If you look at doc comments, you will notice return value is declared as an array of SoapUser objects. SoapUser class is defined as follows:

lib/soap/SoapUser.class.php

<?php
class SoapUser
{
	/**
	 * User name
	 * 
	 * @var string
	 */
	public $name;
 
	/**
	 * Country ID
	 * 
	 * @var integer
	 */
	public $country_id;
 
	/**
	 * State ID
	 * 
	 * @var integer
	 */
	public $state_id;
 
	/**
	 * City ID
	 * 
	 * @var integer
	 */
	public $city_id;
}
?>

Doc comments are REQUIRED here too, because WSDL generator will use it to build the object definition. When sending result back, our result (array of User objects) will be converted into SoapUser objects, making these properties available.

Our first method is complete. In order to start using it, we need to generate the WSDL definition, using the built-in WSDL generator. The generator will also create the frontend dispatcher, in web/ directory:

barrosws@barros.ws [~/symfony/blog]# symfony webservice:generate-wsdl frontend soap http://blog.barros.ws/symfony
>> file-     /home/barrosws/symfony/blog/web/soap.php
>> file+     /home/barrosws/symfony/blog/web/soap.php
>> tokens    /home/barrosws/symfony/blog/web/soap.php
>> file+     /home/barrosws/symfony/blog/web/soap.wsdl

In order to test it we can use a nice piece of software called SoapUI. This software will read soap.wsdl and build the request, all using a nice GUI. I recommend downloading the trial of PRO version, as it is capable of generating forms (web like) where you can input parameters:

executeDel actions is similar to executeIndex:

apps/frontend/modules/soap/actions/actions.class.php:

  /**
  * Deletes an user
  *
  * @ws-enable
  * @param integer $id
  * 
  * @return boolean
  */
  public function executeDelUser($request)
  {
  	// call actual action
  	$this->getController()->forward('citypicker','del');
 
  	// set result
  	$actionInstance = $this->getLastActionInstance();
  	$actionInstance->result = true;
  }

Now, executeEdit (executeNewUser in our wrapper) is a bit trickier:

apps/frontend/modules/soap/actions/actions.class.php:

  /**
  * Creates a new user in the system
  *
  * @ws-enable
  * @param SoapUser $user
  * 
  * @return boolean
  */
  public function executeNewUser($request)
  {
  	// convert input param from OBJECT to ARRAY
  	$request->setParameter('user',get_object_vars($request->getParameter('user')));
 
  	// call actual action
  	$this->getController()->forward('citypicker','edit');
 
  	// check errors
  	$actionInstance = $this->getLastActionInstance();
  	if(!$actionInstance->form->isValid()) $this->throwSoapFormException($actionInstance->form);
 
  	$actionInstance->result = true;
  }

First difference we can note is the fact this action requires one input parameters. In doc comment we declare that this action expects an SoapUser object as input, but the actual action expects an simple array. The first step then is to convert received object into an array. For this we use get_object_vars and after conversion, we set it back to the $request object. Finally we call actual action, that will act as if the user had submitted the form. Next difference is that we need to check if there was any error processing input data. We do this by checking if form, in actual action instance, is valid, and if not return an error message. In order to throw an exception with detailed errors, I created an small method called throwSoapFormException, that will iterate through all errors in the form and build single string, with one error per line:

apps/frontend/modules/soap/actions/actions.class.php:

  /**
   * Throw a SoapFault error based on form errors
   * 
   * @param sfForm $form
   */
  public function throwSoapFormException($form)
  {
	foreach($form->getFormFieldSchema()->getError() as $e)
		$errors[] = $e;
 
	throw new SoapFault('ERROR',implode("n",$errors));
  }

And that’s it, we can now create new users using the new SOAP interface:

Well, actually one small thing is missing to make it really work… Did u notice that I didn’t touch actual actions yet? Sometimes we don’t need to touch it, but that’s not our case. If you look at citypicker post you will notice that both “del” and “edit” actions redirect the user back to index page on success. We can’t do this when running on soap mode, or we will lose control and we won’t be able to send correct result back to the client. To fix this, we just need to make an small change:

if(!$this->isSoapRequest()) return $this->redirect('citypicker/index');

isSoapRequest is a new method added by ckWebservicePlugin and it will return true when executing the actions via SOAP. Adding this check we just perform the redirect when NOT in SOAP mode.

That’s all we need to talk about how to expose your actions via SOAP, but in order to complete our example, we need to create some methods to fetch countries/states/cities informations. For this we create 6 new actions:

  /**
  * Get countries list
  * @ws-enable
  *
  * @return SoapGeo[]
  */
  public function executeGetCountries($request)
  {
  	$this->result = CountryPeer::doSelect(new Criteria());
  }
 
  /**
  * Get a country
  *
  * @ws-enable
  * @param integer $id
  * 
  * @return SoapGeo
  */
  public function executeGetCountry($request)
  {
  	$this->result = CountryPeer::retrieveByPK($request->getParameter('id'));
  }
 
  /**
  * Get states list
  *
  * @ws-enable
  * @param integer $country_id
  *
  * @return SoapGeo[]
  */
  public function executeGetStates($request)
  {
  	$country = CountryPeer::retrieveByPK($request->getParameter('country_id'));
  	if(!$country) throw new SoapFault('ERROR','Invalid country');
 
  	$this->result = $country->getStates();
  }
 
  /**
  * Get a state
  *
  * @ws-enable
  * @param integer $id
  * 
  * @return SoapGeo
  */
  public function executeGetState($request)
  {
  	$this->result = StatePeer::retrieveByPK($request->getParameter('id'));
  }
 
  /**
  * Get cities list
  *
  * @ws-enable
  * @param integer $state_id
  *
  * @return SoapGeo[]
  */
  public function executeGetCities($request)
  {
  	$state = StatePeer::retrieveByPK($request->getParameter('state_id'));
  	if(!$state) throw new SoapFault('ERROR','Invalid state');
 
  	$this->result = $state->getCitys();
  }
 
  /**
  * Get a city
  *
  * @ws-enable
  * @param integer $id
  * 
  * @return SoapGeo
  */
  public function executeGetCity($request)
  {
  	$this->result = CityPeer::retrieveByPK($request->getParameter('id'));
  }

And to finish, we need to create the SoapGeo class, that will store country name and id:

lib/soap/SoapGeo.class.php:

<?php
class SoapGeo
{
    /**
     * ID
     *
     * @var integer
     */
    public $id;
 
    /**
     * Name
     *
     * @var string
     */
    public $name;
}

And we’re done. With this we can now build an external app to create/edit/delete users in the database. I spent several days working with the plugin before coming up with this solution and I hope this will save other developers some time dealing with SOAP implementations. Also, I’d like to thank Christian Kerl for all new changes made to trunk version that is making this plugin even better.

If one wants to test our SOAP interface, the .wsdl file is located at http://blog.barros.ws/symfony/soap.wsdl

Share this post...
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Ma.gnolia
  • MySpace
  • Rec6
  • Reddit
  • StumbleUpon
  • Technorati

Comments

      saganxisNo Gravatar in
    • Wow this is a great tutorial. It ‘d be great if it can be posted in the symfony’s snippets site just to be sure we can search for it when needed

      omabilNo Gravatar in
    • Hi Everybody,
      I discovered a strange issue:
      inorder to get your correct complexTypes in the wsdl, the class fields PhpDoc must be in the format:

      /**
      *
      * @var string
      */
      public $field1;

      so if your doc is only:

      /** @var string */
      public $field2;

      field2 will not be inserted in the complexType!!

      Thank you Carlos, this post is really helpful for the work I’m doing.

      Carlos BarrosNo Gravatar in
    • Hi omabil.

      Nice observation, probably some issue on PhpDoc parser integrated into ckWebservicePlugin.

      I’m happy to see this tuto was helpful for you and for other guys :)

      Thanks

      Christian KerlNo Gravatar in
    • Hi,

      great tutorial Carlos!

      The bugs mentioned in the article and the issue with doc block parsing reported by omabil here in the comments have been fixed in version 2.2.1!

      Carlos BarrosNo Gravatar in
    • Hi Christian, thanks..

      Sorry for not reporting that bug directly to you, I was supposed to do this but I’m having a crazy week and forgot to do it.

      Carlos BarrosNo Gravatar in
    • HI Williams… Unfortunately I don’t have this document in spanish, and that’s my only example….. Actually this is not really an example, but a snippet of a real life project…..

      Carlos

      MykeNo Gravatar in
    • I’d love to see some more detailed functional tests.. I’m trying to get one built off of the README but it’s not 100% clear and I keep getting exceptions when executing requests.

      jailenNo Gravatar in
    • public function getResultProperty()
      {
      – return $this->resultMember;
      + return $this->resultProperty;
      }
      where I can find this code to be fixed.

      Carlos BarrosNo Gravatar in
    • Ooops, I just noticed I didn’t say the exact location of this…

      lib/adapter/ckPropertyResultAdapter.class.php

      That’s the file.. But in the latest version, this is already fixed….

      Carlos

      AlexNo Gravatar in
    • Thanks! Great article!

      But I don’t understand what happens when the parameters of the web method are different from primitive types ?

      Thanks

      Carlos BarrosNo Gravatar in
    • Hi Alex. Sorry for the long delay, was taking my vacation :) Hmm, I didn’t understand very well your question, can you explain again?

      Thanks

      Carlos

      PrzemekNo Gravatar in
    • Nice tutorial.

      I have a small question – what about including date and time (mysql ‘date’ type) in SOAP response?

      Thanks,
      Przemek

      DennisNo Gravatar in
    • Thanks for the Tutorial.

      Perhals someone else has the problem in development: WSDL ist cached.

      soap_options:
      cache_wsdl: WSDL_CACHE_NONE

      Carlos BarrosNo Gravatar in
    • Hi Przemek. I think u can simply return it as an normal string, but I never did it.

      Dennis: Thanks for the tip, I used to delete the cached file from my tmp directory, but it was a pain, I’ll give it a try next time.

      Carlos

      DennisNo Gravatar in
    • Correction2 (tags seem not to be allowed):

      soap_options:
      cache_wsdl: <?php echoln(WSDL_CACHE_NONE) ?>

      RichNo Gravatar in
    • Hi Carlos – great post, thanks a lot :-) bit stuck at the moment though, checked out tag 3.0.0 from the SVN repository (nothing in ‘trunk’ at the moment?), and I don’t get any parameters passed to my action $request object, despite everything else being fine. Any ideas at all? Eg if I submit 1 in the SOAP request, $request->getParameter("myId") is NULL…

      Rich

      Carlos BarrosNo Gravatar in
    • Hi Rich.. I’m glad this article helped you :)

      About your problem, it’s hard to say without looking at your code and config… but one common problem I used to have is the fact that php will cache the .wsdl file and reuse it all the time, so making changes to the actual .wsdl file (or automatically generating it) wont affect current file being used by php. I had similar problems a lot of times.. Not sure if that’s your case, but try deleting the .wsdl file located in your /tmp directory, and try shutting off wsdl cache on your php.ini (if u didn’t already do this)..

      Carlos

      Jonathan VogtNo Gravatar in
    • Hi Carlos,

      great tutorial. In your tutorial you mention your email contact and that the correct way would be to implement a custom soaphandler. Are there any updates on that issue. Do you know if it still breaks the WSDL generator. If not how would one implement such a soaphandler?

      Thanks for sharing this tutorial