Logaholic.de

Avatar

web development

Zend_MVC, Controller Plugins and Annotations

While it is common practice in many enterprise grade environments to use annotations to not only document but also influence code, I am missing some of this goodness in Zend Framework MVC (1.x) applications.

Recently I had the idea to influence Controller Actions with annotations but discarded it with thoughts like “In PHP I will have to use reflection and some black magic to get this working which will have insane performance hits for my applications”.
… until I set everything up to see that it costs just 1-2ms in average per request without any form of caching.

As example this is code I use to set the used layout script via @layout annotation:

Before:

public function fooAction()
{
    $this->_helpers->layout->setLayout('bar');
    //...
}

After:

/**
 * @layout bar
 */
public function fooAction()
{
    //...
}

Abstract Class (reflection = php5.3) to get the action’s docblock

This needs more work to reflect the configurable module/controller directory and prefixDefaultModule Zend_MVC settings – this may not work for you out of the box.

/**
 * @author Karsten Deubert <karsten@deubert.net>
 */
abstract class My_Controller_Plugin_Annotation_Abstract extends Zend_Controller_Plugin_Abstract
{
    /**
     * @return string
     */
    protected function _getDocblockForControllerAction()
    {
        /** @var $request Zend_Controller_Request_Http */
        $request = $this->getRequest();

        $frontController = Zend_Controller_Front::getInstance();

        /** @var $dispatcher Zend_Controller_Dispatcher_Standard */
        $dispatcher = $frontController->getDispatcher();

        $controllerName =
            ucfirst($dispatcher->formatModuleName($request->getModuleName()))
            .'_'
            .$dispatcher->formatControllerName($request->getControllerName());

        $controllerPath =
            $frontController->getModuleDirectory($request->getModuleName())
            .DIRECTORY_SEPARATOR
            .$frontController->getModuleControllerDirectoryName()
            .DIRECTORY_SEPARATOR
            .$dispatcher->formatControllerName($request->getControllerName())
            .'.php';

        require_once $controllerPath;

        $actionName = $dispatcher->formatActionName($request->getActionName());

        $reflectionClass = new ReflectionClass($controllerName);
        if (!$reflectionClass->hasMethod($actionName))
        {
            return '';
        }

        $reflectionMethod = $reflectionClass->getMethod($actionName);

        $docBlock = $reflectionMethod->getDocComment();
        return $docBlock;
    }
}

Controller Plugin code:

/**
 * @author Karsten Deubert <karsten@deubert.net>
 */
class My_Controller_Plugin_Annotation_Layout extends My_Controller_Plugin_Annotation_Abstract
{
    /**
     * @param Zend_Controller_Request_Abstract $request
     * @return void
     */
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        /** @var $request Zend_Controller_Request_Http */
        if (!$request instanceof Zend_Controller_Request_Http)
        {
            return;
        }

        $docBlock = $this->_getDocblockForControllerAction();

        $pattern = '#@layout (\S+)#iu';

        $matches = array();
        $matchesCount = preg_match($pattern, $docBlock, $matches);

        if (!$matchesCount)
        {
            return;
        }

        $layout = Zend_Layout::getMvcInstance();

        if ($matches[1] === 'disabled')
        {
            $layout->disableLayout();
        }
        else
        {
            $layout->setLayout($matches[1]);
        }
    }
}

I have several Annotation Controller Plugins in place for very different purposes and… love them!

,