Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PHP

Write Enhanced SOAP-Webservices with PHP

4.79/5 (11 votes)
10 Apr 2012CDDL11 min read 58.5K   2.4K  
A tutorial about how to write enhanced PHP-SOAP-webservices with automatic WSDL-generation

This is a tutorial about how to write enhanced SOAP-webservices with PHP.

Introduction

PHP is very useful in connection with SOAP when wanting business logic to be shared on a central location. This article shows how webservices can be written in PHP with ease and how they can be accessed with C#. The code is kept simple that even intermediate beginners can understand how to write webservices but flexible enough to be used by advanced developers.

Because the default SoapServer-class has some missing features, I have written a framework to support features like automatic WSDL-generation or transferring big data which I will explain step by step.  

Defining a Service

A service is not more than a "thing" everybody who is authorized can ask to do some predefined tasks and return the result.

Services can be realized by objects - but keep in mind that services are procedural, not object-orientated. So the mentioned "thing" is an object, the tasks equates to methods and parameters and the result is the returning value of the method.

It is important that webservices are well defined, so clients know exactly how to consume these services and methods. For this Web Service Description, a XML based Language has been established: WSDL.

WSDL-Generation

Writing WSDL-documents manually leads to redundancy code, since each webservice operation in PHP has to be specified in WSDL. When changing the signature of a webservice-method, the corresponding WSDL part has to be rewritten. So why not let it be generated by PHP itself?

The Problem of Types in PHP

WSDL requires to describe all used types for parameters and results in XSD (XML Schema). But in PHP, the types of variables have not to be specified, so by default there is no way to determine the types used in methods if the programmer does not know them. Thus the webservice has to be commented in a way the WSDL-builder can understand.

My framework provided within this article requires the following comment-syntax to extract type-information. For methods:

PHP
/**
 * @param ComplexNumber the first summand
 * @param ComplexNumber the second summand
 * @return ComplexNumber the sum of a and b
 * @access web
 */
function AddTwoComplexNumbers($a, $b) { [...] }

And for structures:

PHP
/**
 * @name Complex
 */
class ComplexNumber
{
  /**
   * The real part
   * @var float
   * @access web
   */
  public $Real;
  /**
   * The imaginary part
   * @var float
   * @access web
   */
  public $Imaginary;
}

The parameter @access is to determine whether the element is accessible from outside or not - elements without this comment will not be published (for security reasons, so unmarked methods cannot be called from outside). The parameter @name is optional and specifies which name is used for WSDL-generation. The optional comment after the type-declaration, separated by a whitespace, will be included in the WSDL-document.

Using the WsdlBuilder-class of the Framework

When the services for which the WSDL-document have to be generated are commented like the example above, the WebserviceInfo-class can be used, otherwise a compatible one has to be written.

The following listing shows how to use the WsdlBuilder-class.

PHP
class TestWebservice
{
  //include method AddTwoComplexNumbers from example above
}

//include class ComplexNumber from example above

require_once "webserviceInfo.php";

WebserviceRegistry::RegisterWebservice
    ("Test", "TestWebservice"); //"TestWebservice" is the class name of the service
    //"Test" is the actual name used for WSDL-generation

require_once "wsdlBuilder.php";
$builder = new WsdlBuilder
    ("http://www.example.com/myservice"); 	//The URI is the target namespace 
					//of the WSDL-document

require_once "functions.php"; //For GetServerUrl

foreach (WebserviceRegistry::ListWebserviceInfos() 
		as $info) //Add all registered Webservices
  $builder->AddService($info, GetServerUrl() . $_SERVER["SCRIPT_NAME"] . 
		"/service/" . $info->serviceName);
    //The second parameter is the full location where the service can be accessed

$doc = $builder->CreateDocument(); //Create DOM-Document
file_put_contents("services.wsdl", $doc->saveXml());

Alternatively, the WsdlFile-class in wsdlBuilder.php can be used:

PHP
require_once "wsdlBuilder.php";

$urlFormat = GetServerUrl() . $_SERVER["SCRIPT_NAME"] . "/service/%s";
  //%s will be replaced with the service name

$wsdlFile = new WsdlFile(WebserviceRegistry::ListWebserviceInfos(), 
		"http://www.example.com/services", $urlFormat);
$wsdlFile->FileName = "services.wsdl";
$wsdlFile->IsFileCached = false;

The method $wsdlFile->GetFileName() will create the file if needed (depending on the property $wsdlFile->IsFileCached, if it is true, the file will be created only if it does not exist, if it is false, the file will be created always) and return its filename.

