Actions, now with parameters!

December 28, 2007

Tutorials, Zend Framework

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

12 Responses to “Actions, now with parameters!”

  1. venkatadry Says:

    SELECT * FROM emp WHERE ename=’smith’ AND id=520

    please tell me how to implement this qury in zend frame work

  2. linde002 Says:

    Because compacted code can make for lousy readability, especially ternary operators (IMHO)

  3. _____anonymous_____ Says:

    Sorry about the errors, like not checking for !isOptional() && empty() before throwing the error — in the compacted code, but you get the picture…

  4. _____anonymous_____ Says:

    Why can’t people learn to write more compact and readable code, like:

    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!

  5. potatobob Says:

    A quick benchmark shows that it was much slower…
    1.5ms standard ZF Action, 6.5ms Custom param func

  6. NirTayeb Says:

    @markusfoss: if you want to avoid the reflection API you can use the ksort function on $params variables and pass it to the function.
    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.

  7. cime Says:

    Hi
    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;
    }
    }

  8. sbarre Says:

    Like Stefan said, including the rest of the dispatch() code in your example would be REALLY helpful..

    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!

  9. sgehrig Says:

    Very nice – thought of something like this some time ago. I resembles the way the net ASP.NET MVC library handles parameters.
    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

  10. markusfoss Says:

    Wount using the reflection API bring in an overhead to handle this. Thinking about the scalablilty if introducing this as a pattern in a webapp (that gains and gains users)

  11. sbarre Says:

    In response to the above post, if you look closely at the code you will see that the foreach() loop is going through the parameters in the order they were returned from the Reflection API call, not from the request, and then the request parameters are searched for a corresponding value and mapped when appropriate.

    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. :-)

  12. maijs Says:

    What happens if parameters in query string are swapped and instead of ?article_id=23&mode=rich the controller would get ?mode=rich& article_id=23?

    First time I encountered this was with CakePHP and I somewhat disliked this approach since PHP does not allow hotswapping arguments in functions.