Tuesday, October 26, 2010

SOAP WebService in Symfony

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:

info@amphee.com [~/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. 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, 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:

info@amphee.com [~/symfony/blog]# symfony generate:module frontend soap
>> dir+ /home/amphee/symfony/blog/apps/frontend/modules/soap/templates
>> file+ /home/amphee/symfony/blog/app...soap/templates/indexSuccess.php
>> dir+ /home/amphee/symfony/blog/apps/frontend/modules/soap/actions
>> file+ /home/amphee/symfony/blog/app.../soap/actions/actions.class.php
>> file+ /home/amphee/symfony/blog/tes...al/frontend/soapActionsTest.php
>> tokens /home/amphee/symfony/blog/tes...al/frontend/soapActionsTest.php
>> tokens /home/amphee/symfony/blog/app...soap/templates/indexSuccess.php
>> tokens /home/amphee/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



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:

info@amphee.com [~/symfony/blog]# symfony webservice:generate-wsdl frontend soap http://blog.barros.ws/symfony
>> file- /home/amphee/symfony/blog/web/soap.php
>> file+ /home/amphee/symfony/blog/web/soap.php
>> tokens /home/amphee/symfony/blog/web/soap.php
>> file+ /home/amphee/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:



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.



You can find original post here : http://blog.barros.ws/2008/11/16/soap-webservice-in-symfony/

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.