Zend_Controller actions that accept parameters?

Tags:

In some MVC style frameworks the controller’s action methods take parameters in the method signature, ie. someMethod($userId)

Zend Framework controllers do not – in ZF, you access parameters through the request object in the controller itself.

I thought it would be nice to get the parameters you want in the method, so let’s check out how to do that and even add a little automatic validation along the way.

A typical ZF usage scenario

In a typical case where you need to access some parameters in the request, you would add them into your route:

some/action/parameter/value

or a custom route, which just maps the parameter in place.

Accessing them is relatively simple:

//this is inside a Zend_Controller_Action subclass
public function someAction() {
  $param = $this->_getParam('parameter');
 
  /* some code here */
}

While this is not a bad way of accessing the parameters – you have good control over it, can provide defaults, choose POST etc. – it may not always be very obvious which parameters a specific action requires.

What if the above code looked like this instead:

/**
 * @param int $parameter
 */
public function someAction($parameter) {
  /* some code here */
}

I think this form is a bit better: the method signature tells you right away what parameters it uses, and the docblock also says what type the parameters are supposed to be.

With a few modifications, we can make our custom controller class which works like this!

Modifying Zend_Controller_Action

As usual, we’re not actually going to modify the class itself, but instead we’ll extend it to create a custom class.

I’ll call the class CU_Controller_Parametrized, since the action methods take parameters. Clever huh?

We’re going to override the dispatch() method in Zend_Controller_Action – by doing this, we can modify the behavior it uses to choose the method it calls. Sadly we will have to do some copy-paste coding, as much of the original code is quite necessary.

You can get the source for the class here. Let’s first take a look at the dispatch method.

class CU_Controller_Parametrized extends Zend_Controller_Action {
  public function dispatch($action) {
    // Notify helpers of action preDispatch state
    $this->_helper->notifyPreDispatch();
 
    $this->preDispatch();
    if ($this->getRequest()->isDispatched()) {
      if (null === $this->_classMethods) {
        $this->_classMethods = get_class_methods($this);
      }
 
      // preDispatch() didn't change the action, so we can continue
      if ($this->getInvokeArg('useCaseSensitiveActions') || in_array($action, $this->_classMethods)) {
        if ($this->getInvokeArg('useCaseSensitiveActions')) {
          trigger_error('Using case sensitive actions without word separators is deprecated; please do not rely on this "feature"');
        }
 
        $this->_callAction($action);
      } else {
        $this->__call($action, array());
      }
      $this->postDispatch();
    }
 
    // whats actually important here is that this action controller is
    // shutting down, regardless of dispatching; notify the helpers of this
    // state
    $this->_helper->notifyPostDispatch();
  }
}

This is pretty much the same as in the parent class. Due to the way it was designed, we sadly had to copy paste most of it to change the behavior. The main change is that instead of calling the action directly, we go to _callAction($action);

Let’s look at that now:

protected function _callAction($action) {
  $reflection = Zend_Server_Reflection::reflectClass($this);
  $methods = $reflection->getMethods();
 
  $mtd = null;
  foreach($methods as $m) {
    if($m->getName() == $action)
    $mtd = $m;
  }
 
  if(!$mtd)
    throw new RuntimeException('Method "' . $action . '" not found');
 
  $protos = $mtd->getPrototypes();
  $args = $protos[0]->getParameters();
 
  $parameters = array();
  $basicTypes = array('int','float','string','bool');
  foreach($args as $arg) {		
    $name = $arg->getName();
    $param = $this->getRequest()->getParam($name, null);
    $type = $arg->getType();
 
    if($arg->isOptional() && $param === null) {
      $param = $arg->getDefaultValue();
    }
    elseif($param === null) {
      throw new RuntimeException("Parameter '$name' does not exist");
    }
 
    if(in_array($type, $basicTypes)) {				
      settype($param, $type);				
    }
 
    $parameters[] = $param;
  }
 
  call_user_func_array(array($this, $action), $parameters);
}

This is where most of the processing takes place. We use Zend_Server_Reflection to get a reflection of the class. We then use the reflected class to find our method – assuming it exists – and then loop over each parameter.

In the loop, we get a value for each parameter or throw an exception if it doesn’t exist and was not optional. We also do a simple typecast to the type specified in the phpdoc block – just to reduce the need for other validation.

Usage

Now that we have the action class, we can create parametrized action methods by simply making our controllers extend CU_Controller_Parametrized instead of Zend_Controller_Action.

class ExampleController extends CU_Controller_Parametrized {
  /**
   * @param string $message
   */
  public function helloAction($message) {
 
  }
 
  /**
   * This method has an optional parameter
   * @param int $id
   */
  public function otherAction($id = 0) {
 
  }
}

These actions work exactly the same as you would expect Zend_Controller_Action based ones work – you route to them the same, and you work with views etc. just the same.

The main difference is that for the helloAction, if the request does not contain a parameter called message, you will get an exception. The otherAction method will work without the id parameter, since we defined a default value for it.

Also, if for some reason the type of the id parameter in otherAction is, say a string, it will get casted into an int.

Some more things

This idea could be expanded a little by customizing the dispatcher rather than the controller. By doing this, we could allow a more Zend_Server-like approach: we could define a specific class which gets used for specific controllers.

By mapping specific classes “into” controllers, we could gain some code re-use, but the impelementation of such would be quite tricky.

We could also add some additional validation logic: if we add a @param which is a class, we could have a “binder” which could map parameters from the request into a class instance – for example if we submit a form we could have values like user[name], user******** in the POST, and the binder would map them into an actual object and pass it to the action.

This idea is quite similar to model binders in ASP.NET MVC.

Finally, bear in mind this code is experimental. I do not know about the performance implications caused by reflection, so it may or may not be slower than usual.