Zend_Soap_AutoDiscover & eAccelerator causes trouble

Last week I used Zend_Soap_AutoDiscover for the first time. It’s pretty awesome, because your entire WSDL file dynamically generated based on docblock reflection. Unfortunately, it all went wrong when I deployed my code to staging. Apparently the issue was caused by eAccelerator.

But first things first: how did I get into trouble, what caused it and how did I solve it?

Docblock comments

One of the methods that stopped working when pushing the code to staging looked a little something like this:

/**
 * This is someFunction that returns Some_Object
 * @param string $arg2
 * @param int $arg2
 * @return Some_Object
 */
public function someFunction($arg1,$arg2)
{
    //Do something here
}

The docblock comment mentioned above is not only convenient when generating phpDocumentor documentation, but the AutoDiscovery service from Zend_Soap uses it

Using Zend_Soap

Now this is what my “soap” action looks like in the “IndexController”:

public function soapAction()
{
    $this->_helper->layout()->disableLayout();
    $this->_helper->viewRenderer->setNoRender(true);
    if($_SERVER['REQUEST_METHOD'] == 'GET') {
        $autodiscover = new Zend_Soap_AutoDiscover();
        $autodiscover->setClass(get_class($this->_service));
        $autodiscover->handle();
    } elseif($_SERVER['REQUEST_METHOD'] == 'POST') {
        $schema = "http";
        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
            $schema = 'https';
        }
        $wsdl = $schema.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
        $server = new Zend_Soap_Server($wsdl,array('cache_wsdl'=>false));
        $server->registerFaultException("Some_Exception");
        $server->setObject($this->_service);
        $server->handle();
    }
}

So when the action gets hit using HTTP POST the SOAP server processes the request. But when the action is hit using HTTP GET, the Zend_Soap_AutoDiscover generates a WSDL file on the fly.

Reflection

Zend_Soap_AutoDiscover uses Zend_Server_Reflection to return the input/output signatures. There’s a lot of code that covers this, but in the end the Zend_Server_Reflection_Function abstract class calls the getDocComment method in the _reflect method.

