ExtendedSoapClientService

1. Introduction

The APF ships a custom SOAP client implementation. It is built on the PHP SOAP extension and easily integrates with the APF. Besides, the API of the custom implementation has been straightened to gain ease of use.

The APF SOAP client implementation supports all functionality of PHP's native SoapClient but eases configuration of the ExtendedSoapClientService using the APF DI container.

2. Configuration

The configuration of the ExtendedSoapClientService can be done in two flavours: directly within the code or using the DIServiceManager.

2.1. Configuration by code

Using the ExtendedSoapClientService requires a WSDL and a service location. In order to send a login request you may use the following code:

PHP code
use APF\tools\soap\ExtendedSoapClientService; $service = new ExtendedSoapClientService(); $service->setWsdlUrl('./login-service.wsdl'); $service->setLocation('https://example-com/services/login/soap'); $result = $service->Login(array( 'user' => $user, 'pass' => $pass ));

In the first two lines the initial configuration of the SOAP client is done. This sample assumes the WSDL file located on the local storage. Of course, remote WSDL files are supported.

In case the service endpoint is directly included in the WSDL definition, line 3 may be omitted. Line 5 shows up a sample call of one of the methods defined within the WSDL.

In order to optimize construction time of the ExtendedSoapClientService it is recommended to use local WSDL files. In case this is no option, you should enable the soap.wsdl_cache_enabled option within your php.ini. To get optimal results, soap.wsdl_cache_ttl and soap.wsdl_cache_limit should be set to suitable values.

2.2. Configuration by DIServiceManager

Using the DIServiceManager to create the SOAP client has two main advantages: configuration and application can be separated and MOCK implementations can easily be configured for development and/or test reasons.

In order to use the ExtendedSoapClientService the following (minimal) configuration is necessary:

APF configuration
[LoginService] servicetype = "..." class = "APF\tools\soap\ExtendedSoapClientService" conf.wsdl.method = "setWsdlUrl" conf.wsdl.value = "./login-service.wsdl" conf.service.method = "setLocation" conf.service.value = "https://example-com/services/login/soap"

Having this configuration you are now able to use the LoginService within a controller or any other component of your software as described in the next code box:

PHP code
$service = $this->getDIServiceObject('...', 'LoginService'); $result = $service->Login(array( 'user' => $user, 'pass' => $pass ));

The namespace argument of the getDIServiceObject() call depends on the setup of your application. Details on the configuration of services can be read about in the complex services chapter, Configuration contains hints on the definition of configurations using the APF in general.

Please have a look at the config/tools/soap/EXAMPLE_serviceobjects.ini file within the apf-configpack-*release package for a detailed configuration example.

Further configuration parameters may be adapted using the methods listed in chapter 2.3 in combination with a custom DI service configuration subsection like this:

APF configuration
conf.xyz.method = "..." conf.xyz.value = "..."

2.3. Parameter overview

The following list contains all configuration directives of the ExtendedSoapClientService and the respective methods. The can be used for configuring the client by code or by a DI service definition:

Parameter Method Description
wsdlUrl setWsdlUrl() Using this parameter the service contract (WSDL file) is defined. WSDL files may be located on the local disk or on a remote server.
location setLocation() Defines the url of the service endpoint. May be defined explicitly or within a WSDL file.
login setHttpAuthUsername() Defines the user for HTTP base authentication secured services.
password setHttpAuthPassword() Defines the passwordfor HTTP base authentication secured services.
compression setCompressionLevel() Activates compressed transmission of payload of SOAP requests and responses. Allowed values are the constants SOAP_COMPRESSION_ACCEPT, SOAP_COMPRESSION_GZIP, SOAP_COMPRESSION_DEFLATE, or a combination of them.
connection_timeout setConnectionTimeout() Defines the timout of connections to the SOAP service. Please note, that this value does not avoid issues with slo response times. In order to set the maximum time for SOAP requests, please use the default_socket_timeout directive within your php.ini.
cache_wsdl setCacheWsdl() Activates WSDL caching for remote files. Please note the configuration parameters within your php.ini!
encoding setEncoding() Defines the character set of the applied and returned characters. Default value is UTF-8.
soap_version setSoapVersion() Defines the SOAP version to use. Possible values are the constants SOAP_1_1 and SOAP_1_2.
classmap registerWsdlObjectMapping() Using this method you may define object mappings. It requires an instance of the WsdlObjectMapping class. Details can be read about in chapter 3.3 .
proxy_host setProxyHost() In case SOAP communication is initiated through a proxy you may define the proxy's host name with this method.
proxy_port setProxyPort() In case SOAP communication is initiated through a proxy you may define the proxy's port with this method.
proxy_login setProxyUsername() If the proxy requires authentication, please provide the appropriate user name with this method.
proxy_password setProxyPassword() If the proxy requires authentication, please provide the appropriate password with this method.
user_agent setUserAgent() In order to announce yourself as a particular client, please use this method. The content passing to this method is transferred to the SOAP server by the UserAgent header.
features enableFeature() The PHP SOAP client implementation supports various features. Details can be taken from php.net/soap. Using this method, you can activate one or more features.

