Categories


Loading feed
Loading feed
Loading feed

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:

  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 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


Friday, December 28, 2007
REVERSE PARAMETERS
6:43AM PST · maijs
REVERSE PARAMS ARE FINE
8:21AM PST · sbarre
REFLECTION -> OVERHEAD
9:40AM PST · markusfoss
NICE BUT...
1:13PM PST · sgehrig
BAD EXAMPLE CODE!
2:56PM PST · sbarre
Saturday, December 29, 2007
NICE BUT....
6:06AM PST · cime
Sunday, December 30, 2007
THANKS ON THE COMMENTS
2:45AM PST · NirTayeb
PERFORMANCE
2:45AM PST · potatobob
Monday, December 31, 2007
BLOATED CODE
12:49AM PST · Anonymous User [unregistered]
BLOATED CODE: ERRORS
12:53AM PST · Anonymous User [unregistered]
Sunday, January 6, 2008
RE: BLOATED CODE
2:24PM PST · linde002