SoapWsdlResponse and SoapWebserviceResponse

For simplifying the development, the classes SoapWsdlResponse and SoapWebserviceResponse in soapResponses.php handle the whole soap-response, by calling the Load()-method on them.

The SoapWsdlResponse-class takes a WsdlFile-instance as construction argument and sends the WSDL-document to the client when calling Load().

The SoapWebserviceResponse-class takes a WsdlFile-instance as construction argument either, but requires additionally the service which should handle the request to be passed. When calling Load(), all the other stuff like checking whether to generate the WSDL-file or creating the proxy (see the next chapter) is done automatically.

The following example shows how to use these classes:

PHP
if ($query === "/services.wsdl") //WSDL-Retrival
	$response = new SoapWsdlResponse($wsdlFile); 
	//$wsdlFile is the WsdlFile-instance from the last example
else if (startsWith($query, "/service/")) //SOAP-Call
{
	$serviceName = substr($query, strlen("/service/"));
	$service = WebserviceRegistry::GetWebservice($serviceName);
	$response = new SoapWebserviceResponse($wsdlFile, $service);
	$response->WsdlCacheEnabled = false;
}

if ($response != null)
	$response->Load(); //Sends the response to the client

How the WsdlBuilder Maps the Types to XSD

The WsdlBuilder uses internally the IWsdlTypeMapping-interface to map types to an URI.

The interface is quite simple:

PHP
interface IWsdlTypeMapping
{
  /**
   * @param WebserviceType $type
   * @param IWsdlTypeMappingContext $context
   * @return string Namespace:Element
   */
  public function GetTypeMapping(WebserviceType $type, IWsdlTypeMappingContext $context);
}

The provided interface IWsdlTypeMappingContext looks like:

PHP
interface IWsdlTypeMappingContext
{
  /**
   * @return DOMDocument
   */
  public function GetDoc();
  public function AppendToSchema(DOMElement $element);
  /**
   * @param WebserviceType $type
   * @return string Namespace:Element
   */
  public function GetTypeMapping(WebserviceType $type);
}

Whereas AppendToSchema adds a custom DOMElement to the Schema-node of the WSDL-Document. A new DOMElement can be obtained by using the GetDoc-method which returns the main DOMDocument. The method GetTypeMapping allows to ask other type-mappers whether they can map subtypes (the type is then added to the document too) - this is needed when e.g. multidimension arrays are mapped. Make sure that this will not end in a never-ending recursion.

Objects which implements the IWsdlTypeMapping-interface can be added to the WsdlBuilder as follows:

PHP
WsdlBuilder::$WsdlTypeMappings[] = new MyCustomTypeMapping();

Custom type-mappers give the ability to change the way parameters of methods are seen by clients. With returning the constant WsdlBuilder::NoMapping it is even possible to hide parameters in the WSDL-description - in connection with the next chapter this is quite an interesting option.

Pre- and Postprocessing through Proxy

Proxy

To avoid the disadvantages of the SoapServer, a proxy can be used to edit the values before passing as parameter to the called method and after returning as result but before sending to the client. This gives enhanced capabilities, e.g., a DateTime-object can be converted into a representation understood by SOAP-clients when passing it as result.

Using the WebserviceProxy-class

Technically, the proxy intercepts the flow control between the SOAP-server and the service-object.

PHP
require_once "webserviceProxy.php";
$server->setObject(new WebserviceProxy($service));

Calls from the server to the proxy are redirected to the service.

The proxy converts values from the client back into PHP native objects (e.g. DateTime values) and values from the service to a representation understood by the client.

Custom Converters

To write a custom converter, simply implement the IObjectConverter-interface:

PHP
interface IObjectConverter
{
  /**
   * @param WebserviceType $objectType the type of the target value
   * @param mixed $message the message from client to decode
   * @param IObjectConvertContext $context
   * @return mixed the decoded value
   */
  public function ConvertBack
	(WebserviceType $targetType, $message, IObjectConvertBackContext $context);

  /**
   * @param WebserviceType $objectType the type of $object
   * @param mixed $object the source from service to convert
   * @param IObjectConvertContext $context
   * @return string the converted message
   */
  public function Convert(WebserviceType $objectType, 
		$object, IObjectConvertContext $context);
}

The context looks like:

C#
interface IObjectConvertContext
{
  public function Convert(WebserviceType $targetType, $object);
}

interface IObjectConvertBackContext
{
  public function ConvertBack(WebserviceType $targetType, $message);
}

