Migrating to Page Controllers
by 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. 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.