Logaholic.de

Avatar

queer as code!

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!

Bookmark and Share

Related posts:

  1. How to add transparent SoapHeader authentication as decorator to any existing service class

One Comment, Comment or Ping

  1. I think that much better and cleaner solution would to be use action helpers instead of controller plugins for that purpose, as they got more natural and native access to the current action controller, through their $_actionController member. And because ReflectionClass constructor also accepts object, you can remove all that coding related to building that controller path, and obtain your reflection class object by simply doing this:
    $reflectionClass = new ReflectionClass($this->_actionController);

    And of course, action helpers can also be hooked into the pre/post dispatch workflow, by overriding preDispatch() and/or postDispatch() methods.

Reply to “Zend_MVC, Controller Plugins and Annotations”