This allows to ask other converters which is important when working with nested types.

Make sure that this will not end in a never-ending recursion, too.

Service-Registration

If multiple services are offered, they have to be organized. For this purpose, the static ServiceRegistry-class is responsible.

Its use is very simple:

PHP
require_once "webserviceInfo.php";

require_once "test.php";
WebserviceRegistry::RegisterWebservice
	("Test", "TestWebservice"); //Register a webservice-class

$service = WebserviceRegistry::GetWebservice("Test"); //Instantiates the webservice-class

WebserviceRegistry::ListWebserviceInfos(); //Returns WebserviceInfo[]

Authentication

There are a few ways to realize authentication. I recommend using the Http Basic-Authentication because it is quite easy and works pretty well with WCF (Windows Communication Foundation), so I will explain this one first. But since not every PHP-server (e.g., CGI) supports this authentication mode, I will explain the Http Header-Authentication too. As my own website does not support Http Basic-Authentication and the authentication by custom headers is more complex, the attached example shows how to use Http Header-Authentication.

Http Basic-Authentication

For Http Basic-Authentication, the client sends at each request two pieces of information: The username, which will be stored in $_SERVER["PHP_AUTH_USER"], and the password, which will be stored in $_SERVER["PHP_AUTH_PW"]. They should be checked before or while handling the SOAP-Request.

To signal that the credentials are missing or wrong (or in general that authentication is needed), the following headers can be sent:

PHP
header('WWW-Authenticate: Basic realm="MyRealm"');
header("HTTP/1.0 401 Unauthorized");

The realm describes the section which is protected, it can be freely selected.

Unfortunately both username and password are sent in plain text, so if they have to be crypted, SSL should be used.
When using Http Basic-Authenication, WCF requires SSL to be enabled.

Http Header-Authentication

This authentication is similar to the Http Basic-Authentication: The client sends on each call both the username and password as header information. But in contrast, on the client side sending the header information has to be done manually and the server does not have to signalize that authentication is needed with "WWW-Authenticate".

There are no default-names for the header-names, but they should called "USER" and "PASSWORD", so the server can check them with $_SERVER["HTTP_USER"] and $_SERVER["HTTP_PASSWORD"].

Even CGI-php-server supports this method and WCF does not require SSL to be enabled (but nevertheless, the password-exchange should be encrypted).  

How Big Data Would be Transferred

For transferring binary data or data with illegal chars, SOAP offers DIME (Direct Internet Message Encapsulation), which both the default PHP SoapServer and WCF do not support. Despite this, to get feedback of the down or upload progress, you have to do really much work.

An alternative is to encode the data with Base64, but even this has a few disadvantages:
It is quite slow, the data is blown up and getting feedback of the transferring progress is very difficult too.

So why not transfer the data with a separate raw http-request beside the SOAP-message? The data need not be encoded and implementing progress feedback is easy since it is a default http-request and has nothing to do with SOAP.

Let's say we have the following method:

C#
/**
 * @return string
 * @access web
 */
public function GetData()
{
  return file_get_contents("picture.jpg");
}

The activity diagram for transmitting the returned data would be look like:

Activity Diagram
  1. Tell the client that the returning value is some kind of key which gives access to the real data
  2. Save the binary data (with the key) in some kind of database or hard drive disk
  3. Return (in the SOAP-call) instead of the data a key which points to the database-entry
  4. Offer a (non-SOAP) script where the client can obtain the data by passing the key
  5. Delete the data in the database

The work flow for uploading data from the client does not differ as much:

  1. Tell the client that the parameter type is some kind of key (which can be obtained by uploading data)
  2. Offer a (SOAP) webservice which returns such a key (like an upload-ticket)
  3. Offer a (non-SOAP) script where the client can upload data by passing the key, save this data (with the key) in a database
  4. When the client calls a (SOAP) method with this key, replace the key with the data stored in the database
  5. Delete the data in the database

For the first step, the interface IWsdlTypeMapping has to be implemented so that the data-type will be mapped to the key.

Because "string" is already mapped to "xsd:string", the data-type has to be changed to "binary" or something else ("@return binary" or "@param binary").

The steps from 1-5 can be done by an object which implements the IObjectConverter-interface.

The Convert-method can perform the steps 2 and 3 for downloading data, the ConvertBack-method the step 4 for uploading data.

It is recommended to include a complete URL with the key, so that the client only has to fetch this URL for downloading or uploading data.