Details on the signatures of the above methods can be taken from the API documentation.

The configuration methods support the fluent interface technique. Using configuration by code you may use the following way:
PHP code
use APF\tools\soap\ExtendedSoapClientService; $service = new ExtendedSoapClientService(); $service ->setProxyHost('proxy-server') ->setProxyPort(8080) ->setProxyUsername('foo') ->setProxyPassword('bar');

Details on the features of the entire PHP SOAP extension can be found on php.net/manual/en/soapclient.soapclient.php.

3. Usage

Using the ExtendedSoapClientService you can use two flavours of SOAP communication: XML-based or object-based. The next two chapters describe both ways in detail.

In order to ease the way of using the result from SOAP requests it is recommended to use an object-style communication. This helps you to keep the signatures of your application's methods clean and strongly typed.

3.1. Communication via XML

In case you intend to use the executeRequest() method of the ExtendedSoapClientService communication is done in pure XML. For this approach it is recommended to use SoapUI. With this tool, sample requests can be generated easily from the desired WSDL file. Having finished with testing, you can directly copy the requests to your source code and replace the dynamic values.

executeRequest() takes the SOAP action and a request XML as it's arguments. As as result, you will receive an XML document based on SimpleXMLElement. Example:

PHP code
use APF\tools\soap\ExtendedSoapClientService; $request = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header/> <soapenv:Body> <type:AuthenticateRequest> <type:ConsumerIdentification> <type:ConsumerAuthentication> <type:Principal>...</type:Principal> <type:Credential>...</type:Credential> </type:ConsumerAuthentication> </type:ConsumerIdentification> <type:Authentication> <type:Identification> <type:Alias>...</type:Alias> </type:Identification> <type:Security> <type:SecretType>...</type:SecretType> <type:Secret>...</type:Secret> </type:Security> </type:Authentication> </type:AuthenticateRequest> </soapenv:Body> </soapenv:Envelope>'; $client = new ExtendedSoapClientService(); $client->setWsdlUrl('https://example.com/services/v1?wsdl'); $client->setLocation('https://example.com/services/v1'); $responseXml = $client->executeRequest('Authenticate', $request);

The content of the $responseXml variable (instance of SimpleXMLElement) can be used within your application directly.

3.2. Communication by DTOs

Using the __call() method of the ExtendedSoapClientService you are given the possibility to use the object mapping feature.

The __call() method - or the desired SOAP method respectively - takes an associative array as it's request definition. The result is returned as DTOs (if configured!). Example:

PHP code
use APF\tools\soap\ExtendedSoapClientService; $request = array( 'ConsumerIdentification' => array( 'ConsumerAuthentication' => array( 'Principal' => '...', 'Credential' => '...' ) ), 'Authentication' => array( 'Identification' => array( 'Alias' => '...' ), 'Security' => array( 'SecretType' => '...', 'Secret' => '...' ) ) ); $client = new ExtendedSoapClientService(); $client->setWsdlUrl('https://example.com/services/v1?wsdl'); $client->setLocation('https://example.com/services/v1'); $response = $client->Authenticate($request);

Please note, that the structure of the request array must be equal to the hierarchy of the type definition within the WSDL file. Otherwise, PHP will not assign the values correctly and it comes to an error.

The names of the array offsets must comply with the XSD typ names (see request structure in chapter 3.1).

The content of the $response variable is no an instance of an DTO class. Please note the configuration of object mappings in chapter 3.3.

