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:
<?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:
<?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:
<?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.

This component shows a lot of promise, but one problem I can't seem to get my head around is that it calls methods inside a Model rather than a Controller.
I only seem to be able to create methods in the Model. How do I query other (multiple) tables and perform business logic?
Thanks,
Jonathan
Thanks so far for all the help. I got the component working to the point where the WSDL XML is generated. I am using SoapUi to test and the example requests are generated when I start a new project in SoapUI.
My problem is that when I call a method I get an zero sized return.
The Request (This is the request generated by SoapUI)
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sch="http://schema.example.com">
<soapenv:Header/>
<soapenv:Body>
<sch:divide>
<a>16</a>
<b>4</b>
</sch:divide>
</soapenv:Body>
</soapenv:Envelope>
Raw return
HTTP/1.1 200 OK
Date: Tue, 22 Dec 2009 09:07:41 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.2.5
Set-Cookie: CAKEPHP_SOAP=ce95c4abe9787b51a41b92c47ada9eba; path=/
P3P: CP="NOI ADM DEV PSAi COM NAV OUR OTRo STP IND DEM"
Content-Length: 0
Connection: close
Content-Type: text/xml
Does anyone have an idea what I am doing wrong.
So It was a problem with my PHP en server config
1. Make sure that you PHP has the SOAP compiled in or a PHP SOAP module is loaded via the php.ini file.
2. SOAP seems to be quite strict on hostnames. Make sure that your server and client resolves the same IP for the name that you are using in your soap call. I had to add the hostname of my dev server on the servers hosts file to make it work.
The array of recognized types is in IPXMLSchema.class.php, around line 145 (in release 1.5 of the jool.nl webservice helper).
/**
* Checks if the given type is a valid XML Schema type or can be casted to a schema type
* @param string The datatype
* @return string
*/
public static function checkSchemaType($type) {
//XML Schema types
$types = Array("string" => "string",
"int" => "int",
"integer" => "int",
"boolean" => "boolean",
"float" => "float");
if(isset($types[$type])) return $types[$type];
else return false;
}
I checked out my phpinfo() and found under the soap section, soap.wsdl_cache=1
Seems once a wsdl is cached, your changes the webservice model would not be automatically updated.
After removing the cache files [wsdl*] located in soap.wsdl_cache_dir and disabling soap.wsdl_cache everything works perfectly.
ini_set('soap.wsdl_cache', 0);That in beforeFilter was enough to disable it.
Hope this helps someone.
if i'm call http://localhost/myapp/service/wsdl/test
the result is good (noting wrong, maybe...)
but if i'm call with http://localhost/myapp/service/call/test
the result is
Is there any solution??<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>
WSDL
</faultcode>
<faultstring>
SOAP-ERROR: Parsing WSDL: Couldn't load from 'http://localhost/myapp/service/service/wsdl/test.wsdl' : failed to load external entity "http://localhost/myapp/service/service/wsdl/test.wsdl"
</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
thanks for reply
Make sure that you server and client both resolve the same IP for the hostname that you use in your SOAP call. I had the same error when the server could not resolve the correct IP for the hostname in die SOAP call.
My bad, I sure was confused :D. Sorry about that.
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.
Thanks, I have added some more explanation to the article.
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.
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 ?
check if there are any trailing spaces after ?> in your component/controller/model files.
I had some and that caused the same problem
/**
* 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:
[code] /**
* 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;
}
[code]
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.
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
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
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")));But in example is "$server = new SoapServer($wsdlURL . '/' . $modelId);" ,how I could modify to using a classmap ?
thanks a lot of.
By the way thanks for the component Charles.
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
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.
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.
I have a problem loading the vendor... i'm using CakePHP 1.2 RC3...
can anyone help me...
Dude, did you see this?
http://book.cakephp.org/view/538/Loading-Vendor-Files
http://groups.google.com/group/cake-php/browse_thread/thread/1856af46a3589303#
In your model example, there is a function Divide.. ok.
But if I create :
function minus($a, $b)
{
return $a - $b;
}
I received in my Soap client this message :
"
Need I to create this function elsewhere ?
Otherwise, how to simply get a listing of a any model ?
Thank you very much for your help !
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.
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