Beneath the mentioned advantages, the client could upload multiple data (needed for several method-calls) at once, maybe zip-compressed to speed up the transfer.

Have fun writing the code!

To be honest, I have already written such an extension, but it requires my container- and cache-framework, which I still did not publish. Nevertheless, it can be downloaded in the download section above.

Example

The example is divided up into several files: index.php is the entry point, security.php contains the Security-class which is responsible for authentication and TestWebservice.php, which contains the example webservice.

I think the example is commented enough to be understood without further explanation.

Client in C#

Writing a SOAP-client in C# is very easy, thanks to WCF and Visual Studio.

At first, create a new C# project, preferable a console application.
In the Solution Explorer, right click "References" and then click "Add Web Reference".
Insert the URL to the WSDL-document (for the given example, http://www.hediet.de/projects/enhanced-soap-webservices/code/index.php/services.wsdl) and click "Go", now Visual Studio parses the WSDL-document and lists all found services and their methods. Give the namespace of the reference a meaningful name and click "Add Reference".

Using the Generated Client

To call methods of the webservice, simply add the namespace you have provided in the Add Web Reference dialog to the using-section and create a client:

C#
TestPortType testClient = new TestPortTypeClient();
testClient.SomeMethod();
For each service a specific client is generated. The configuration of these clients is stored in app.config.

Authentication

Http Basic-Authentication

As I said in earlier chapters, WCF supports Http Basic-Authentication. To enable this, go to the app.config file in the Solution Explorer.

Edit the security-node (configuration/system.serviceModel/bindings/basicHttpBinding/binding/security) so it looks like:

XML
<security mode="Transport">
  <transport clientCredentialType="Basic" proxyCredentialType="None"
    realm="MyRealm" />
  <message clientCredentialType="UserName" algorithmSuite="Default" />
</security>

The realm has to be the same as in the header of the response (see chapter Authentication).

After creating the client, the credentials have to be set:

C#
client.ClientCredentials.UserName.UserName = "user";
client.ClientCredentials.UserName.Password = "password";

Important note: WCF requires SSL to be enabled (https instead of http)!

If the certificate is not valid, you can use the following code snippet to disable validation-checks:

C#
private static bool ValidateRemoteCertificate(object sender,
  X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors)
{
  return true;
}
[...].ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate;

Http Header-Authentication

The other possibility is using custom headers to pass the credentials.

For this, neither the app.config has to be edited nor the SSL-protocol has to be used, but a custom client message inspector has to be developed for intercepting the communication of WCF.

To insert the message inspector, a custom endpoint behavior has to be written by implementing the IEndpointBehavior:

C#
public class CredentialHeaderBehavior : IEndpointBehavior
{
    private readonly NetworkCredential credential;

    public CredentialHeaderBehavior(NetworkCredential credential)
    {
        this.credential = credential; //The credential to pass with the header
    }

    public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Dispatcher.ClientRuntime behavior)
    {
		//Add the custom header message inspector
        behavior.MessageInspectors.Add(new CredentialHeaderMessageInspector(credential));
    }

	//Not used methods, but required when implementing IEndpointBehavior
    public void AddBindingParameters(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Channels
        .BindingParameterCollection bindingParameters) { }

    public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
        System.ServiceModel.Dispatcher
        .EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint serviceEndpoint) { }
}

And the CredentialHeaderMessageInspector, which actually sets the headers:

internal class CredentialHeaderMessageInspector : IClientMessageInspector
{
    private readonly NetworkCredential credential;

    public CredentialHeaderMessageInspector(NetworkCredential credential)
    {
        this.credential = credential;
    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request,
        System.ServiceModel.IClientChannel channel)
    {
        HttpRequestMessageProperty httpRequestMessage;
        object httpRequestMessageObject;
        if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
            httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
        else
        {
            httpRequestMessage = new HttpRequestMessageProperty();
            request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);
        }

		//Add the username and password to the headers
        httpRequestMessage.Headers["USER"] = credential.UserName;
        httpRequestMessage.Headers["PASSWORD"] = credential.Password;

        return null;
    }

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply,
        object correlationState) { }
}

Extension Points

Beneath the mentioned possible extensions, the following could be implemented too:

  • Ajax-Access

    The webservices could be accessed with JavaScript.

  • Base64-Support

    The type "base64" could be automatically decoded in the preprocessing and encoded in the postprocessing.

History

  • Version 1.2 - C# demo added, PHP-demo and explanation improved, new authentication method introduced
  • Version 1.1 - Corrected some typos
  • Version 1.0 - Initial article

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)