The request may also be defined as an object structure based on stdClass instances instead of an associative array. The above request then looks like this:
PHP code
$request = new stdClass(); $request->ConsumerIdentification = new stdClass(); $request->ConsumerIdentification->ConsumerAuthentication = new stdClass(); $request->ConsumerIdentification->ConsumerAuthentication->Principal = '...'; $request->ConsumerIdentification->ConsumerAuthentication->Credential = '...'; $request->Authentication = new stdClass(); $request->Authentication->Identification = new stdClass(); $request->Authentication->Identification->Alias = '...'; $request->Authentication->Security = new stdClass(); $request->Authentication->Security->SecretType = '...'; $request->Authentication->Security->Secret = '...';

3.3. Configuration of object mappings

Mapping of XSD types to PHP objects is a powerful tool that in turn has various pitfalls. For this reason the ExtendedSoapClientService exposes a simple configuration mechanism based on the WsdlObjectMapping class:

PHP code
class WsdlObjectMapping extends APFObject { public function __construct($wsdlType = null, $phpClassName = null) { ... } public function setPhpClassName($phpClassName) { ... } public function getPhpClassName() { ... } public function setWsdlType($wsdlType) { ... } public function getWsdlType() { ... } }

Creating a mapping definition the WSDL or XSD type declaration, the namespace of the PHP class, and it's name is necessary.

The name of the type declaration must be the name from the WSDL or XSD type and is not equal to the name of the XML tag of the response. In case the response contains a structure like
XML code
<type:Identification> <type:Alias>...</type:Alias> </type:Identification>
with an underlying type declaration of
XML code
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="MemberIdentificationType"> <xs:complexType> <xs:sequence> <xs:element type="xs:string" name="Alias"/> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
the value of $wsdlType must be MemberIdentificationType. Trying to use Identification or type:Identification mapping will fail.

Adding mapping definitions may be done via service configuration using the DIServiceManager or additionally or exclusively using the registerWsdlObjectMapping() Method within your lines of code.

3.3.1. Configuration via service configuration

The APF DI container is able to configure a particular service using another one. This can be used as a basis for injecting desired mapping definitions into the ExtendedSoapClientService.

In case the the mapping type mentioned in the last chapter should be added to the service, you may want to use the following configuration:

APF configuration
[LoginService] servicetype = "..." class = "APF\tools\soap\ExtendedSoapClientService" conf.wsdl.method = "setWsdlUrl" conf.wsdl.value = "./login-service.wsdl" conf.service.method = "setLocation" conf.service.value = "https://example-com/services/login/soap" init.auth-response.method = "registerWsdlObjectMapping" init.auth-response.name = "VENDOR\..\AuthenticationResponseMapping" [AuthenticationResponseMapping] servicetype = "NORMAL" class = "APF\tools\soap\WsdlObjectMapping" conf.type.method = "setWsdlType" conf.type.value = "AuthenticationResponseType" conf.class.method = "setPhpClassName" conf.class.value = "VENDOR\..\AuthenticationResponse"

The "service" AuthenticationResponseMapping defines an object mapping for the AuthenticationResponseType XSD type using the WsdlObjectMapping class. During initialization of the LoginService the object mapping service is injected and is available during usage. The number of object mapping definitions is not limited.

In order to represent the response of an authentication call using the AuthenticationResponse the class must be created. The class itself is a simple PHP class that contains variables for the content of the answer. The following example assumes that each successful login returns the date of the last login. In case of errors, an exception is thrown. The response XML is as follows:

XML code
<type:AuthenticateResponse> <type:LastLoginDate>...</type:LastLoginDate> </type:AuthenticateResponse>

The signature of the appropriate PHP class is as follows:

PHP code
class AuthenticationResponse { private $LastLoginDate; public function getLastLoginDate() { ... } }

The above sample does not contain complex types. In case a response XML contains complex data structures all levels have to be registered as an object mapping. Further, all DTO classes must be aware of the response structure. This means, that complex data types must be represented by an internal variable within the DTO classes within the AuthenticationResponse.

In case the mapping is not defined for each hierarchy level continuously or no mapping has been defined, PHP returns a structure of stdClass instances.

In case one instance of the ExtendedSoapClientService is used for multiple cases with equal XSD types it is recommended to configure the object mapping via service configuration. Further types may be defined on-demand as described within the next chapter.
3.3.2. Configuration via registerWsdlObjectMapping()

Registering a data type for a SOAP request can be done as follows:

