ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Migrating to Page Controllers

by Q Ethan McCallum
10/14/2004

Mixed code/HTML tools--PHP, JSP, ASP, and so on--give web developers the power to create dynamic sites with little effort. They also make it easy to slip into bad habits. Consider the all-in-one page, or Model 1 architecture, in which a lone page handles an entire action by itself, typically in poor fashion. An example page might display a form, validate the submission, process the data, and show the user a thank-you message.

Developers can maintain their sanity by refactoring such an app or redesigning it in flight. The refactoring cure for the all-in-one page is the Page Controller. This design pattern is appropriate in places where a single component has the dual responsibilities of logic and display.

This article will explain how Page Controller works, when to use it, and how to refactor your multifunction pages. It includes a sample implementation in PHP, but in true pattern style it's quite portable. Case in point: I first stumbled across the Page Controller pattern in J2EE apps.

Theory

Most design patterns take advantage of the notion of loose coupling, in which an app's components know only enough about one another to do their job. Such modularity permits developers to change or replace components with little impact to any others. The Page Controller pattern achieves loose coupling by distilling an action into elements of logic and presentation.

Consider the flowchart in Figure 1. It describes how an app might pull data from a database and display it for the user. The three diamond shapes indicate opportunities for the process to deviate: when the app connects to the database, when it processes the query, and when it counts the number of results. Each error condition presents the user with a different error page.


Figure 1
Figure 1. A flowchart for a database application. Click on the image for a full-size screenshot.

You could certainly cram the entire flowchart into a single component. That may seem tempting at first, but the complications eventually will outweigh the convenience. Changes to this page may introduce defects in calls to business logic or HTML formatting, and testing just one piece is difficult if not impossible.

By comparison, a Page Controller implementation separates this action according to the dotted lines in the figure: the controller encapsulates the logic (the diamonds on the left), and four separate views handle the presentation (for "success" and the various errors).

The controller acts as a router for the presentation layer, choosing between the views based on the decisions in the diamonds. It can store data, such as the results of a database query, in a known location for the view to fetch and format for the user.

Such a setup is an improvement over a single, all-encompassing page, because changes to any one component affect only those components directly related to it. Components are therefore easier to reuse and to test in isolation.

Structure and Practice

From a code perspective, the controller is the PHP page (or servlet, or CGI, or whatever) called when a user accesses the Uniform Resource Identifier (URI). Implementing a Page Controller is as simple as knowing how to dispatch to a view page and store data.

Dispatching is a language-specific concern. If you're familiar with Java, you may have used RequestDispatcher#forward() to pass control from a servlet to a JSP. PHP developers have include(). That function parses the specified file, so it may include PHP code as well as plain HTML.

A file loaded by include() falls under the same scope as its caller. A variable declared in the controller is visible to the included view page. The sample code uses an array instead of a scalar variable, so view pages find their required data based on a known key in that array.

The following code excerpt is a skeleton of such a controller:

<?php

// ... perform database call
$db        = pg_connect( ... ) ;
$dbResults = pg_query( ${db} , ${dbQuery} ) ;

if( ! ${dbResults} )
{
  // store data where the error page can find it
  // ($viewData is visible in the include()'d page's scope)
  $viewData[ 'ErrorMessage' ] = "Error fetching data" ;

  // dispatch to an error page
  include( 'sql-error.php' ) ;
  return ;

}
else if( 0 == pg_num_rows( ${dbResults} ) )
{
  $viewData[ 'ErrorMessage' ] = "No results from query" ;

  // dispatch to another error page
  include( 'no-results.php' ) ;
  return ;
}
else
{
  // ... convert results into custom objects and store them
  // in $someResults ...
  $viewData[ 'results' ] = ${someResults} ;

  // dispatch to "success" page
  include( 'success.php' ) ;

  // ...
}

?>

All of this dispatching is opaque to end users and the web server software itself: they see only the request URI, which triggers the action behind the scenes.

Migrating to a Page Controller Setup

Converting mixed logic/view pages to their Page Controller equivalents is straightforward. The migration is transparent to users because the original page's URI doesn't need to change unless you want it to.

This section uses a sample page to demonstrate the migration process. Both the "before" and "after" versions (old.zip and new.zip, respectively) are available for download. Feel free to review the files and follow along:

Related Reading