This is what it looks like:

    /**
     * Use code reflection to create method signatures
     *
     * Determines the method help/description text from the function DocBlock
     * comment. Determines method signatures using a combination of
     * ReflectionFunction and parsing of DocBlock @param and @return values.
     *
     * @param ReflectionFunction $function
     * @return array
     */
    protected function _reflect()
    {
        $function           = $this->_reflection;
        $helpText           = '';
        $signatures         = array();
        $returnDesc         = '';
        $paramCount         = $function->getNumberOfParameters();
        $paramCountRequired = $function->getNumberOfRequiredParameters();
        $parameters         = $function->getParameters();
        $docBlock           = $function->getDocComment();
 
        if (!empty($docBlock)) {
            // Get help text
            if (preg_match(':/\*\*\s*\r?\n\s*\*\s(.*?)\r?\n\s*\*(\s@|/):s', $docBlock, $matches))
            {
                $helpText = $matches[1];
                $helpText = preg_replace('/(^\s*\*\s)/m', '', $helpText);
                $helpText = preg_replace('/\r?\n\s*\*\s*(\r?\n)*/s', "\n", $helpText);
                $helpText = trim($helpText);
            }
 
            // Get return type(s) and description
            $return     = 'void';
            if (preg_match('/@return\s+(\S+)/', $docBlock, $matches)) {
                $return = explode('|', $matches[1]);
                if (preg_match('/@return\s+\S+\s+(.*?)(@|\*\/)/s', $docBlock, $matches))
                {
                    $value = $matches[1];
                    $value = preg_replace('/\s?\*\s/m', '', $value);
                    $value = preg_replace('/\s{2,}/', ' ', $value);
                    $returnDesc = trim($value);
                }
            }
 
            // Get param types and description
            if (preg_match_all('/@param\s+([^\s]+)/m', $docBlock, $matches)) {
                $paramTypesTmp = $matches[1];
                if (preg_match_all('/@param\s+\S+\s+(\$\S+)\s+(.*?)(?=@|\*\/)/s', $docBlock, $matches))
                {
                    $paramDesc = $matches[2];
                    foreach ($paramDesc as $key => $value) {
                        $value = preg_replace('/\s?\*\s/m', '', $value);
                        $value = preg_replace('/\s{2,}/', ' ', $value);
                        $paramDesc[$key] = trim($value);
                    }
                }
            }
        } else {
            $helpText = $function->getName();
            $return   = 'void';
 
            // Try and auto-determine type, based on reflection
            $paramTypesTmp = array();
            foreach ($parameters as $i => $param) {
                $paramType = 'mixed';
                if ($param->isArray()) {
                    $paramType = 'array';
                }
                $paramTypesTmp[$i] = $paramType;
            }
        }
 
        // Set method description
        $this->setDescription($helpText);
 
        // Get all param types as arrays
        if (!isset($paramTypesTmp) && (0 < $paramCount)) {
            $paramTypesTmp = array_fill(0, $paramCount, 'mixed');
        } elseif (!isset($paramTypesTmp)) {
            $paramTypesTmp = array();
        } elseif (count($paramTypesTmp) < $paramCount) {
            $start = $paramCount - count($paramTypesTmp);
            for ($i = $start; $i < $paramCount; ++$i) {
                $paramTypesTmp[$i] = 'mixed';
            }
        }
 
        // Get all param descriptions as arrays
        if (!isset($paramDesc) && (0 < $paramCount)) {
            $paramDesc = array_fill(0, $paramCount, '');
        } elseif (!isset($paramDesc)) {
            $paramDesc = array();
        } elseif (count($paramDesc) < $paramCount) {
            $start = $paramCount - count($paramDesc);
            for ($i = $start; $i < $paramCount; ++$i) {
                $paramDesc[$i] = '';
            }
        }
 
        if (count($paramTypesTmp) != $paramCount) {
            require_once 'Zend/Server/Reflection/Exception.php';
            throw new Zend_Server_Reflection_Exception(
               'Variable number of arguments is not supported for services (except optional parameters). '
             . 'Number of function arguments in ' . $function->getDeclaringClass()->getName() . '::'
             . $function->getName() . '() must correspond to actual number of arguments described in the '
             . 'docblock.');
        }
 
        $paramTypes = array();
        foreach ($paramTypesTmp as $i => $param) {
            $tmp = explode('|', $param);
            if ($parameters[$i]->isOptional()) {
                array_unshift($tmp, null);
            }
            $paramTypes[] = $tmp;
        }
 
        $this->_buildSignatures($return, $returnDesc, $paramTypes, $paramDesc);
    }

Did you spot the $docBlock = $function->getDocComment(); call ?

eAccelerator

So now you know what happens behind the scenes. I admit it was a pretty long intro in order to get to the point and I guess the clue is a short one.

But before we reach the clue, we have to ask ourselves how eAccelerator got involved in all of this? eAccelerator is a PHP optimizer and accelerator. Kinda like Zend Optimizer or APC. It works its way around the overhead that the PHP lexing and compilation stages bring along.

It stores compiled byte code in cache and achieves better performance by skipping the compilation for already compiled (and cached) PHP code. In the process it removes PHP comments for extra performance. Docblocks aren’t just comment blocks, they reflect the API of the function or method. The necessity of it all is caused by the fact that PHP is a loosely typed language.

The clue

But you obviously understand that without having the docblock at runtime, there’s no way to reflect the API. Without this reflection, the autodiscovery process cannot generate a WSDL file and without the WSDL file it isn’t so easy to perform SOAP calls.

Solving it

To solve the issue, I’d just say:

Don’t use eAccelerator

But there are people out there who are smarter than me. In a blog post, Herman Radke explains how the eAccelerator compilation can be tweaked to avoid docblock removal and he also shares a filtering technique that makes eAccelerator skip optimization for certain files.

Make sure you read his blog post about the topic.

One Comment

Leave a Reply

Your email is never shared.Required fields are marked *