PHP code
use APF\tools\soap\ExtendedSoapClientService; use APF\tools\soap\WsdlObjectMapping; $client->registerWsdlObjectMapping( new WsdlObjectMapping( 'AuthenticationResponseType', 'VENDOR\..\AuthenticationResponse' ) );

$client is an instance of the ExtendedSoapClientService that has been created as described in chapter 2. The namespace of the DTO (second argument of the WsdlObjectMapping) depends on your project structure.

Calling registerWsdlObjectMapping() multiple times adds multiple object mappings. This approach is equal to adding more configuration sections using the service configuration flavour.

4. Examples

4.1. Logging

Consuming SOAP services multiple types of errors may occur: functional and technical. To be well prepared for error analysis on production systems it is recommended to log all reasonable information.

Using the capabilities of the Logger together with the getLastRequest() and getLastResponse() methods of the ExtendedSoapClientService you are able to collect all relevant data. Based on the example described in chapter 3 relevant information can be written to a log file in case of errors like this:

PHP code
use APF\core\logging\Logger; use APF\core\logging\LogEntry; use APF\core\singleton\Singleton; use APF\tools\soap\ExtendedSoapClientService; /* @var $client ExtendedSoapClientService */ $client = $this->getDIServiceObject('...', 'LoginService'); /* @var $logger Logger */ $logger = Singleton::getInstance(Logger::class); try { $response = $client->Authenticate($request); } catch (Exception $e) { $logger->logEntry( 'service_calls', $e->getMessage() . ' Details: ' . $e, LogEntry::SEVERITY_ERROR); }

In case you intend to log every request and response this can be done with the threshold feature of the Logger:

PHP code
use APF\core\registry\Registry; use APF\core\logging\Logger; use APF\core\logging\LogEntry; use APF\core\singleton\Singleton; use APF\tools\soap\ExtendedSoapClientService; /* @var $client ExtendedSoapClientService */ $client = $this->getDIServiceObject('...', 'LoginService'); /* @var $logger Logger */ $logger = Singleton::getInstance(Logger::class); $logTarget = 'service_calls'; $writer = clone $l->getLogWriter( Registry::retrieve('APF\core', 'InternalLogTarget') ); $l->addLogWriter($logTarget, $writer); try { $response = $client->Authenticate($request); $logger->logEntry( $logTarget, $client->getLastRequest(), LogEntry::SEVERITY_TRACE); $logger->logEntry( $logTarget, $client->getLastResponse(), LogEntry::SEVERITY_TRACE); } catch (Exception $e) { $logger->logEntry( $logTarget, $e->getMessage() . ' Details: ' . $e, LogEntry::SEVERITY_ERROR); $logger->logEntry( $logTarget, $client->getLastRequest(), LogEntry::SEVERITY_INFO); $logger->logEntry( $logTarget, $client->getLastResponse(), LogEntry::SEVERITY_INFO); }

The content of the current request and it's corresponding response with the try block is only written to a log file if it matches the defined threshold. Nevertheless, information is logged in case of an error.

In order to have the above behavior the log threshold must be set to

PHP code
$logger->setLogThreshold(Logger::$LOGGER_THRESHOLD_ALL);

for development and to

PHP code
$logger->setLogThreshold(Logger::$LOGGER_THRESHOLD_INFO);

for production environments.

4.2. Retrieve registered types and functions

PHP's SOAP implementation supports displaying the defined data types and methods. This is not only interesting for object mappings but also for debugging purposes to see whether the WSDL has been loaded correctly.

In order to display the defined data types, please use

PHP code
echo $client->getTypes();

To display the registered commands call

PHP code
echo $client->getFunctions();

To improve readability the returned arrays may be formatted using var_dump(), print_r(), oder the APF function printObject().

4.3. Encapsulation of SOAP services

Encapsulation of SOAP services increases maintainability of the software and lets you easily replace SOAP functionality by others. Setting up a multi-tier architecture should thus include an extra layer for external services to separate this specific functionality from the rest of the application. Besides, adding a separate component makes testability much more easy and you are able to change the implementation or the technology on-demand.

To support this approach the APF offers a DI-Containers. It can be used to exchange the implementation by configuration or add a MOCK implementation of desired. In case you create your code for usage with those paradigms and techniques it is more likely that the created code can be validated easily with unit tests.

Another advantage is that the service implementation must not take care of creating and configuring the SOAP client itself. This is all done by the DI container that provides a ready-to-use service to your application.

