Zend Developer Zone
Where the ElePHPants come to learn

Zend Developer Zone

Actions, now with parameters!

Incredibly Old Content Warning!

The content you are viewing is more than three years old. It is here for archival purposes. Do not use any advice in this article.

::Jedi Hand Wave:: This is not the information you are looking for. Move along.


Zend_Controller_Action, Now With Parameters!

Brief Introduction for Zend_Controller_Action

Basically, href="http://framework.zend.com/manual/en/zend.controller.html">Zend_Controller_Action
is the parent of all of the controllers in your application. This controller
is what C stands for in href="http://en.wikipedia.org/wiki/Model-view-controller">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:

  1. Make a new controller class that inherits from
    Zend_Controller_Action;
  2. Override the dispatch method;
  3. Get all request parameters;
  4. Get all action method parameters;
  5. Invoke the action and passing the request parameters as actions method
    parameters according to their order and names;
  6. 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 href="http://php.net/reflection">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 href="http://php.net/call_user_func_array">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;
		}
	}
}