Learning PHP 5
By David Sklar

  1. Draw a map. Draft a flowchart that maps the page's execution into logic and presentation content. Make note of which logic triggers a given set of content, and what dynamic data that content uses.

    The sample code uses the flowchart from Figure 1. The controller (new.php) passes objects generated from database query results (Lines 56-75, then 82) and error messages (Lines 28 and 38) to the views.

  2. Create the views. Extract presentation content into separate pages. The sample code's views are the files new-DBConnectError.php, new-NoResults.php, new-SQLError.php, and new-Success.php. (The view pages may have duplicate content, such as menu bars and footers. Later in the article I offer one solution to this.)

    The remaining code will become the controller. Leaving the controller at the old page's URI makes for a seamless transition, as old links to that URL will continue to work.

    Notice that the sample pages have no logic for error handling. That should take place within the controller itself, which can dispatch to the appropriate error page. (If you haven't done so already, set display_errors = 0 in php.ini to avoid interleaving PHP's code-level error messages with your view pages' content.) View pages should have just enough logic to format the data that the controller provides.

  3. Formalize the controller. Rework the remaining code (the controller) to dispatch to the view pages based on the results of each decision. Place data in a variable that both the controller and views can access.

    The controller in the sample app puts all of the data in the array $viewData. The view pages will call this same variable.

  4. Update the views. Rework the view pages to pull data from variables assigned in the controller.

    For example, note how the new-Success.php page iterates through the array $viewData['CustomerList'] (Lines 33-45). The error pages look for a message under $viewData['ErrorMessage'].

Follow a similar process when designing a new app with Page Controllers in mind.

Growth and Scalability

Converting a large, dynamic web application may leave you with tens or hundreds of Page Controllers. These can be pockets of stability in a larger mess unless you take a long-range view of your design. Consider the following ideas to help scale your application accordingly:

Design controllers to fetch URIs from an app-wide map instead of using direct file paths. That makes the app more adaptable to changes in file structure and makes it easier to debug dispatches to nonexistent pages. The "map" could even be a custom object that returns a predefined error page when someone requests a nonexistent alias.

Identify and eliminate duplicate pages. While the "success" page is closely tied to a specific controller, chances are the error pages are very similar. (The sample code's error pages are near clones of one another.) You can reduce your overall page count by sharing a generic error page that looks for a message in a predefined variable.

Don't stop refactoring now. Converting an existing Model 1 app to use Page Controllers will uncover code duplication. The sample app, for example, could encapsulate its data access in a Data Access Object (DAO) or DataObject.

You can also extract your business processes into objects or function libraries, whittling down your controllers to the bare minimum of code:

<?php

// ... any include() or require() calls ...
$busObj = new BusObj( ... ) ;
$result = ${busObj}->exec( ... ) ;

if( defined( $result ) )
{
  $viewData[ 'Result' ] = $result ;
  include( 'success.php' ) ;
}
else
{
  $viewData[ 'ErrorMessage' ] = "No result" ;
  include( 'error.php' ) ;
}

?>

This level of separation lets you put other faces on your business processing. (Think fat-client GUI or web services.)

Mixing Page Controllers and Front Controller

Those of you who have read my previous article on the Front Controller pattern may wonder when to use that instead of Page Controller. You can (and probably should) use both.

The two patterns coexist peacefully within the same application because they hold different, yet complementary, responsibilities. The Front Controller specifies where to go (the page to fetch for the requested URI), while a Page Controller decides what to do (the action to perform).

Furthermore, they work without the knowledge of one another. The Front Controller doesn't realize it's dispatching to a Page Controller, and the Page Controller doesn't know what called it. To integrate the two, map a Page Controller to one of the Front Controller's target URIs.

One reason to use a Front Controller with a Page Controller is to minimize the duplicated content in the extracted view pages. The Front Controller can set up common menu bars, footers, and so on, while the Page Controller will handle the specific request.

That's a Wrap

Migrating all-in-one pages to Page Controllers is the first step in a top-down refactoring of your web application. Your end users will notice that the app is more stable and that it takes you less time to implement new features. The clear separation between logic and display will make it easier for you to fix bugs and make changes.

Page Controller is a small-scale pattern that can apply to several places within an application. Combine it with large-scale patterns such as the Front Controller to keep your design clean both from far away and up close.

Ethan would like to thank "Mr .NET the SB" for reviewing and improving this article.

Resources

Q Ethan McCallum grew from curious child to curious adult, turning his passion for technology into a career.


Return to the PHP DevCenter

Copyright © 2009 O'Reilly Media, Inc.