We are taking the authentication interface as an example for the following chapters. There, we are going to solve the task of providing an authentication service consuming a SOAP interface.

4.3.1. Definition of the interface

The internal representation of the authentication service (="perception" of the service within the application) is defined by the following interface:

PHP code
interface AuthenticationService { /** * @param string $username The user's name. * @param string $password The user's password. * @return bool True in case the authentication succeeded, false if it fails. */ public function authenticate($username, $password); }
4.3.2. Implementation of the service

Based on the interface described within the preceding chapter the service can now be implemented. Since the services uses the ExtendedSoapClientService and is configured via the DO container it must provide a setter to allow injection of the SOAP client.

PHP code
use APF\core\logging\Logger; use APF\core\logging\LogEntry; use APF\core\singleton\Singleton; use APF\tools\soap\ExtendedSoapClientService; use VENDOR\..\AuthenticationService; class AuthenticationServiceImpl implements AuthenticationService { /** * @var ExtendedSoapClientService */ private $client; public function setClient(ExtendedSoapClientService $client) { $this->client = $client; } public function authenticate($username, $password) { /* @var $logger Logger */ $logger = Singleton::getInstance(Logger::class); $request = array( 'ConsumerIdentification' => array( 'ConsumerAuthentication' => array( 'Principal' => '...', 'Credential' => '...' ) ), 'Authentication' => array( 'Identification' => array( 'Alias' => $username ), 'Security' => array( 'SecretType' => 'Password', 'Secret' => $password ) ) ); try { $this->client->Authenticate($request); return true; } catch (\Exception $e) { $logger->logEntry( 'service_calls', 'Authentication failed: ' . $e->getMessage(), LogEntry::SEVERITY_ERROR); return false; } } }
4.3.3. Configuration of the Service

To use the service a configuration for the DI container is necessary. The configuration should both configure the AuthenticationService as well as the SOAP client.

The APF DI container offers the possibility to inject services as resources of other services. In our example the AuthenticationService depends on the SOAP service. Defining the SOAP service as a separate DI service it can be oused throughout the authentication services.

In order to limit the complexity of this example let's assume that both service definitions reside within the same file. Even for production use, this doesn't limit the reusability but increases readability for now.

The configuration of the LoginService is as follows:

APF configuration
[LoginService] servicetype = "SINGLETON" class = "VENDOR\..\AuthenticationServiceImpl" init.soap-client.method = "setClient" init.soap-client.namespace = "VENDOR\.." init.soap-client.name = "SoapService" [SoapService] servicetype = "SINGLETON" class = "APF\tools\soap\ExtendedSoapClientService" conf.wsdl.method = "setWsdlUrl" conf.wsdl.value = "http://example.com/.../soap?wsdl" conf.service.method = "setLocation" conf.service.value = "http://example.com/.../soap"
4.3.4. Usage of the service

Assuming that the configuration resides with the VENDOR\.. namespace, the LoginService may be used as follows:

PHP code
use VENDOR\..\AuthenticationServiceImpl; /* @var $service AuthenticationServiceImpl */ $service = &$this->getDIServiceObject('VENDOR\..', 'LoginService'); if ($service->authenticate($username, $password)) { echo 'Login succeeded!'; } else { echo 'Login failed!'; }

Details on the configuration of the APF can be taken from the Configuration chapter, more information on service configuration using the DI container can be found in chapter Services.

5. Tips and tricks

This chapter contains development experience essences that might help you implementing services with the PHP SOAP extension or point to some common pitfalls.

  • executeRequest() expects a complete XML request: Using the executeRequest() method you have to provide a complete request XML as described in chapter 3.1. This is required by the PHP SOAP extension. Only applying the request body will cause the operation to fail.
  • Mapping of single elements to arrays: In case an XSD schema of the consumed service contains lists that may contain single elements it is recommended to activate the SOAP_SINGLE_ELEMENT_ARRAYS feature. This maps all single elements to lists and there are no issues with foreach loops that complain about invalid input.
  • Inheritence is not implemented: PHP's SOAP extension does not support type inheritance within the XSD schema definition (see <xsd:redefine />). For this reason, a bug has been filed under bugs.php.net.
  • Object mapping can only be used by magic call: The PHP SOAP extension only applies the object mapping using the __call() method of the ExtendedSoapClientService. This is also true for native usage of the SoapClient.

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.