Extending class SoapServer (PHP5) for debugging

In one of our large projects the application platform we developed provides a SOAP Service to several external entities (mainly .NET, Java and VisualBasic based, both web application and desktop software). These entities are spread across Europe and involve both internal consumers (departments from our customer) and external consumers (other solution providers that want to dock their solution onto the platform and interact with it remotely through the SOAP interface).

The SOAP Service, a SOAP server based on PHP5’s ext/soap, provided only small debugging capabilities, mainly for some methods during the pre-testing phase that error_log‚ed to a logfile on the server. This was a sufficient short-term solution up-to-date, for eliminating outstanding bugs during development and testing with the SOAP consumers.

However, for long-term reasons (it’s a large multi-year project) and to provide more comfort for our customer (internal monitoring) and the communication from us (the development team) to the external entities (internal/external consumers of the webservice), we decided to improve the debugging capabilities of the SOAP service. It should log the time of the SOAP request (to pinpoint bottlenecks and see if bottlenecks come from routing reasons or from the platform itself), the issued SOAP commands, the full XML that was received and sent and several other information.

For those of you who don’t know, there’s a pretty easy way to achieve this (thanks to Marco for implementing the solution): you can easily extend PHP5’s SOAPServer class and hook into the constructor and handle() method to implement your debugging code. Unfortunately at the moment (does anyone know if there’s perhaps some undocumented function for this? Or does SCA support any of these capabilities out of the box?) we have to preg the submitted SOAP XML to get all information out of it. Below you can find some sample code.

class overloadedSoapServer extends SoapServer
{
    /**
     * Timer object
     *
     * @var Timer
     */
    private $debugTimer = null;
    
    /**
     * Array with all debug values
     *
     * @var array
     */
    protected $soapDebug = array();
    
    /**
     * Constructor
     *
     * @param mixed $wsdl
     * @param array[optional] $options
     */
    public function __construct($wsdl, $options = null)
    {
        $this->debugTimer = new Timer();
        $this->debugTimer->start();
        
        return parent::__construct($wsdl, $options);
    }
    
    /**
     * Store a named value in the debug array.
     *
     * @param string $name
     * @param mixed $value
     */
    private function setDebugValue($name, $value)
    {
        $this->soapDebug[$name] = $value;
    }
    
    /**
     * Returns a value from the debug values.
     *
     * @param string $name
     * @return mixed
     */
    public function getDebugValue($name)
    {
        if (array_key_exists($name, $this->soapDebug)) {
            return $this->soapDebug[$name];
        }
        
        return false;
    }
    
    /**
     * Returns all debug values as array.
     *
     * @return array
     */
    public function getAllDebugValues()
    {
        return $this->soapDebug;
    }
    
    /**
     * Collect some debuging values and handle the soap request.
     *
     * @param string $request
     * @return void
     */
    public function handle($request = null)
    {
        // store the remote ip-address
        $this->setDebugValue('RemoteAddress', $_SERVER['REMOTE_ADDR']);
        
        // check variable HTTP_RAW_POST_DATA
        if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
            $GLOBALS['HTTP_RAW_POST_DATA'] = file_get_contents('php://input');
        }
        
        // check input param
        if (is_null($request)) {
            $request = $GLOBALS['HTTP_RAW_POST_DATA']; 
        }
        
        // get soap namespace identifier
        if (preg_match('°:Envelope[^>]*xmlns:([^=]*)="urn:NAMESPACEOFMYWEBSERVICE"°im',
            $request, $matches)) {
            $soapNameSpace = $matches[1];
            
            // grab called method from soap request
            $pattern = '°<' . $soapNameSpace . ':([^\/> ]*)°im';
            if (preg_match($pattern, $request, $matches)) {
                $this->setDebugValue('MethodName', $matches[1]);
            }            
        }
        
        // anonymize passwords
        $modifiedRequest = preg_replace('°(<password[^>]*>)([^<]*)(</password[^>)°im', 
        '$1' . str_repeat('X', 8) . '$3', $request);
        
        // store the request string
        $this->setDebugValue('RequestString', Util::beautifyXmlString($modifiedRequest));
        
        // store the request headers
        if (function_exists('apache_request_headers')) {
            $this->setDebugValue('RequestHeader', serialize(apache_request_headers()));
        }
        
        // start output buffering
        ob_end_flush();
        ob_start();
        
        // finaly call SoapServer::handle() - store result
        $result = parent::handle($request);
        
        // stop debug timer and store values
        $this->setDebugValue('CallDuration', $this->debugTimer->fetch(5));
        
        // store the response string
        $this->setDebugValue('ResponseString', Util::beautifyXmlString(ob_get_contents()));
        
        // flush buffer
        ob_flush();
        
        // store the response headers
        if (function_exists('apache_response_headers')) {
            $this->setDebugValue('ResponseHeader', serialize(apache_response_headers()));
        }
        
        // grab userid + mandid from session
        [...]
        
        // store additional timer values
        $this->setDebugValue('CallStartTime', $this->debugTimer->getStartTime());
        $this->setDebugValue('CallStopTime', $this->debugTimer->getStopTime());
        
        // return stored soap-call result
        return $result;
    }
}

This small example code utilizes some helper functions like a Timer class, XML string beautifier and the like and enables the SOAP Server itself to save the data into the database later on. The SOAP server code was changed from:

$server = new SoapServer(NULL, array('uri' => 'urn:NAMESPACEOFWEBSERVICE',
'encoding' => SOAP_ENCODED));
$server->setClass('NAMESPACEOFWEBSERVICE');
$server->handle();

to:

$server = new overloadedSoapServer(NULL, array('uri' => 'urn:NAMESPACEOFWEBSERVICE',
'encoding' => SOAP_ENCODED));
$server->setClass('NAMESPACEOFWEBSERVICE');
$server->handle();

Et voilà. From there it’s pretty easy to put on top a small paging application that lets you flip
through all the saved datasets from the DB. With this small effort you get a pretty decent
debugging possibility for SOAP calls to your SOAP service.

Avatar-Foto

Von Björn Schotte

Björn Schotte ist geschäftsführender Gesellschafter der Mayflower GmbH und Senior Consultant im Umfeld von Software- und Agilen Organisations-Themen. Er twittert unter @BjoernSchotte und ist auf Xing sowie LinkedIn erreichbar. Seine Vorträge finden sich bei Slideshare.

7 Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert