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:
function AddTwoComplexNumbers($a, $b) { [...] }
And for structures:
class ComplexNumber
{
public $Real;
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.
class TestWebservice
{
}
require_once "webserviceInfo.php";
WebserviceRegistry::RegisterWebservice
("Test", "TestWebservice");
require_once "wsdlBuilder.php";
$builder = new WsdlBuilder
("http://www.example.com/myservice");
require_once "functions.php";
foreach (WebserviceRegistry::ListWebserviceInfos()
as $info)
$builder->AddService($info, GetServerUrl() . $_SERVER["SCRIPT_NAME"] .
"/service/" . $info->serviceName);
$doc = $builder->CreateDocument();
file_put_contents("services.wsdl", $doc->saveXml());
Alternatively, the WsdlFile
-class in wsdlBuilder.php can be used:
require_once "wsdlBuilder.php";
$urlFormat = GetServerUrl() . $_SERVER["SCRIPT_NAME"] . "/service/%s";
$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:
if ($query === "/services.wsdl")
$response = new SoapWsdlResponse($wsdlFile);
else if (startsWith($query, "/service/"))
{
$serviceName = substr($query, strlen("/service/"));
$service = WebserviceRegistry::GetWebservice($serviceName);
$response = new SoapWebserviceResponse($wsdlFile, $service);
$response->WsdlCacheEnabled = false;
}
if ($response != null)
$response->Load();
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:
interface IWsdlTypeMapping
{
public function GetTypeMapping(WebserviceType $type, IWsdlTypeMappingContext $context);
}
The provided interface IWsdlTypeMappingContext
looks like:
interface IWsdlTypeMappingContext
{
public function GetDoc();
public function AppendToSchema(DOMElement $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:
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
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.
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:
interface IObjectConverter
{
public function ConvertBack
(WebserviceType $targetType, $message, IObjectConvertBackContext $context);
public function Convert(WebserviceType $objectType,
$object, IObjectConvertContext $context);
}
The context looks like:
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:
require_once "webserviceInfo.php";
require_once "test.php";
WebserviceRegistry::RegisterWebservice
("Test", "TestWebservice");
$service = WebserviceRegistry::GetWebservice("Test");
WebserviceRegistry::ListWebserviceInfos();
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:
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:
public function GetData()
{
return file_get_contents("picture.jpg");
}
The activity diagram for transmitting the returned data would be look like:
- Tell the client that the returning value is some kind of key which gives access to the real data
- Save the binary data (with the key) in some kind of database or hard drive disk
- Return (in the SOAP-call) instead of the data a key which points to the database-entry
- Offer a (non-SOAP) script where the client can obtain the data by passing the key
- Delete the data in the database
The work flow for uploading data from the client does not differ as much:
- Tell the client that the parameter type is some kind of key (which can be obtained by uploading data)
- Offer a (SOAP) webservice which returns such a key (like an upload-ticket)
- Offer a (non-SOAP) script where the client can upload data by passing the key, save this data (with the key) in a database
- When the client calls a (SOAP) method with this key, replace the key with the data stored in the database
- 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:
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:
<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:
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:
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
:
public class CredentialHeaderBehavior : IEndpointBehavior
{
private readonly NetworkCredential credential;
public CredentialHeaderBehavior(NetworkCredential credential)
{
this.credential = credential;
}
public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
System.ServiceModel.Dispatcher.ClientRuntime behavior)
{
behavior.MessageInspectors.Add(new CredentialHeaderMessageInspector(credential));
}
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);
}
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:
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