Actions, now with parameters!
Zend_Controller_Action, Now With Parameters!
Brief Introduction for Zend_Controller_Action
Basically, Zend_Controller_Action is the parent of all of the controllers in your application. This controller is what C stands for in MVC, a design pattern used lately in web application development, especially in RIA development.
A basic controller class looks like this:
class ArticlesController extends Zend_Controller_Action {
public function init() {
$this->articlesTable = new ArticlesTable(); // ArticlesTable is a subclass of Zend_Db_Table
}
public function indexAction() {
echo "I'm the main action";
}
public function showAction() {
echo "I show your data";
}
}
Actions are the handlers of our http requests. When we access http://your_domain.com/articles/index, we actually call the method indexAction.
If you use the default dispatcher, each action needs to have the Action suffix.
When the controller initializes for each request, it calls the method init; it therefore may be used to initialize resources like database table objects (using Zend_Db_Table) and so on.
Using Query String and Post Data Variables with Actions
Say we need to handle the URL http://your_domain.com/articles/edit?article_id=23&mode=rich. How do we use the variables article_id and mode?
We use the _getParam(key) method of the Zend_Controller_Action class instance (the action is referenced with $this as we are in the class itself) like that:
class ArticlesController extends Zend_Controller_Action {
// ... other actions (index, show, ...)
public function editAction() {
$article_id = $this->_getParam("article_id");
$mode = $this->_getParam("mode");
}
}
In the same way, we use the _getParam(key) for accessing Post data variables as attributes.
The Design Problem
There isn't actually a problem, but I think there is a more elegant way to access these variables: using function parameters. This way we don't need to call _getParam each time we want to access a variable and we can, for example, declare a default value for an optional parameter, or type hinting the parameter's value:
class ArticlesController extends Zend_Controller_Action {
// ... other actions (index, show, ...)
public function editAction($article_id, $mode="text-plain") {
$article = $this->articlesTable->find($article_id);
switch($mode) {
default: case "text-plain":
echo '<textarea>'.$article->content.'</textarea>';
break;
case 'rich':
// Assume the each textarea.RTE selector will be replaced with a tinyMCE editor.
echo '<textarea class="RTE">'.$article->content.'</textarea>';
break;
}
}
}
So how we can make this work? For this purpose we need to take the following six steps:
- Make a new controller class that inherits from Zend_Controller_Action;
- Override the dispatch method;
- Get all request parameters;
- Get all action method parameters;
- Invoke the action and passing the request parameters as actions method parameters according to their order and names;
- Make our application controllers inherit from the controller from Step 1.
The first three steps are easy to do. For the third step we use the _getAllParams method declared in the Zend_Controller_Action class (and therefore inherited by our class). The fourth and fifth steps, however, involve more advanced operations.
I will show the code, then explain what I do:
// 1. Make a new controller class the inherit Zend_Controller_Action
class Action_With_Parameters_Controller extends Zend_Controller_Action {
// 2. Override the `dispatch` method
public function dispatch($action) {
// 3. Get all request parameters
$params = $this->_getAllParams();
// 4. Get all action method parameters
$method_params_array = $this->get_action_params($action);
$data = array(); // It will sent to the action
foreach($method_params_array as $param) {
$name = $param->getName();
if($param->isOptional()) { // Check whether the parameter is optional
// If there is no data to send, use the default
$data[$name] = !empty($params[$name])? $params[$name] : $param->getDefaultValue();
} elseif(empty($params[$name])) {
// The parameter cannot be empty as defined
throw new Exception('Parameter: '.$name.' Cannot be empty');
} else {
$data[$name] = $params[$name];
}
}
// 5. Invoke the action and pass the request parameters as actions method parameters, according to their order and names.
call_user_func_array(array($this, $action), $data);
}
private function get_action_params($action) {
$classRef = new ReflectionObject($this);
$className = $classRef->getName();
$funcRef = new ReflectionMethod($className, $action);
$paramsRef = $funcRef->getParameters();
return $paramsRef;
}
}
To get all action parameters without knowing their signature, we need to inspect the declaration of the actions. For this purpose we have the Reflection mechanism. PHP 5 comes with a complete reflection API that adds the ability to reverse-engineer classes, interfaces, functions and methods as well as extensions. Additionally, the reflection API also offers ways of retrieving documentation comments for functions, classes and methods.
So I use the ReflectionMethod to inspect into our action method, and I retrieve the ReflectionParameter array with the getParameters method of the ReflectionMethod instance. ReflectionParameter has some properties about the held parameter, like its name, default value, whether it's optional and so on.
After I get the ReflectionParameter array, I iterate on it and check whether each parameter is optional. If so, I check for the default value unless there is a matched request value; if not, I check for a matched request value, and if there is none, throw an exception.
Then, I invoke the action with call_user_func_array method whose first paramter is the method we want to run and its context, and whose second parameter is the parameters for that method as an array.
Now, our controller should look like this (step 6):
class ArticlesController extends Action_With_Parameters_Controller {
// ... other actions (index, show, ...)
public function editAction($article_id, $mode="text-plain") {
$article = $this->articlesTable->find($article_id);
switch($mode) {
default: case "text-plain":
echo '<textarea>'.$article->content.'</textarea>';
break;
case 'rich':
// Assume the each textarea.RTE selector will be replaced with tinyMCE editor.
echo '<textarea class="RTE">'.$article->content.'</textarea>';
break;
}
}
}

