A Component to help creating SOAP services

By Charles (char101)
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($cachePathnull$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($class0, -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 name
that 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 your
favorite 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 549

CakePHP Team Comments Author Comments
 

Comment

1 Link to jool.nl webservice handler

Posted Oct 24, 2007 by Edmunds Kalnins
 

Comment

2 Undefined offset

Thanks for 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
Posted Nov 15, 2007 by Nathan
 

Comment

3 Has anyone had success with this tutorial

Just curious if anyone has implemented this tutorial successfully...
Posted Nov 16, 2007 by Nathan
 

Comment

4 The fix (maybe)

The model class inherits some functions from its parent class (cakePHP's base model class). The wshelper library requires that methods that do not return anything be tagged with @return void but since there are some of the inherited methods that do not have the @return tag, the library emits notice.

To fix it, simply edit the IPReflectionCommentParser.class.php around line 135, add the check

if (! isset($tagArr[1])) {
    $tagArr[1] = 'void';
}
just below the

case 'return':

Sorry if I didn't put this in the article.
Posted Nov 17, 2007 by Charles
 

Comment

5 Maybe you can help

I make SOAP service, but with authorization(SOAP headers) and handler - controller and i have problems with it.
http://groups.google.com/group/cake-php/browse_thread/thread/1856af46a3589303#
Posted Mar 27, 2008 by wDevil
 

Bug

6 Problem loading

Hi,

I have a problem loading the vendor... i'm using CakePHP 1.2 RC3...

can anyone help me...
Posted Nov 20, 2008 by Bryan de Asis
 

Comment

7 Problem loading

Hi,

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
Posted Jan 31, 2009 by Caio Gouveia
 

Comment

8 Works fine

I've just used this with cake 1.2 and it 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.
Posted Feb 19, 2009 by Max Pimm
 

Question

9 Problem with customization

Ok, I don't know what I'm missing here. I've followed these directions and it works perfectly with Max's recommended changes. However, if I change the code, presumably to actually use it for something useful, things break.. Even little changes. Take the model:

Model Class:

<?php 

        
function divide($a$b)
        {
                if (
$b != 0) {
                        return 
$a $b;
                }
                return 
0;
        }
?>

if I change it to:

Model Class:

<?php 
        
function 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

Posted Feb 27, 2009 by Brett Nemeroff
 

Comment

10 -

@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.
Posted Feb 27, 2009 by Charles
 

Question

11 Custom types and arrays

Has anyone had luck implementing custom types and then using it as an array.

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
Posted Mar 2, 2009 by Alix
 

Comment

12 Re: Custom types and arrays

Are you getting just one StdClass or an array of empty objects with StdClass?

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")));
Posted Mar 2, 2009 by Max Pimm
 

Comment

13 Re: Custom types and arrays

I added the classmap map however now I get an empty Appointment object instead of an array of Appointments.

By the way thanks for the component Charles.


Are you getting just one StdClass or an array of empty objects with StdClass?

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")));
Posted Mar 3, 2009 by Alix
 

Question

14 custom types

Can someone post an example of a custom complex type?

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

Posted Mar 13, 2009 by Brett Nemeroff
 

Comment

15 Must tag model methods!

Earlier in the comments, people were having problems dealing with parameters, as was I. In the article, the author shows an example model method as this:


    /**
     * 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.
Posted Jun 3, 2009 by Matt Mendick
 

Question

16 Problem to generate the wsdl xml

hi,

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é externe
Emplacement : 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 ?

Posted Jun 4, 2009 by Kwan Joel
 

Comment

17 Reply

@Matt Mendick

Thanks, I have added some more explanation to the article.
Posted Jun 4, 2009 by Charles
 

Comment

18 Reply

@Matt Mendick
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.
Posted Jun 5, 2009 by Kwan Joel
 

Comment

19 Whitespace/xml header problems

Hi again - I think there's some confusion with @Charles about who was commenting on what here, not that it makes a big difference to the ultimate decyphering of the code, but the person who posted the comment has their name below the comment, not above it, so I was the one who posted about the @params, and @Kwan Joel was posting about the xml header issues.

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.
Posted Jun 5, 2009 by Matt Mendick
 

Comment

20 -

@Matt Mendick

My bad, I sure was confused :D. Sorry about that.
Posted Jun 5, 2009 by Charles
 

Comment

21 np

No problem, thanks for all your work on this.

@Matt Mendick

My bad, I sure was confused :D. Sorry about that.
Posted Jun 5, 2009 by Matt Mendick
 

Comment

22 XML Parsing Error: XML or text declaration not at start of entity

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é externe
Emplacement : 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
Posted Jun 15, 2009 by Sava
 

Comment

23 Thanks everyone for helping !

I have checked my model, controller, and component files. And there are one space caracter at the end of the files after ?>. I've deleted them and now it works perfectly. Thanks everyone !
Posted Jun 16, 2009 by Kwan Joel
 

Comment

24 delete your wsdl cache when testing

Wsdl cache caused me headache when I was testing my web service while changing return types (objects in particular) - saying no method with that name is available. After I deleted /tmp/wsdl* it started working as expected.
Posted Jun 18, 2009 by Sava