Redirecting in the View

Although this is not a common practise in developing with Zend Framework, I see a couple of benefits in doing your redirects in the View part of a MVC framework. In this blog post I present an idea why it might be wrong to make your redirects in the controller layer and what you gain if you write them in your view scripts instead.  This could be a bit touchy subject and I don't in any way insist that I am right in this, but let me explain myself a bit more before burning me as a witch :)

The program flow..

Let's take a look what a typical application does. The Action Controller recieves the request, reads data from the Request object, validates parameters and passes the validated data to the Model. Data read from the Model is then forwarded to the View layer. The request might have came anywhere, from an HTML form, Javascript function, RSS reader etc. The controller also manages the program flow by manipulating the Request object and in many examples also performing HTTP redirects (using the Redirector Action Helper).

The View layer is the part of the MVC framework that determines how the data is pushed to the Response object. It pulls the data from the view variables and outputs them to the requesting user in the way the user expects it. Using the ContextSwitch Action Helper, the application can automatically select the right view script for the type of the data it needs to render. So whether we're packing the Response with HTML, JSON or XML data, it is done in the View script.

.. and how it could be changed

The Controller does not need to know how the call to the requested action was made - it just needs to do its thing and let the View layer handle the rest.

An HTTP redirect is done by manipulating the Response object, which IMHO should be done in the View layer. The Action Controller controls the program flow within the dispatch loop by manipulating the Request object and should not bother itself with View related things. This leads to stronger separation of the Controller and View components.

The problem of several request sources

In some cases it is practical that the same Action Controller can accept requests from several "clients", for example a Javascript component located on your page, a dedicated form or a client using a RESTful interface. I use a login feature as an example, you might want that your site has:

  • A login component performing Ajaxy-type login. Let's say that your site has some neat features where you need to log in your users in the middle of some process (say, buying products from a Web store). The front end component (shopping cart) recieves a notification of a successfull login from the component responsible for authenticating the user using an AJAX request to the server.
  • A dedicated login form for other situations or clients that don't understand Javascript that well. In this case the application presents the user a form, authenticates and does a redirect.

In this case, whenever a form is posted, you'd probably check that the credentials are correct and then log the user in. The tricky part begins here: if the user came via form, you'd probably want to redirect him to the page they came from. The AJAX call does not need redirecting, so you'd need to provide the calling JS function some information on how the login process went in a JSON string.

This gets a bit difficult if you have implemented your redirection in the Controller layer, since you need first to determine where the call came from (Zend_Controller_Request_Http::isXmlHttpRequest()) and then act accordingly (forward or return JSON). To accomplish this you would either put an if..else to your authentication action or just write two separate Controllers.

Solving the problem

We'll use the available tools to solve the problem described above. The ContextSwitch Action Helper provides a way of selecting the correct view script automatically based on the context of which the action was called. This enables us to remove the if...else control statement and subsequent redirection from the controller. The controller is now only responsible for logging in the user, writing the results of the process in the view variables and rendering the selected view script.

The view script for Ajax requests checks from the view variables, if the log in was a success and renders a JSON response.

The view script for our traditional form based log in passes the status information to the flashMessenger helper (yes, I am well aware it is an action helper - I'm just calling it from the view layer) and then does the redirect. On the forwarded page, the user is presented the login status message.

Since the View layer does not have any tools for manipulating flashMessenger or redirecting the user, I implemented view helper proxy classes for both.

Summary

The separation explained above has several benefits:

  • Easier way of implementing re-usable actions when using several ways of interracting with a single controller (AJAX, REST, form posts etc). If the design is correct, you could even execute the same action through a command line interface - but this is a bit far fetched situation.
  • The use of the flashMessenger (messages are part of the View layer, right?) in combination with a redirect gets more logical
  • Semantics: View should be responsible of the View layer, which in this case is populating the HTTP response. The controller layer should be totally non-aware of the way the Response is treated.

If using the Redirector Action Helper, you should also mind the fact that the default functionality is to exit after setting the Location header. This interrupts the program flow, because the exit is forced and no plugin hooks get executed (postDispatch, dispatchLoopShutdown etc).

The idea mentioned here is not the only solution to the problem, I am pretty certain that there are other ways to accomplish the same. Regarding the example provided, it probably is possible to send both the HTTP Location header and the JSON payload in the same request and let the front end code just ignore the header, or just do add an http-equiv metatag to the document.