Comments
First time I encountered this was with CakePHP and I somewhat disliked this approach since PHP does not allow hotswapping arguments in functions.
This ensures that the array that is ultimately passed to your action method has the parameters in the correct order.
I had a similar system in place using the Reflection API in my custom-built controller before I started working with the Zend Framework and decided to let better programmers do the heavy lifting in my application. :-)
But for a complete solution the complete dispatching process of Zend_Controller_Action should be included in the dispatch() method:
- notify helpers of dispatch ($this->_helper->notifyPreDispatch();)
- call controller's preDispatch ($this->preDispatch())
- check isDispatched() to allow for skipping of current action (if ($this->getRequest()->isDispatched())...)
- run actual action method (call_user_func_array(array($this, $action), $data))
- call controller's postDispatch($this->postDispatch())
- notify helpers of postDispatch ($this->_helper->notifyPostDispatch())
But very nice idea indeed - thank's a lot!
Best regards
Stefan
I am a little bit new to the Zend Framework, and I liked your idea so I implemented it, and then spent a good hour trying to figure out why my output wasn't displaying, until I realized that your dispatch() method was missing very important parts like all the pre/post calls and the helper notifiers..
ViewRenderer wasn't being notified so my output wasn't happening!
Stefan you mean something like this:
<?php
// 1. Make a new controller class the inherit Zend_Controller_Action
class Zend_Controller_WP_Action extends Zend_Controller_Action {
// 2. Override the `dispatch` method
public function dispatch($action) {
$this->_helper->notifyPreDispatch();
$this->preDispatch();
if ($this->getRequest()->isDispatched()) {
// 3. Get all request parameters
$params = $this->_getAllParams();
// 4. Get all action method parameters
$method_params_array = $this->get_action_params($action);
$data = array(); // It will sent to the action
foreach($method_params_array as $param) {
$name = $param->getName();
if($param->isOptional()) { // Check whether the parameter is optional
// If there is no data to send, use the default
$data[$name] = !empty($params[$name])? $params[$name] : $param->getDefaultValue();
} elseif(empty($params[$name])) {
// The parameter cannot be empty as defined
throw new Exception('Parameter: '.$name.' Cannot be empty');
} else {
$data[$name] = $params[$name];
}
}
// 5. Invoke the action and pass the request parameters as actions method parameters, according to their order and names.
call_user_func_array(array($this, $action), $data);
$this->postDispatch();
}
$this->_helper->notifyPostDispatch();
}
private function get_action_params($action) {
$classRef = new ReflectionObject($this);
$className = $classRef->getName();
$funcRef = new ReflectionMethod($className, $action);
$paramsRef = $funcRef->getParameters();
return $paramsRef;
}
}
Now you only need to write your function parameters by A-Z order.
@Stefan: You are right, and i'm sorry for that.
I'm a basic user of Zend Framework, and i not use already this class with View or any other helper so i don't know about that.
I can't edit the article now, after it had published.
1.5ms standard ZF Action, 6.5ms Custom param func
foreach($method_params_array as $param) {
$name = $param->getName();
if(empty($params[$name])) {
throw new Exception('Parameter: '.$name.' Cannot be empty');
}
$data[$name] = $param->isOptional() && empty($params[$name]) ? $param->getDefaultValue() : $param[$name];
}
Instead of bloated stuff like:
foreach($method_params_array as $param) {
$name = $param->getName();
if($param->isOptional()) { // Check whether the parameter is optional
// If there is no data to send, use the default
$data[$name] = !empty($params[$name])? $params[$name] : $param->getDefaultValue();
} elseif(empty($params[$name])) {
// The parameter cannot be empty as defined
throw new Exception('Parameter: '.$name.' Cannot be empty');
} else {
$data[$name] = $params[$name];
}
}
Get rid of the unnecessary conditional blocks!