A Component to help creating SOAP services
A component providing automatic WSDL generation using jool.nl Webservice Helper library, CakePHP caching of generated WSDL, and automatic handling of SOAP calls.
Concepts
- SOAP methods will be implemented as model class methods. Models that will handle SOAP calls will have suffix Service, for example BookService
- The Soap component will provide function to generate WSDL from the model class definition and to handle SOAP calls to the model class methods.
- A controller will act as a SOAP proxy to the service models using the SOAP component
Requirements
- PHP Soap extension
- CakePHP 1.2 (haven't tested it in CakePHP 1.1)
Webservice Handler library
Download Webservice Handler library from jool.nl site and extract the contents into app/vendors/wshelper.Create the SOAP component
Save the code below into app/controllers/components/soap.php.The generated WSDL will be cached into app/tmp/cache directory. If DEBUG configuration
is greater than 0, the cache file modified time will be compared the model file
modified time and updated appropriately.
Component Class:
Download code
<?php
vendor('wshelper/lib/soap/IPReflectionClass.class');
vendor('wshelper/lib/soap/IPReflectionCommentParser.class');
vendor('wshelper/lib/soap/IPXMLSchema.class');
vendor('wshelper/lib/soap/IPReflectionMethod.class');
vendor('wshelper/lib/soap/WSDLStruct.class');
vendor('wshelper/lib/soap/WSDLException.class');
/**
* Class SoapComponent
*
* Generate WSDL and handle SOAP calls
*/
class SoapComponent extends Component
{
var $params = array();
function initialize(&$controller)
{
$this->params = $controller->params;
}
/**
* Get WSDL for specified model.
*
* @param string $modelClass : model name in camel case
* @param string $serviceMethod : method of the controller that will handle SOAP calls
*/
function getWSDL($modelId, $serviceMethod = 'call')
{
$modelClass = $this->__getModelClass($modelId);
$expireTime = '+1 year';
$cachePath = $modelClass . '.wsdl';
// Check cache if exist
$wsdl = cache($cachePath, null, $expireTime);
// If DEBUG > 0, compare cache modified time to model file modified time
if ((Configure::read() > 0) && (! is_null($wsdl))) {
$cacheFile = CACHE . $cachePath;
if (is_file($cacheFile)) {
$modelMtime = filemtime($this->__getModelFile($modelId));
$cacheMtime = filemtime(CACHE . $cachePath);
if ($modelMtime > $cacheMtime) {
$wsdl = null;
}
}
}
// Generate WSDL if not cached
if (is_null($wsdl)) {
$refl = new IPReflectionClass($modelClass);
$controllerName = $this->params['controller'];
$serviceURL = Router::url("/$controllerName/$serviceMethod", true);
$wsdlStruct = new WSDLStruct('http://schema.example.com',
$serviceURL . '/' . $modelId,
SOAP_RPC,
SOAP_LITERAL);
$wsdlStruct->setService($refl);
try {
$wsdl = $wsdlStruct->generateDocument();
// cache($cachePath, $wsdl, $expireTime);
} catch (WSDLException $exception) {
if (Configure::read() > 0) {
$exception->Display();
exit();
} else {
return null;
}
}
}
return $wsdl;
}
/**
* Handle SOAP service call
*
* @param string $modelId : underscore notation of the called model
* without _service ending
* @param string $wsdlMethod : method of the controller that will generate the WSDL
*/
function handle($modelId, $wsdlMethod = 'wsdl')
{
$modelClass = $this->__getModelClass($modelId);
$wsdlCacheFile = CACHE . $modelClass . '.wsdl';
// Try to create cache file if not exists
if (! is_file($wsdlCacheFile)) {
$this->getWSDL($modelId);
}
if (is_file($wsdlCacheFile)) {
$server = new SoapServer($wsdlCacheFile);
} else {
$controllerName = $this->params['controller'];
$wsdlURL = Router::url("/$controllerName/$wsdlMethod", true);
$server = new SoapServer($wsdlURL . '/' . $modelId);
}
$server->setClass($modelClass);
$server->handle();
}
/**
* Get model class for specified model id
*
* @access private
* @return string : the model id
*/
function __getModelClass($modelId)
{
$inflector = new Inflector;
return ($inflector->camelize($modelId) . 'Service');
}
/**
* Get model id for specified model class
*
* @access private
* @return string : the model id
*/
function __getModelId($modelClass)
{
$inflector = new Inflector;
return $inflector->underscore(substr($class, 0, -7));
}
/**
* Get model file for specified model id
*
* @access private
* @return string : the filename
*/
function __getModelFile($modelId)
{
$modelDir = dirname(dirname(dirname(__FILE__))) . DS . 'models';
return $modelDir . DS . $modelId . '_service.php';
}
}
?>
Create the controller that will handle SOAP calls
This is an example controller. You can change the method namethat will handle SOAP calls and provide WSDL definition as you wish.
But don't forget to change the arguments to the handle and
getWSDL methods.
Save the file into app/controllers/service_controller.php
Controller Class:
Download code
<?php
class ServiceController extends AppController
{
public $name = 'Service';
public $uses = array('TestService');
public $helpers = array();
public $components = array('Soap');
/**
* Handle SOAP calls
*/
function call($model)
{
$this->autoRender = FALSE;
$this->Soap->handle($model, 'wsdl');
}
/**
* Provide WSDL for a model
*/
function wsdl($model)
{
$this->autoRender = FALSE;
header('Content-Type: text/xml'); // Add encoding if this doesn't work e.g. header('Content-Type: text/xml; charset=UTF-8');
echo $this->Soap->getWSDL($model, 'call');
}
}
?>
Create the service model
This is a test model. Save it into app/models/test_service.php. Note that the webservice handler library parses the method comments to create the WSDL, so you'll need to make sure that all the function parameters and return value are documented in the function docblock. Make sure that you specify the type of each parameters and make sure the ordering matches the order of the parameters in the function. (Thanks to Brett Nemeroff for pointing this).Model Class:
Download code
<?php
class TestService extends AppModel
{
var $name = 'TestService';
var $useTable = false;
/**
* Divide two numbers
*
* @param float $a
* @param float $b
* @return float
*/
function divide($a, $b)
{
if ($b != 0) {
return $a / $b;
}
return 0;
}
}
?>
Testing the service
My favorite tool for testing SOAP services is SoapUI. You can use it or yourfavorite tool to test the service. To access the WSDL, direct your tool to
http://yourhost/service/wsdl/test. The SOAPAction URL will be
http://yourhost/service/call/test.
Comments
Comment
1 Link to jool.nl webservice handler
Comment
2 Undefined offset
I am getting the following error after following this tutorial:
Notice (8): Undefined offset: 1 [CORE\app\vendors\wshelper\lib\soap\IPReflectionCommentParser.class.php, line 136]
I am using:
cake 1.2.0.5875 pre-beta
php 5.2.3
jool.nl webservices helper 1.5.0
It appears to throw an error when it attempts to generate the wsdl file for the first time from the
->setService($refl)call on line 64 of the soap.php component:60 $wsdlStruct = new WSDLStruct('http://schema.example.com',
61 $serviceURL . '/' . $modelId,
62 SOAP_RPC,
63 SOAP_LITERAL);
64 $wsdlStruct->setService($refl);
65 try {
66 $wsdl = $wsdlStruct->generateDocument();
67 // cache($cachePath, $wsdl, $expireTime);
68 } catch (WSDLException $exception) {
Any ideas?
NSM
Comment
3 Has anyone had success with this tutorial
Comment
4 The fix (maybe)
To fix it, simply edit the IPReflectionCommentParser.class.php around line 135, add the check
just below theif (! isset($tagArr[1])) {
$tagArr[1] = 'void';
}
case 'return':
Sorry if I didn't put this in the article.
Comment
5 Maybe you can help
http://groups.google.com/group/cake-php/browse_thread/thread/1856af46a3589303#
Bug
6 Problem loading
I have a problem loading the vendor... i'm using CakePHP 1.2 RC3...
can anyone help me...
Comment
7 Problem loading
Dude, did you see this?
http://book.cakephp.org/view/538/Loading-Vendor-Files
Comment
8 Works fine
I did have to change a couple of things.
1) Vendor imports
App::import('Vendor', 'IPReflectionClass', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'IPReflectionClass.class.php'));App::import('Vendor', 'IPReflectionCommentParser', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'IPReflectionCommentParser.class.php'));
App::import('Vendor', 'IPXMLSchema', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'IPXMLSchema.class.php'));
App::import('Vendor', 'IPReflectionMethod', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'IPReflectionMethod.class.php'));
App::import('Vendor', 'WSDLStruct', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'WSDLStruct.class.php'));
App::import('Vendor', 'WSDLException', array('file' => 'wshelper' . DS . 'lib' . DS . 'soap' . DS . 'WSDLException.class.php'));
2) If debug is 2 the xml output is not valid so i turned it off in the controller
function afterFilter(){
if (Configure::read('debug') > 1){
Configure::write('debug', 0);
}
}
I also found a couple of things that you might want to consider mentioning or changing
1) The WSDL file generated makes references to the http://schema.example.com namespace.
An alternative would be
$wsdlStruct = new WSDLStruct(Router::url('/', true),$serviceURL . '/' . $modelId,
SOAP_RPC,
SOAP_LITERAL);
2) The line that saves the generated WSDL file in cache is commented
//cache($cachePath, $wsdl, $expireTime);Thanks again for the component, it integrates nicely with cakephp.
Question
9 Problem with customization
Model Class:
<?php
function divide($a, $b)
{
if ($b != 0) {
return $a / $b;
}
return 0;
}
?>
if I change it to:
Model Class:
<?phpfunction divide($a, $c)
{
if ($c != 0) {
return $a / $c;
}
return 0;
}
?>
It doesn't work. It returns 0 everytime. In fact, if I just return $c in that example, it returns null. It's worth mentioning that the wsdl IS rendered properly requesting a 'c' var instead of a 'b' var. The soap request changed to 'c' looks good too:
<SOAP-ENV:Body>
<m:divide xmlns:m="http://www.example.com/">
<a xsi:type="xsd:float">5</a>
<c xsi:type="xsd:float">6</c>
</m:divide>
</SOAP-ENV:Body>
I'm not sure what I'm missing here..
Now I added a function to the model:
Model Class:
<?php/**
* Add two numbers
*
* @param float $a
* @param float $b
* @return float
*/
function add($a,$b)
{
return $a + $b;
}
}
?>
Which once again, the WSDL is generated perfectly, but when I make a request I get "Procedure 'add' not present. Am I supposed to register my functions somewhere? That would explain all of this, but in the working example, I don't see the functions or the variables registered. So I'm really confused.
Lastly, and this I'm sure will expose my lack of experience in Cake, why are the functions in the model, and not the controller? I'm not sure how to do all the same controller type actions from the model. Am I missing something?
Thanks!
-Brett
Comment
10 -
Are you using some opcode cache (eAccelerator, APC). Probably you can try disabling it. Also check that the file refered by $wsdlCacheFile exists and that the content is right.
The functions are placed in the model because SoapServer needs a class and it will be rather messy if the controller that calls the component will be used by the component to handle the request. Besides, controller outputs view and models returns data. Web service functions does not deal with view and more with data so I think it fits more in the model.
Question
11 Custom types and arrays
eg:
class appointment
{
/** @var float */
public $patient;
/** @var string */
public $time;
}
/**
* Add appointments
*
* @param appointment[] $a
* @return int
*/
function add($a)
{
...
}
I'm trying to send an array of appointments from a C# app. But either get a null passed into param $a or an empty StdClass.
Thanks in advanced.
Alix
Comment
12 Re: Custom types and arrays
If its the later then try using a classmap when you instantiate your soap server so that it can map the array objects to your class.
$server = new SoapServer("appointments.wsdl", array("classmap" => array("appointment" => "localAppointmentClass")));Comment
13 Re: Custom types and arrays
By the way thanks for the component Charles.
Question
14 custom types
Here's what I'm trying, proof of concept style:
/**
* List of patients
*
* @return string[]
*/
function getlist() {
$val=array('one',array('joe','mack','rob'),'three');
return $val;
}
This produces:
<SOAP-ENV:Body>
-<ns1:getlistResponse xmlns:ns1="http://schema.example.com">
-<getlistReturn>
-<xsd:string>one</xsd:string>
-<xsd:string>Array</xsd:string>
-<xsd:string>three</xsd:string>
</getlistReturn>
</ns1:getlistResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I know I probably need to make a class and reference it, but I'm not really sure how to even define the class as a complex type. A small example would go a long way. :)
Thanks,
Brett
Comment
15 Must tag model methods!
/**
* Divide two numbers
*
* @param float $a
* @param float $b
* @return float
*/
function divide($a, $b)
{
if ($b != 0) {
return $a / $b;
}
return 0;
}
But they probably forgot to mention that the comments appearing above the function are extremely important! The vendor wshelper files must read them to determine the structure of the WSDL. Therefore, if I wanted 4 parameters in my function (name, age, height, weight) and for it to return a string, I would need to put them in the comments, like so:
/**
* Divide two numbers
*
* @param string $name
* @param int $age
* @param int $height
* @param int $weight
* @return string
*/
function divide($name, $age, $height, $weight)
{
// your code
return $mystring;
}
So, in case anyone else couldn't figure that out, it's there now, explicitly stated.
Thanks for this article, it's made soap so much easier for me, I really appreciate your time put into this.
Question
16 Problem to generate the wsdl xml
I recently working with CakePHP and I have to implement a SOAP Server with my models. I have follow all the tutorial, and the comments. But My server can not generate the wdsl xml.
In a first time, I have to add
header('Content-type: text/xml; charset=UTF-8');in the getWSDL method in the component soap.php. If I do not have this line in this method, the wdsl file is not considered as an xml file but as an html file.
And when I add the type of the content, I get this error:
Erreur d'analyse XML : instruction de traitement XML ou texte pas au début d'une entité externeEmplacement : http://127.0.0.1/siv/service/wsdl/test
Numéro de ligne 1, Colonne 4 : <?xml version="1.0"?>
---^
Is there any solution to delete the default line ?
Comment
17 Reply
Thanks, I have added some more explanation to the article.
Comment
18 Reply
Hi thanks. I've found that the generated xml has three space characters at the beginning of the file. How could I delete them ? I don't really know what file I have to edit.
Comment
19 Whitespace/xml header problems
To complicate things more, I'll now talk about the xml header issues:
I was also having the problem where the generated text was a valid wsdl (determined by copying and pasting the source into a plain .xml static file, then viewing it with firefox: valid wsdl), but the header was still coming through as text/html on the generated document. I haven't had time to dig through and figure out exactly what is going on, but it seems as if the method showWSDL in WSHelper.class.php is not getting called, as the header() call in that method is not being called, therefore the header is not being set and is defaulting to text/html. So, I "fixed" the problem (at least my problem) by removing the header() call in showWSDL, and adding this line:
header('Content-type: text/xml; charset=UTF-8');
as the first line in the method getWSDL, which is in soap.php (component class).
This seems to work quite well, and I get valid xml, which is shown in "nice form" in firefox.
Now I really have working code, thanks a lot. For people talking about having spaces, I definitely would check all php files involved with the service, to make sure there are no spaces or newlines at the beginning or end of the files (before or after the tags) because that will definitely be causing you troubles.
Comment
20 -
My bad, I sure was confused :D. Sorry about that.
Comment
21 np
Comment
22 XML Parsing Error: XML or text declaration not at start of entity
check if there are any trailing spaces after ?> in your component/controller/model files.
I had some and that caused the same problem
Comment
23 Thanks everyone for helping !
Comment
24 delete your wsdl cache when testing