In previous chapters, we explored techniques for writing highly maintainable, reusable, and reliable HTML, CSS, and JavaScript. In this chapter, we explore techniques for binding together these disparate technologies to assemble complete pages. To do this, we’ll look at a large web application in terms of two deceptively simple yet powerful abstractions: modules and pages. A module is a self-contained component of the user interface that encompasses everything needed (e.g., the HTML, CSS, and JavaScript) to make an independently functioning and cohesive unit that you can use in a variety of contexts across various pages. A page, from the point of view of this chapter, is the canvas responsible for assembling a collection of modules so that they work together within a single context.
This chapter presents PHP as the implementation language for classes to represent pages and modules in large web applications. However, as mentioned in Chapter 1, all of the concepts presented here are relatively easy to transfer to other object-oriented, server-side scripting languages as well. Object orientation provides a more structured, extensible alternative to building pages than using a purely procedural approach. Fortunately, PHP 5 (and to a lesser extent PHP 4) offers a rich set of object-oriented features. Object orientation is an important part of achieving Tenet 7, as well as Tenet 6, from Chapter 1:
Tenet 7: Pages are constructed from highly reusable modules that encapsulate everything required (e.g., HTML, CSS, JavaScript, and anything else) to make each module an independently functioning and cohesive unit that can be used in a variety of contexts across various pages.
Tenet 6: Dynamic data exchanged between the user interface and the backend is managed through a clearly defined data interface. Pages define a single point for loading data and a single point for saving it.
We begin this chapter by introducing a skeleton implementation of a modular web page using a PHP class. It includes loading and saving data and creating content as a set of modules. Next, we explore the interfaces and implementations for some classes that represent various types of pages and modules. We then examine some real examples of modules, including modules for a slideshow and special modules that act as reusable layouts and containers for other modules. Finally, we look at special considerations for working with modules and pages, including handling variations of the same module, placing multiple instances of a module on a single page, generating dynamic CSS and JavaScript, and implementing nested modules.
A modular web page contains many potentially reusable pieces that interact in predictable ways when used together. Our goal is also to make it as simple as possible to create a page. When you implement a page as a nicely encapsulated class, you don’t need much in your index.php file (or your index.html file if your server is configured to run .html files as PHP), as Example 7-1 shows. The class for the page is included from a file called index_main.inc, which resides at the same point in the directory structure as index.html or index.php.
<?php require_once(".../index_main.inc"); $page = new NewCarSearchResultsPage(); $body = $page->create(); print($page->get_page()); ?>
As you can see, the create
method, a
factory method in design pattern
parlance, does most of the work. The create
method assembles the content that goes in
the body
tag for the page and
stores it in the page object (it also returns it). The get_page
method is then
responsible for doing the final assembly of the page by marrying its body
content with everything else a page requires to be complete. Since the
steps executed by create
and get_page
are the same for most pages, both
methods are good candidates to implement in a base class for all pages.
Later, we’ll define a base class for all pages called Page
.
Although the steps performed by create
and get_page
are the same for each page, the
specific items that go into each step differ, of course. To define how to
carry out each of these steps for a specific page, such as a page for new
car search results, you derive your own page class from Page
and implement several methods that create
and get_page
call at the appropriate moments.
The PHP that you’ll see in a moment to generate a page looks very different from the PHP code that most web developers are used to. When web developers build a page in a brute force manner, loading each element in order, they tend to just print strings and variables that contain the desired HTML. This chapter presents one approach to generating more structured pages using object orientation.
The Page
base class performs
the main tasks that all pages require: aggregating the HTML, CSS, and JavaScript from modules on
the page and wrapping the page with the usual other tags (title
, head
, etc.). Each specific page class that you
derive from Page
creates the modules
needed to build a page piece by piece. For each module in your
application, you derive a module class from Module
and implement methods that return the
HTML, CSS, and JavaScript for just that module. Each module knows what
it needs to function, such as the CSS to set the font and the JavaScript
to animate a unique element on the page.
The create
method for the page
sets the process of generating the page in motion. Although we won’t
explore the complete code for create
until later, some of the key tasks that create
performs are:
Calling save_data
, which
you define in your own page class, if needed, as the single point at
which to save data to the backend.
Calling load_data
, which
you define in your own page class, if needed, as the single point at
which to load data from the backend.
Calling get_content
, which
you define in your own page class as the single point at which to
return the main content for the page.
You create the modules for a page in its get_content
method. To create a module, call
its create
method, just as for
creating pages. To use data from the backend in your modules, pass data
retrieved via load_data
into the
module’s constructor.
The create
method for a module
performs two very important tasks: it returns the HTML markup for the
module, which you insert into the appropriate place within the overall layout for the page,
and it adds to the page any CSS and JavaScript that the module requires.
Modules are able to add CSS and JavaScript to a page because they store
a reference to the page on which they reside. The reference is passed to
the module when it is constructed by the application and stored in its
$page
member.
Using the $page
member that
every module contains, modules add CSS files to the page by doing the
following:
$this->page->add_to_css_linked($this->get_css_linked());
Using a similar approach via the $page
member, modules add JavaScript files to
the page by doing the following:
$this->page->add_to_js_linked($this->get_js_linked());
Here, we’ve explained just enough of the mechanics of these object-oriented structures to let you see past them to the main goal. The key idea is that all parts of a module’s implementation, including its CSS and JavaScript, need to travel as a neatly encapsulated bundle wherever the module is used.
In the rest of this chapter, we’ll explore more of the details about how this object-oriented approach works. For now, Example 7-2 shows the implementation of a simple web page using the concepts just described.
<?php require_once(".../common/sitepage.inc"); require_once(".../common/navbar.inc"); require_once(".../common/subnav.inc"); require_once(".../common/nwcresults.inc"); ... require_once(".../layout/resultslayout.inc"); ... require_once(".../datamgr/nwcqueries.inc"); require_once(".../datamgr/nwclistings.inc"); ... class NewCarSearchResultsPage extends SitePage { ... public function __construct() { parent::__construct(); // Do whatever is needed to set up the page class at the start. // This often includes calling methods to process URL arguments. ... } public function save_data() { // If your page needs to save data to the backend, instantiate // the data managers you need (see Chapter 6) and call set_data. $dm = new NewCarQueriesDataManager(); // The class members for saving are provided by the Page class. // Set them as needed to use the data manager and call set_data. ... $dm->set_data ( $this->save_args["new_car_queries"], $this->save_data["new_car_queries"], $this->save_stat["new_car_queries"] ); // Check the status member and handle any errors. Errors often // require a redirect to another page using the header function. if ($this->save_stat != 0) header("Location: ..."); ... } public function load_data() { // If your page needs to load data from the backend, instantiate // the data managers you need (see Chapter 6) and call get_data. $dm = new NewCarListingsDataManager(); // The class members for loading are provided by the Page class. // Populate them as needed by the data manager and call get_data. ... $dm->get_data ( $this->load_args["new_car_listings"], $this->load_data["new_car_listings"], $this->load_stat["new_car_listings"] ); // Check the status member and handle any errors. Errors often // require a redirect to another page using the header function. if ($this->load_stat != 0) header("Location: ..."); ... } public function get_content() { // Create a module for the navigation bar to place on the page. $mod = new NavBar ( $this, ... ); $navbar = $mod->create(); // Create a module for the sub navigation to place on the page. $mod = new SubNav ( $this, ... ); $subnav = $mod->create(); // Create a module for showing new car search results. This module // uses the dynamic data loaded earlier by the load_data method. $mod = new NewCarSearchResults ( $this, $this->load_data["new_car_listings"] ); $search = $mod->create(); // There would typically be several other modules to create here. ... // Place the HTML markup for each module within the page layout. $mod = new ResultsLayout ( $this, array($navbar, $subnav, ...), array($search), array(...), array(...), array(...), array(...) ); // Return the content, which the create method for the page uses. return $mod->create(); } ... } ?>
Example 7-2 also illustrates the goal that using a module on a page should be easy. To this end, only a single include file is required for each module, the data for each module flows through a clearly defined interface in the constructor, and the creation of each module follows a clear and consistent pattern.
The first point about Example 7-2 requiring only a single
include file for each module is key for encapsulation. Just as the
implementation of NewCarSearchResultsPage
includes only the
files it needs for its components (e.g., specific data managers,
specific modules, etc.), the files required for the implementation of a
module should be included by that module itself. This way, its
implementation details are hidden from users of the module. Using
require_once
is
important so that a file included by multiple, nicely encapsulated
implementations is included wherever necessary, but never more than
once.
Research conducted with real pages at Yahoo! showed no significant change in overall performance when pages redeveloped using object-oriented PHP were compared against original versions of the same pages implemented without it. Even should you experience a slight increase on the server, be sure to consider the benefits you’ll achieve from better software engineering, and remember that most of the overall latency for a page comes from downloading components in the browser (see Chapter 9).
As mentioned earlier, a page, from the point of view of this chapter, is the canvas responsible for assembling a collection of modules so that they work well together within a single context. Because most pages perform a similar set of tasks, it’s useful to define a base class that provides a minimum set of capabilities for all pages. For example, all pages fundamentally need a way to save and load data, define content, and assemble the page’s components, among other things.
In this section, we’ll take a closer look at Page
, the base class that performs tasks that
are common for all pages. Although it’s not hard to imagine features
beyond those presented here for such a class, the example provides a good
starting point for many large web applications. We’ll explore the class by
examining its public interface, abstract interface, and implementation
details.
The public interface for Page
consists of methods for which most pages
can benefit from a default implementation. For example, the public
interface for Page
provides methods
for assembling a page as well as managing the CSS and JavaScript for the
page overall. It’s worthwhile to take a moment to observe carefully how
the methods in this class are implemented because these provide a
high-level definition of the steps that allow pages to be assembled in a
modular fashion.
The methods for working with the structure and assembly of a page let you generate the body of the page, assemble the final page, and get some individual tags for the document type, title, and various metadata about the page:
create()
Creates the body for the page and returns the HTML
markup for the body
tag. The
body is returned so that pages that would prefer to assemble
themselves rather than calling get_page
have
that option. In the process, create
performs several important
tasks. These include, in order, registering links (see register_links
), saving and loading
dynamic data, setting various parameters for the page, setting
up the CSS and JavaScript common to the entire site, and getting
the site header, content, and footer. Saving data, if needed, is
performed before loading, because a problem encountered when
saving often means the page should redirect itself or load
different data. If your application differs from this model, you
can always override create
.
The create
method performs
most of its tasks by calling methods from the abstract interface
(see Abstract Interface for the Page Class).
get_page()
A convenience method for assembling the final page. Call
get_page
anytime after
create
has been called. To
display the page, simply print what get_page
returns. This method calls
the next three methods to get the critical elements at the start
of a page—the document type, meta
tags, and title, respectively.
Because they are public methods, developers who are assembling
the final page without the help of get_page
can call them
directly.
get_doctype()
Gets the document type for the page. The default implementation returns the HTML 4.01 Strict DTD document type, but you can override this however you wish.
get_meta()
Gets the meta
tags for the page. The default implementation returns several
meta
tags, but you can
override this method to return whichever tags you desire.
get_title()
Gets the title for the page. The default
implementation returns the title that you’ve set for the page
wrapped in a title
tag.
These methods in the public interface let modules add
the CSS that each requires to the page as links to CSS files or embedded CSS. There is also a
method to get the entire block of CSS assembled for the page, which
includes all CSS links and embedded CSS. If PHP supported C++’s
concept of friends of classes, the two methods for adding CSS would
not be necessary because the Module
base class could provide an implementation for adding CSS
itself:
add_to_css_linked($keys)
Adds links for CSS files to the set of CSS links
for the page. $keys
must
contain an array of keys defined in register_links
(described later). Each
link is added when the first module requests it; subsequent
requests are ignored to prevent duplicates.
add_to_css($css)
Adds the text in $css
to the string of embedded CSS for
the page.
get_all_css()
Gets the entire block of CSS for the page. The various forms of CSS are given the following order:
CSS links specified by get_css_common
(for the global CSS
files)
CSS links specified by the page class (see get_css_linked
)
Embedded CSS specified by the page class (see get_css
)
CSS links added by modules (see get_css_linked
in Public Interface for the Module Class)
Embedded CSS added by modules (see get_css
in Public Interface for the Module Class)
The CSS for modules appears in the order in which each
module was created. This ordering works well and is
deterministic; however, you can always override it by providing
an alternate implementation for get_all_css
in a derived page
class.
These methods in the public interface let modules add
the JavaScript that each requires to the page as links to JavaScript
files or embedded JavaScript. There are also methods to get the entire
block of JavaScript assembled for the page, which includes all
JavaScript links and embedded JavaScript, and to set a flag that
causes the JavaScript to be placed at the top of the page instead of
the bottom. As we mentioned for CSS, if PHP supported C++’s concept of friends of classes,
the two methods for adding JavaScript would not be necessary because
the Module
base class could provide
an implementation for adding JavaScript itself:
add_to_js_linked($keys)
Adds links for JavaScript files to the set of
JavaScript links for the page. $keys
must contain an array of keys
defined in register_links
(described later). Each link is added when the first module
requests it; subsequent requests are ignored to prevent
duplicates.
add_to_js($js)
Adds the text in $js
to the string of embedded
JavaScript for the page.
get_all_js()
Gets the entire block of JavaScript for the page. The various forms of JavaScript are given the following order:
JavaScript links specified by get_js_common
(for the global
JavaScript files)
JavaScript links specified by the page class (see
get_js_linked
)
Embedded JavaScript specified by the page class (see
get_js
)
JavaScript links added by modules (see get_js_linked
in Public Interface for the Module Class)
Embedded JavaScript added by modules (see get_js
in Public Interface for the Module Class)
The JavaScript for modules appears in the order in which
each module was created. This ordering works well and is
deterministic; however, you can always override it by providing
an alternate implementation for get_all_js
in a derived page
class.
set_js_top()
Sets a flag to indicate that get_page
should
place all JavaScript at the top of the page. The get_page
method normally places
JavaScript at the bottom for better performance; however, for
some pages, you may want an easy way to change this placement
(for example, where JavaScript is needed for the primary call to
action on the page).
The abstract interface for Page
consists of methods that we expect
various types of pages to need and that each subclass of Page
can implement as needed. The Page
class calls upon these methods at the
appropriate moments, primarily via the create
method. Because Page
provides empty implementations for each
of the methods, a class derived from Page
is not required to implement all of the
methods in the abstract interface; it implements only the methods that
it requires. For example, if a page doesn’t have any data to save, it
doesn’t have to provide an implementation for save_data
. The simplest pages may implement
little more than just the get_content
method.
The methods in the abstract interface for managing CSS let you link CSS files that most pages have in common across an entire web application, as well as CSS files or embedded CSS to use in specific pages:
get_css_common()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for the
common CSS files to link across all pages in your entire web
application. You normally define this method in the base class
from which you will derive all the pages in your entire
application (see Defining a sitewide page class). This is a good
place to include the CSS for browser resets (see Chapter 4), font normalization (see Chapter 4), and certain highly standardized
elements (e.g., links), for example.
get_css_linked()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for
additional CSS files to link, beyond what the modules on the
page specify. Define this method for specific pages or in the
base class from which pages within a certain section of your
entire web application will be derived (see Defining sectional page classes).
get_css()
Implement this method to return a string of CSS to embed on the page. This method generally is useful for embedding a small amount of CSS on a specific page in order to affect the styling of a module outside its borders (see Scoping at the page level in Chapter 4), or to apply other very minimal stylistic changes to one instance of a module within the context of a specific page.
The methods in the abstract interface for managing JavaScript let you link JavaScript files that most pages have in common across an entire web application, as well as JavaScript files or embedded JavaScript to use in specific pages:
get_js_common()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for the
common JavaScript files to link across all pages in your entire
web application. You normally define this method in the base
class from which you will derive all the pages in your entire
application (see Defining a sitewide page class). This is a good
place to include the JavaScript required for site analytics on
all pages, for example.
get_js_linked()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for
additional JavaScript files to link, beyond what the modules on
the page specify. Define this method for specific pages or in
the base class from which pages within a certain section of your
entire web application will be derived (see Defining sectional page classes).
get_js()
Implement this method to return a string of JavaScript to embed on the page. This method generally is useful to embed a small amount of dynamically generated JavaScript in a specific page. This JavaScript is often needed to initialize or stitch together the layer of behavior for modules.
The methods for dynamic data management provide a single interface in your program for loading data from the backend and a single interface for saving data that the backend needs to store:
load_data()
Implement this method to instantiate data managers
to load data for the page from the backend (see Chapter 6). Call get_data
for each data manager. You
typically implement load_data
only in the page class for a specific page. The class Page
defines the following data members:
$load_args
The arguments passed to data managers when loading data
$load_data
Where data managers store data
$load_stat
Where data managers record their status when loading data
Your load_data
method
should be capable of handling errors based on the value passed
back in $load_stat
. On
return, the $load_data
data
member contains all data loaded from the backend.
save_data()
Implement this method to instantiate the data
managers to save data for the page within the backend (see Chapter 6). The behavior for saving mirrors
the behavior just described for loading. Call set_data
for each data manager. You
typically implement save_data
only in the page class for a specific page. The class Page
defines the following data
members:
$save_args
The arguments passed to data managers when saving data
$save_data
Where data managers read the data to be stored
$save_stat
Where data managers record their status when saving data
Your save_data
method
should be capable of handling errors based on the value passed
back in $save_stat
.
The focus of any web page, of course, is its content.
Since most large web applications have a standard header and footer
across the top and bottom of all pages, respectively, our Page
class provides methods for managing the
header and footer separately from the main content:
get_header()
Implement this method to return the HTML markup for the header of a page.
The create
method places the
header immediately before the content. You typically implement
this method in the sitewide page class (see Defining a sitewide page class) for all pages
across the site and override it in derived classes for just the
pages on which you need a different header.
get_footer()
Implement this method to return the HTML markup
for the footer of a page. The create
method places the footer
immediately after the content. You typically implement this
method in the sitewide page class (see Defining a sitewide page class) for all pages
across the site and override it in derived classes for just the
pages on which you need a different footer.
get_content()
Implement this method to return the HTML markup for the content of a page. You typically implement this method in just the page classes for specific pages since the content for every page is different. This is the method in which you normally create most modules. You place the HTML markup that each module returns into a layout whose markup is ultimately returned by this method.
The remaining methods defined by Page
manage general information about a
page. These methods set the title and meta information for a page, set a CSS ID for the body,
and register keys for links to CSS and JavaScript files. The CSS ID is
useful for creating individual namespaces for pages (see Scoping at the page level in Chapter 4). The use of keys for links to CSS and
JavaScript files in a large web application centralizes the management
of filenames, versioning of CSS and JavaScript for caching (see Chapter 9), and switching between local and
also-known-as paths to support different locations for different
environments (e.g., production versus development):
set_title()
Implement this method to set the $title
data member. This member is
used to construct a title
tag
when get_title
is
called.
set_meta()
Implement this method to set the $equiv
, $desc
, and $keywd
data members. These members are
used to construct several meta
tags when get_meta
is called. You can also use
this method to set additional members in your derived class and
have your own implementation of get_meta
build appropriate tags for
them to be placed at the top of the page.
set_css_id()
Implement this method to set the $css_id
data member. This member is
used in get_page
when adding
the CSS ID to the body
tag.
register_links()
Implement this to set up the $css_linked_info
and $js_linked_info
data members. These
members are used to resolve keys for CSS and JavaScript files
into paths to files that can be linked. You typically implement
this method in your sitewide page class and augment the data
structures (as opposed to overriding the method) in classes for certain sections of
pages on the site. Extending the Page Class
contains an example of the data structures for $css_linked_info
and $js_linked_info
.
This section presents some of the implementation details
for the Page
class. Many of the
implementation details are managed through private methods and therefore
are accessible only to the Page
class
itself. All of the private methods focus on the aggregation of CSS and JavaScript for the page. This
aggregation takes place as pages and modules are created, each
incrementally specifying the CSS and JavaScript that it requires:
manage_css_linked($keys)
Looks up and returns the corresponding CSS links for
an array of keys in $keys
. The
method keeps track of all CSS links already included and skips the
addition of any CSS link that was added previously. This ensures
that a CSS link is included at the first point it is required, but
never more than once.
create_css_linked($k)
Converts a single key, $k
, for a CSS file registered in
register_links
to a CSS link.
The resulting link is returned.
create_css($css)
Creates a complete block of CSS by wrapping the CSS
specified in $css
within the
proper style
tags. The method
returns the resulting block of CSS.
set_css_common
Sets the $css_common
member of the class so that
CSS links common to the entire web application are stored and can
be included at the proper time.
set_css_page
Sets the $css_page
member of the class so that
CSS links included at the page level are stored and can be
included at the proper time.
manage_js_linked($keys)
Looks up and returns the corresponding JavaScript
links for an array of keys in $keys
. The method keeps track of all
JavaScript links already included and skips the addition of any
JavaScript link added previously. This ensures that a JavaScript
link is included at the first point it is required, but never more
than once.
create_js_linked($k)
Converts a single key, in $k
, for a JavaScript file registered in
register_links
to a JavaScript
link. The resulting link is returned.
create_js($js)
Creates a complete block of JavaScript by wrapping
the JavaScript specified in $js
within the proper script
tags.
The method returns the block of JavaScript.
set_js_common
Sets the $js_common
member of the class so that
JavaScript links common to the entire web application are stored
and can be included at the proper time.
set_js_page
Sets the $js_page
member of the class so that JavaScript links included at the page
level are stored and can be included at the proper time.
Example 7-3
presents the code for the Page
class,
including implementations for many of the methods presented
earlier.
class Page { // Members for aggregating CSS styles from modules as they are added // to the page, storing information about how and when to link files, // and keeping track of various sections of CSS styles on the page. protected $css; protected $css_linked; protected $css_linked_info; protected $css_linked_used; protected $css_is_local; protected $css_common; protected $css_page; protected $css_module; protected $css_id; // Members for aggregating JavaScript from modules as they are added // to the page, storing information about how and when to link files, // and keeping track of various sections of JavaScript on the page. protected $js; protected $js_linked; protected $js_linked_info; protected $js_linked_used; protected $js_is_local; protected $js_common; protected $js_page; protected $js_module; protected $js_is_top; // Members to manage loading and saving data stored by the backend. protected $load_args; protected $load_data; protected $load_stat; protected $save_args; protected $save_data; protected $save_stat; protected $save_data_flag; // Members to manage meta information about the page and its body. protected $title; protected $equiv; protected $desc; protected $keywd; protected $body; /* * The following methods comprise the public interface for the class. */ public function __construct() { $this->css = ""; $this->css_linked = ""; $this->css_linked_info = array(); $this->css_linked_used = array(); $this->css_is_local = true; $this->css_common = ""; $this->css_page = ""; $this->css_module = ""; $this->css_id = ""; $this->js = ""; $this->js_linked = ""; $this->js_linked_info = array(); $this->js_linked_used = array(); $this->js_is_local = true; $this->js_common = ""; $this->js_page = ""; $this->js_module = ""; $this->js_is_top = false; $this->load_args = array(); $this->load_data = array(); $this->load_stat = ""; $this->save_args = array(); $this->save_data = array(); $this->sava_stat = ""; $this->save_data_flag = false; $this->title = ""; $this->equiv = ""; $this->desc = ""; $this->keywd = ""; } public function create() { $this->register_links(); if ($this->save_data_flag) $this->save_data(); $this->load_data(); // Do these steps now to give the page the opportunity to execute // them based on data from the backend that may have been loaded. $this->set_title(); $this->set_meta(); $this->set_css_id(); // This needs to be done before the modules add JavaScript and CSS // so that files from multiple sources appear in the order linked. $this->set_js_common(); $this->set_js_page(); $this->set_css_common(); $this->set_css_page(); $header = $this->get_header(); $content = $this->get_content(); $footer = $this->get_footer(); // We wrap the body of the page in a canvas division with its own // body, which provides some additional hooks useful for styling. $this->body = <<<EOD <div id="sitecvs"> <div class="sitecvsbd"> $header $content $footer <!-- sitecvsbd --> </div> <!-- sitecvs --> </div> EOD; return $this->body; } public function get_page() { if (empty($this->css_id)) $css_id = ""; else $css_id = " id="".$this->css_id."""; $doctype = $this->get_doctype(); $meta = $this->get_meta(); $title = $this->get_title(); // Generally, it's a good idea for performance to place JavaScript // at the bottom of the page; however, a flag lets us alter this. if ($this->js_is_top) { $js_top = $this->get_all_js(); $js_btm = ""; } else { $js_top = ""; $js_btm = $this->get_all_js(); } $css = $this->get_all_css(); // Return the entire page suitable for echoing back to the browser. return <<<EOD $doctype <html> <head> $meta $title $css $js_top </head> <body{$css_id}> $this->body $js_btm </body> </html> EOD; } public function get_doctype() { return <<<EOD <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> EOD; } public function get_meta() { $meta = <<<EOD <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> EOD; if (!empty($this->equiv)) { $meta .= <<<EOD <meta name="http-equiv" content="{$this->equiv}" /> EOD; } if (!empty($this->desc)) { $meta .= <<<EOD <meta name="description" content="{$this->desc}" /> EOD; } if (!empty($this->keywd)) { $meta .= <<<EOD <meta name="keywords" content="{$this->keywd}" /> EOD; } return $meta; } public function get_title() { return <<<EOD <title>{$this->title}</title> EOD; } public function add_to_css_linked($keys) { $this->css_linked .= $this->manage_css_linked($keys); } public function add_to_css($css) { $this->css .= $css; } public function get_all_css() { // First, we get all the styles that were appended by modules. $this->css_module = $this->css_linked; $this->css_module .= $this->create_css($this->css); // Then we assemble all the CSS styles for the page in one block. return <<<EOD <!-- Common CSS --> $this->css_common <!-- Page CSS --> $this->css_page <!-- Module CSS --> $this->css_module EOD; } public function add_to_js_linked($keys) { $this->js_linked .= $this->manage_js_linked($keys); } public function add_to_js($js) { $this->js .= $js; } public function get_all_js() { // First, we get all JavaScript that was appended by modules. $this->js_module = $this->js_linked; $this->js_module .= $this->create_js($this->js); // Then we assemble all the JavaScript for the page in one block. return <<<EOD <!-- Common JS --> $this->js_common <!-- Page JS --> $this->js_page <!-- Module JS --> $this->js_module EOD; } public function set_js_top() { $this->js_is_top = true; } /* * The following methods comprise the abstract interface for the * class. These are methods with empty implementations by default, * many of which specific page classes override for their needs. */ public function get_css_common() { } public function get_css_linked() { } public function get_css() { } // See the section on the abstract interface for the complete list // of methods for which empty implementations would be given here. ... /* * The following methods are for implementation details in the class. */ private function manage_css_linked($keys) { $css = ""; if (empty($keys)) return ""; // Normalize so that we can pass keys individually or as an array. if (!is_array($keys)) $keys = array($keys); foreach ($keys as $k) { // Log an error for unknown keys when there is no link to add. if (!array_key_exists($k, $this->css_linked_info)) { error_log("Page::manage_css_linked: Key "".$k."" missing"); continue; } // Add the link only if it hasn't been added to the page before. if (array_search($k, $this->css_linked_used) === false) { $this->css_linked_used[] = $k; $css .= $this->create_css_linked($k); } } return $css; } private function create_css_linked($k) { // Links can be fetched locally or from an also-known-as location. if ($this->css_is_local) $path = $this->css_linked_info[$k]["loc_path"]; else $path = $this->css_linked_info[$k]["aka_path"]; // Links have an optional media type (with a default type "all"). if (empty($this->css_linked_info[$k]["media"])) $media = "all"; else $media = $this->css_linked_info[$k]["media"]; return <<<EOD <link href="$path" type="text/css" rel="stylesheet" media="$media" /> EOD; } private function create_css($css) { if (!empty($css)) { return <<<EOD <style type="text/css" media="all" > $css</style> EOD; } else { return ""; } } private function set_css_common() { $this->css_common = $this->manage_css_linked($this->get_css_common()); } private function set_css_page() { $this->css_page = $this->manage_css_linked($this->get_css_linked()); $this->css_page .= $this->create_css($this->get_css()); } private function manage_js_linked($keys) { $js = ""; if (empty($keys)) return ""; // Normalize so that we can pass keys individually or as an array. if (!is_array($keys)) $keys = array($keys); foreach ($keys as $k) { // Log an error for unknown keys when there is no link to add. if (!array_key_exists($k, $this->js_linked_info)) { error_log("Page::manage_js_linked: Key "".$k."" missing"); continue; } // Add the link only if it hasn't been added to the page before. if (array_search($k, $this->js_linked_used) === false) { $this->js_linked_used[] = $k; $js .= $this->create_js_linked($k); } } return $js; } private function create_js_linked($k) { // Links can be fetched locally or from an also-known-as location. if ($this->js_is_local) $path = $this->js_linked_info[$k]["loc_path"]; else $path = $this->js_linked_info[$k]["aka_path"]; return <<<EOD <script src="$path" type="text/javascript"></script> EOD; } private function create_js($js) { if (!empty($js)) { return <<<EOD <script type="text/javascript"> $js</script> EOD; } else { return ""; } } private function set_js_common() { $this->js_common = $this->manage_js_linked($this->get_js_common()); } private function set_js_page() { $this->js_page = $this->manage_js_linked($this->get_js_linked()); $this->js_page .= $this->create_js($this->get_js()); } }
One of the most important benefits of an object-oriented
approach to defining pages for a large web application is the ease with
which you can derive classes for new types of pages from classes that
you have already created. In the previous section, we focused on one
example of a class, Page
, with
features generally useful to all types of pages across all types of web
applications. In this section, we look at some common derivations of
Page
. These include a page class to
handle the specifics of a single web application, page classes for
certain sections of a web application, and page classes for specific
pages. As we explore these types of classes, we’ll look at the role that
each is likely to play in a large web application, especially in terms
of which parts of Page
’s abstract
interface each class is likely to implement.
Such a systematic hierarchy of page classes helps us create large
web applications that are ultimately more maintainable because each
class is highly modular and has a great potential for reuse. In addition
to maintainability and reusability, page classes create a nice division
of responsibility. One group of engineers can focus on extending the
Page
base class to build a framework
that makes sense for an entire web application while another group can
work on the classes that support various sections of it. Other teams can
then focus on specific pages. As common needs arise across certain
scopes of the site, appropriate teams can perform those implementations
within the right level of the class hierarchy.
A sitewide page class is derived from Page
and customizes Page
for the unique characteristics that
apply to your entire web application. This type of page class
typically implements the following methods from Page
’s abstract interface: get_css_common
,
get_js_common
, register_links
,
get_header
, and
get_footer
. In
addition, you often use the sitewide page class to define other
methods and data members of your own that you expect to be relevant
across the entire web application. Data members used for paths are a good example. The
placement of members in a sitewide page class is a nice alternative to
using global variables. Some examples are listed below:
$path_root
The path to the starting point in your directory structure at which to find PHP include files and other source code. This is commonly called a prefix path. This path often has the form /home/userid/docroot.
$path_common
The path where you plan to place components that are
common to most parts of your web application. This member is
typically derived from $path_root
.
$path_layout
The path where you plan to place components related to
layout and containers, which are typically common across an
entire application. This member is typically derived from
$path_root
.
$path_datamgr
The path for all data managers (see Chapter 6). This member is typically derived
from $path_root
.
$path_base
The prefix address and path for all URLs in your web application (e.g., links to CSS files, links to JavaScript files, sources for images, etc.). This path has the form http://hostname.com/path.
$path_css
The prefix address and path for all URLs related to CSS
files. This member is typically derived from $path_base
.
$path_js
The prefix address and path for all URLs related to
JavaScript files. This member is typically derived from $path_base
.
$path_img
The prefix address and path for all URLs used for image
sources. This member is typically derived from $path_base
.
Example 7-4 presents
an example of a sitewide page class, SitePage
, which illustrates implementations
for the methods mentioned previously. The example also shows the ease
with which you can override default implementations provided by
Page
. For example, SitePage
overrides Page
’s implementation of get_all_js
to add Google analytics to the site (see Chapter 9). It does this by delegating most of the work
back to the implementation of get_all_js
in Page
. However, it then appends the analytics
code to the JavaScript previously assembled. It makes sense to
implement this in the sitewide page class because analytics are run
for the entire site.
Example 7-4 shows a
common way to add capabilities in object-oriented systems, which may
be unfamiliar to PHP programmers who are new to objects. To
differentiate between a method in the base class that has also been
implemented in the derived class, you use the scope (::
) operator.
For instance, __construct
starts
with:
parent::__construct();
Derived classes typically do this as the first line in the constructor to set up the parts of the object inherited from the base class. More statements can then be added to the derived class constructor to carry out tasks specific to the derived class.
class SitePage extends Page { const path_css = "..."; const path_js = "..."; const path_img = "..."; ... public function __construct() { parent::__construct(); ... } public function get_all_js() { // First, get all the JavaScript that was assembled for modules, // etc. on the page. $js = parent::get_all_js(); $analytics = <<<EOD <!-- Google Analytics --> <script type="text/javascript"> var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); var pageTracker = _gat._getTracker("..."); pageTracker._setDomainName("..."); pageTracker._trackPageview(); </script> EOD; } // Append Google Analytics to the JavaScript that was assembled // otherwise for the page. return <<<EOD $js $analytics EOD; } public function get_js_common() { // Specify an array of JavaScript files to link for every page. return array ( "yahoo-dom-event.js", "sitewide.js" ); } public function get_css_common() { // Specify an array of stylesheet files to link for every page. return array ( "sitewide.css" ); } public function get_header() { // Return the HTML markup for the header across the entire site. return <<<EOD <div id="sitehdr"> ... </div> EOD; } public function get_footer() { // Return the HTML markup for the footer across the entire site. return <<<EOD <div id="siteftr"> ... </div> EOD; } public function register_links() { // Build the data structure for resolving stylesheet filenames. $this->css_linked_info = array ( "sitewide.css" => array ( "aka_path" => "...", "loc_path" => "...", "media" => "all" ), ... ); // When this member is set to true, stylesheet keys resolve to // the loc_path paths; otherwise, the aka_path paths are used. $this->css_is_local = false; // Build the data structure for resolving JavaScript filenames. $this->js_linked_info = array ( "sitewide.js" => array ( "aka_path" => "...", "loc_path" => "..." ), "yahoo-dom-event.js" => array ( "aka_path" => "...", "loc_path" => "..." ), ... ); // When this member is set to true, JavaScript keys resolve to // the loc_path paths; otherwise, the aka_path paths are used. $this->js_is_local = false; } // You would likely define a number of other methods here for tasks // specific to your web application and that apply to all parts of it. ... }
A sectional page class is a class derived from your
sitewide page class that customizes the sitewide page class for a
section of your application (e.g., NewCarsPage
for a section of the site
containing multiple pages related to new cars). Some methods that a
sectional page class might define are get_header
and
get_footer
to display
a different header and footer for a section, or get_css_linked
and
get_js_linked
to
link additional CSS and
JavaScript files for just that section.
In addition, a sectional page class may want to register its own
set of links for CSS and JavaScript files. It does this by
implementing its own register_links
method, which calls the parent’s register_links
method first to let the
parent add links for the entire web application, then appends its own
entries for CSS and JavaScript files to the $css_linked_info
and $js_linked_info
members.
Page-specific classes are the most common pages that
you’ll implement. These are classes like NewCarSearchResultsPage
, presented earlier
in Example 7-1. In page-specific
classes, you typically implement the following from Page
’s abstract interface: save_data
, load_data
, get_content
,
set_title
, set_meta
, and
set_css_id
. You might
also implement get_css
and
get_js
to provide
small amounts of CSS and JavaScript to embed on the page beyond what
the modules already provide; however, the modules normally should have
already specified everything they are able to encapsulate themselves.
Of course, object orientation lets you override any method in a page’s
specific class to do something different from the base class.
As mentioned at the start of the chapter, a module is a self-contained component of the user interface that encompasses everything needed (e.g., the HTML, CSS, and JavaScript) to make an independently functioning and cohesive unit that can be used in a variety of contexts across various pages. Considering that a page is the canvas responsible for assembling a collection of modules so that they work together within a single context, modules must provide a page with everything that the page requires to assemble itself. This common requirement among modules suggests that we should have a base class from which to derive all modules.
In this section, we’ll look at one example of a base class
for modules called Module
. Although
it’s not hard to imagine features beyond those presented here for such a
class, the example provides a good starting point by which to implement
modules in many large web applications.
A module specifies what it needs loaded for HTML, CSS, and
JavaScript, but these pieces are actually assembled later by the page,
because only the page can put everything together in the proper order.
Therefore, when you create an instance of the Module
class, you pass in a reference to the
page on which the module will reside. The module then places references to
all the pieces it needs in data structures within that page, and the page
uses them later.
Just as we did for Page
, let’s
explore the Module
class by examining
its public interface, abstract interface, and implementation
details.
The public interface for Module
consists of a single method named
create
, which has an implementation
in the base class that is sufficient for most modules. This interface is
nicely parallel to the interface for creating pages:
create()
Creates a module and returns its HTML markup. In the
process, create
performs
several important tasks. These include, in order, adding
CSS links and embedded CSS for the module to the page, adding
JavaScript links and embedded JavaScript for the module to the page, and
getting the content for the module. The create
method performs each of these
tasks by calling methods from the abstract interface, discussed
next.
The abstract interface for Module
consists of methods that specific
modules are expected to implement as needed. The Module
class calls upon these methods at the
appropriate moments from its create
method. Because Module
provides empty
implementations for each of the methods, a class derived from Module
is not required to implement all of the
methods in the abstract interface; it implements only the methods that
it requires. For example, if a module doesn’t need to link any
JavaScript files, it doesn’t have to provide an implementation for
get_js_linked
. The simplest modules
may implement just the get_content
and get_css_linked
methods.
The methods in the abstract interface for managing CSS let you specify the CSS files to link and the CSS to embed for a module. These are aggregated by the page on which the module resides when the module is created so that the page can insert them at the appropriate point when the page is assembled:
get_css_linked()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for the
CSS files to link for the module. If you follow the convention
that the get_css_common
method of the page class will include a sitewide file, you don’t
have to specify it here; however, specifying it will cause no
harm because the page will check and make sure not to include it
twice.
get_css()
Implement this method to return a string of CSS to embed for the module. This method is useful when you need to specify dynamically generated CSS for a module.
The methods in the abstract interface for managing JavaScript let you specify the JavaScript files to link and the JavaScript to embed for a module. As with CSS, the links and embedded JavaScript are passed to the page to assemble later:
get_js_linked()
Implement this method to return an array of keys
registered in register_links
(see register_links
) for the
JavaScript files to link for the module. Modules that require
JavaScript often link several JavaScript libraries to ensure
that all dependencies between the libraries are
addressed.
get_js()
Implement this method to return a string of JavaScript to embed for the module. This method is useful when you need to specify dynamically generated JavaScript for a module.
Modules define a single method for working with content. This method generates the HTML that appears in the module:
get_content
Implement this method to return the HTML markup
for the content of the module. If needed, you can also create
other modules within the get_content
method for a module in the
same way as you do for pages (see Implementing Nested Modules).
Because modules perform just a few tasks, our
implementation of Module
does not
need any private or protected methods. Example 7-5 presents the code
for the Module
class.
class Module { // Used to store a reference to the page on which the module resides. protected $page; /* * The following methods comprise the public interface for the class. */ public function __construct($page) { // All modules store a reference to the page on which they reside. // In PHP 5, objects are passed by reference. In PHP 4, we would // have had to specify we wanted a reference explicitly (using &). $this->page = $page; } public function create() { // Add module CSS styles to the page on which the module resides. $this->page->add_to_css_linked($this->get_css_linked()); $this->page->add_to_css($this->get_css()); // Add module JavaScript to the page on which the module resides. $this->page->add_to_js_linked($this->get_js_linked()); $this->page->add_to_js($this->get_js()); return $this->get_content(); } /* * The following methods comprise the abstract interface for the * class. These are methods with empty implementations by default, * many of which specific module classes override for their needs. */ public function get_css_linked() { } public function get_css() { } public function get_js_linked() { } public function get_js() { } public function get_content() { } }
Just as we extended the Page
class for new types of pages, we can
derive new types of modules from Module
. Generally, you will find that most
modules can be derived directly from Module
. However, our object-oriented approach
to developing modules provides the same opportunities for good
maintainability and reusability as we saw with page classes earlier. One
example of a specific type of module that provides a good opportunity
for reuse is the Layout
base class for
all layouts and containers (see Layouts and Containers). Layouts require the same
capabilities as other modular-type entities, while adding some of their
own.
Modules can be many things in a large web application: a list of search results, a form for entering search queries, a menu bar, a wrapper for standard advertising units, or a highly reusable user interface component like a selection list, paginator, or stylized button, to name a few. In this section, we explore a popular component in many large web applications: a slideshow, which presents a series of slides along with some navigation (right, left, or an arbitrarily chosen slide).
One way to implement a slideshow is to define two modules that work together. One module, which we’ll call the Picture Slider module, provides a slider of thumbnail images from which the visitor makes selections. The other module, which we’ll call the Picture Viewer module, provides a larger view of the image selected in the picture slider (see Figure 7-1). This slideshow isn’t fancy; it doesn’t change slides automatically at fixed time intervals, but it keeps the current slide as well as the position of the slider in sync with the movements specified by the visitor.
Example 7-6
presents implementations for PictureSlider
and PictureViewer
, the two classes that define the
Picture Slider and Picture Viewer modules, respectively. Because these
classes are normally used together, you might decide to place them both
in a single include file called slideshow.inc.
You’ll notice with a closer look that even though the classes for the
two modules are often used together, they are not tightly coupled; each
works completely independently. One benefit of defining two separate
classes is that you can arrange the slider and viewer on the page
however you desire. For example, you can place the slider above or below
the viewer, or you can place a small module of some other type between
them.
The Picture Slider and Picture Viewer modules are easy to
configure. When you instantiate PictureSlider
, for example, you simply pass it
a gallery of images to display as an array of image data. Each member in
the array is an associative array consisting of the URL for a thumbnail
of the image (img_t
), a URL for the
large version of the image (img_l
),
the text for the caption (text
), and
text for the attribution (attr
). When
you instantiate PictureViewer
, you
pass it one member of the gallery for it to display.
Notice that both PictureSlider
and PictureViewer
define the methods
outlined for Module
earlier that let
you specify the CSS and JavaScript for a module and get its content. These
effectively allow the CSS and JavaScript to travel with the module
wherever it is used. Furthermore, they document exactly where to find
the CSS and JavaScript for the module, should you decide to refactor the
code for the module in the future.
<?php require_once(".../common/module.inc"); class PictureSlider extends Module { var $gallery; var $type; var $picture_width; var $slider_frames; public function __construct($page, $gallery) { parent::__construct($page); $this->gallery = $gallery; $this->type = "default"; $this->picture_width = 65; $this->slider_frames = 8; } public function get_css_linked() { // Specify the file in which the CSS for the module is provided. return array("sitewide.css"); } public function get_js_linked() { // Specify the JavaScript files that must be included on the page // where this module is created. The JavaScript for this module // needs YUI libraries for managing the DOM and doing animation. // Presumably, the module's own JavaScript resides in sitewide.js. return array ( "yahoo-dom-event.js", "animation.js", "sitewide.js" ); } public function get_js() { // The JavaScript here is dynamically generated. We're using PHP // to create some JavaScript that is parameterized by the module. // For instance, the total width depends on the number of slides, // calculated from the slides passed to the module's constructor. $strip_width = $this->picture_width * $this->slider_frames; $count = count($this->gallery); $total_width = $this->picture_width * $count; return <<<EOD var picsld = new PictureSlider(); picsld.init = function() { this.stripWidth = $strip_width; this.totalCount = $count; this.totalWidth = $total_width; this.update(); // Show the slider only after the JavaScript it needs is loaded. If // we're placing JavaScript at the bottom of the page for performance // reasons, we must ensure that no interactions take place before the // JavaScript has been loaded to handle them. this.loaded(); } picsld.init(); EOD; } public function get_content() { $strip = $this->get_strip(); $count = count($this->gallery); if ($count > 0) { $showing = <<<EOD Showing picture <strong>1</strong> of $count EOD; } else $showing = ""; return <<<EOD <div id="picsld" class="{$this->type}"> <div class="sldpos"> $showing </div> <div class="sldtab"> <img class="btnl" src=".../slide_arrow_l.gif" width="14" onclick= "picsld.slideL();" /> <div class="vwpt"> $strip </div> <img class="btnr" src=".../slide_arrow_r.gif" width="14" onclick= "picsld.slideR();" /> </div> </div> EOD; } protected function get_strip() { // Initialize the HTML that lays out the pictures and the number // to assign each picture. $items = ""; $i = 0; foreach ($this->gallery as $picture) { $item_id = "picslditm".$i; // Prepare the strings for insertion later between the single // quotes in the picsld.select method. $img = str_replace("'", "'", $picture["img_l"]); $text = str_replace("'", "'", $picture["text"]); $attr = str_replace("'", "'", $picture["attr"]); $n = $i + 1; // At the start, the leftmost slide is selected; therefore, it // is shown in the picture viewer. if ($i == 0) $sel = " selected"; else $sel = ""; // Create the HTML for one picture. The HTML will be added to // the module's HTML after all the pictures have been created. $items .= <<<EOD <td> <div id="$item_id" class="item{$sel}"> <img src="{$picture["img_t"]}" alt="{$picture["text"]}" width="55" height="55" onmousedown="picsld.select ('$item_id', $n, '$img', '$text','$attr')," /> </div> </td> EOD; $i++; } // Add blank slides to fill frames when the number of pictures is // not evenly divisible by the frames that appear in the slider. while ($i % $this->slider_frames != 0) { $items .= <<<EOD <td> <div class="item"> <img src=".../slide_blank_bg.gif" width="55" height="55" /> </div> </td> EOD; $i++; } return <<<EOD <table cellspacing="0" cellpadding="0" border="0"> <tr> $items </tr> </table> EOD; } } class PictureViewer extends Module { var $picture; var $type; public function __construct($page, $picture) { parent::__construct($page); $this->picture = $picture; $this->type = "default"; } public function get_css_linked() { // Specify the file in which the CSS for the module is provided. return array("sitewide.css"); } public function get_content() { // The content for this module consists of a single image, along // with text for the caption and the attribution. $attr = ""; if (!empty($this->picture["attr"])) { $attr = <<<EOD <cite> courtesy of {$this->picture["attr"]} </cite> EOD; } if (empty($this->picture["img_l"])) $img = ""; else { $img = <<<EOD <img src="{$this->picture["img_l"]}" alt="{$this->picture["text"]}" width="600" /> EOD; } return <<<EOD <div id="picvwr" class="{$this->type}"> <div class="vwrimg"> $img </div> $attr <div class="vwrcap"> {$this->picture["text"]} </div> </div> EOD; } } ?>
Example 7-7
presents the JavaScript that implements behaviors for the Picture
Slider module. In a manner consistent with what we discussed for
large-scale JavaScript in Chapter 5, the
module has a JavaScript object that neatly encapsulates the
functionality that the component requires, and the object is named
PictureSlider
to reflect the module’s
name. In Example 7-6, the
get_js_linked
method of PictureSlider
specifies that this object is
defined in the file identified by the key
sitewide.js. The JavaScript for the module needs
two YUI libraries (which you can download from http://developer.yahoo.com/yui) to
support it. Therefore, get_js_linked
specifies keys for these files before
sitewide.js.
PictureSlider = function() { // Set up references to the elements needed for the slider and viewer. this.slider = document.getElementById("picsld"); if (this.slider) { this.tab = this.slider.getElementsByTagName("table"); this.tab = (this.tab && this.tab.length > 0) ? this.tab[0] : null; } if (this.slider) { this.lb = YAHOO.util.Dom.getElementsByClassName ( "btnl", "img", this.slider ); this.lb = (this.lb && this.lb.length > 0) ? this.lb[0] : null; this.rb = YAHOO.util.Dom.getElementsByClassName ( "btnr", "img", this.slider ); this.rb = (this.rb && this.rb.length > 0) ? this.rb[0] : null; } this.viewer = document.getElementById("picvwr"); // You pass values for the following parameters to the module's // constructor in PHP. The module's get_js method in PHP dynamically // generates the JavaScript to set these members from those values. this.stripWidth = 0; this.totalCount = 0; this.totalWidth = 0; // This lock is needed to ensure that one left or right move of the // slider runs to completion; otherwise, misalignment could happen. this.lock = false; }; PictureSlider.prototype = new Object(); PictureSlider.prototype.slideL = function() { // Moving to the left adjusts the slider in a positive direction. this.adjust(+(this.stripWidth)); }; PictureSlider.prototype.slideR = function() { // Moving to the right adjusts the slider in a negative direction. this.adjust(-(this.stripWidth)); }; PictureSlider.prototype.adjust = function(amt) { // If already locked, do nothing; otherwise, get the lock and go. if (this.lock) return; else this.lock = true; var anim; var ease = YAHOO.util.Easing.easeOut; var pos = parseInt(YAHOO.util.Dom.getStyle(this.tab, "left")); // Prevent moving past either end of the slider during an adjustment. if (amt > 0) { if (pos + amt > 0) amt = 0; } if (amt < 0) { if (pos + amt <= -(this.totalWidth)) amt = 0; } // The following creates a closure that ensures access to members // of the PictureSlider instance from inside the update method. var obj = this; function handleComplete() { obj.update(); obj.lock = false; } // Do the sliding animation if there is any amount to move; otherwise, // just call update directly to ensure the arrow buttons are updated. if (amt != 0) { anim = new YAHOO.util.Anim(this.tab, {left: {by: amt}}, 0.5, ease); anim.onComplete.subscribe(handleComplete); anim.animate(); } else { this.update(); this.lock = false; } }; PictureSlider.prototype.update = function() { var pos; pos = parseInt(YAHOO.util.Dom.getStyle(this.tab, "left")); // Switch images to indicate which buttons are enabled or disabled. if (pos >= 0) this.lb.src = ".../slide_arrow_off_l.gif"; else this.lb.src = ".../slide_arrow_l.gif"; if (pos <= -this.totalWidth + this.stripWidth) this.rb.src = ".../slide_arrow_off_r.gif"; else this.rb.src = ".../slide_arrow_r.gif"; }; PictureSlider.prototype.select = function(targ, n, img, text, attr) { var sld; var el; // Switch the selection by changing the frame with the selected class. el = YAHOO.util.Dom.getElementsByClassName ( "selected", "div", this.slider ); if (el && el.length > 0) YAHOO.util.Dom.removeClass(el[0], "selected"); if (targ) YAHOO.util.Dom.addClass(targ, "selected"); // Reload the picture viewer with the current selection in the slider. this.reload(img, text, attr); // Update the text indicating the position of the selected picture. el = YAHOO.util.Dom.getElementsByClassName ( "sldpos", "div", this.slider ); if (el && el.length > 0) { el[0].innerHTML = "Showing picture <strong>" + n + "</strong> of " + this.totalCount; } }; PictureSlider.prototype.reload = function(img, text, attr) { // Handle the case of no viewer associated with the picture slider. if (!this.viewer) return; var el; // Get the image viewer and change the image currently being shown. el = YAHOO.util.Dom.getElementsByClassName ( "vwrimg", "div", this.viewer ); if (el && el.length > 0) { el = el[0].getElementsByTagName("img"); if (el && el.length > 0) { el[0].src = img; el[0].alt = text; } } // Change the attribution in the picture viewer for the selection. el = this.viewer.getElementsByTagName("cite"); if (el && el.length > 0) el[0].childNodes[0].nodeValue = "courtesy of" + attr; // Change the caption in the picture viewer based on the selection. el = YAHOO.util.Dom.getElementsByClassName ( "vwrcap", "div", this.viewer ); if (el && el.length > 0) el[0].childNodes[0].nodeValue = text; }; PictureSlider.prototype.loaded = function() { // Fire this from your initialization method for the picture slider. var el; el = YAHOO.util.Dom.getElementsByClassName ( "sldtab", "div", this.slider ); YAHOO.util.Dom.setStyle ( el[0], "visibility", "visible" ); };
As you can see, a slideshow contains enough interrelated pieces
that defining it using nicely encapsulated modules is critical to making
it highly reusable, maintainable, and ultimately reliable in the long
life cycle of a large web application. Example 7-8
demonstrates how easy this module is to use despite all the
interconnected pieces of its implementation. Just as we saw for the
simple modules at the start of the chapter, you need only instantiate
the modules with the necessary arguments to configure them, call their
create
methods, and place them within
the proper section of the layout for the page. Furthermore, you need
only include a single include file, slideshow.inc,
to use the slideshow. The
necessary HTML, CSS, and JavaScript for its modules travel with the
slideshow wherever you use it.
<?php require_once(".../common/sitepage.inc"); require_once(".../common/slideshow.inc"); ... class NewCarDetailsPage extends SitePage { ... public function get_content() { ... // Create the picture slider and picture viewer for the slideshow. // The backend data will have been loaded earlier during a call to // load_data. $mod = new PictureSlider ( $this, $this->load_data["new_car_info"]["gallery"] ); $slider = $mod->create(); $mod = new PictureViewer ( $this, $this->load_data["new_car_info"]["gallery"][0] ); $viewer = $mod->create(); ... // Place the HTML markup for each module within the page layout. $mod = new DetailsLayout ( $this, array(...), array($slider, $viewer), array(...), array(...), array(...), array(...) ); // Return the content, which the create method for the page uses. return $mod->create(); } ... } ?>
In Chapter 4, we discussed layouts as highly reusable, generic templates that define the overarching structure of pages. You saw that containers are even finer groupings of modules typically placed within layouts. Together, layouts and containers play a vital role in fostering reusability, maintainability, and reliability in large web applications by defining a number of standard sections in which to place modules on a page. A section is simply a region in which we can insert one or more modules.
As we explore implementations for layouts and containers, you’ll see that layouts and containers are just specialized types of modules. That is, they require many of the same things that other modules do. The main distinction is that aside from the additional structural elements that a layout or container defines, the content for a layout or container is actually just the content of other modules.
Because the operations you need to perform to generate the
individual sections of a layout or container are generally the same for
most layouts and containers, it’s useful to define a base class, Layout
, for this purpose. Example 7-9 presents the Layout
class, which is derived from Module
. This class defines get_section
, which places
a set of modules within a div
and
assigns the div
a specified class so
that styles for positioning and formatting the section can be
applied.
class Layout extends Module { public function __construct($page) { parent::__construct($page); } public function get_section($class, $modules) { if (count($modules) == 0) return ""; foreach ($modules as $content) $section .= empty($content) ? "" : $content; if (empty($section)) return ""; return <<<EOD <div class="$class"> $section <!-- $class --> </div> EOD; } }
Example 7-10 presents a layout class intended for laying out any page related to search results, be they search results for new cars, used cars, reviews, or articles in our web application. This layout has sections for a header at the top, three content sections in the middle, and two footers across the bottom (this is the same layout from Figure 4-3 in Chapter 4). Because any HTML for modules that you pass through the layout’s interface for each section simply gets inserted into the proper locations in the overall structure of the layout, it is highly reusable for any page whose design observes this same structure; you simply insert different modules. As a result, layouts and containers provide a good opportunity for engineers and designers to work together to establish standard guidelines under which pages can be designed and built. Classes for containers are defined in much the same way as for layouts, but they typically organize smaller groups of modules for use within sections of a layout.
class ResultsLayout extends Layout { protected $layreshdr; protected $layrespri; protected $layressec; protected $layrester; protected $layresftr1; protected $layresftr2; public function __construct ( $page, $layreshdr, $layrespri, $layressec, $layrester, $layresftr1, $layresftr2 ) { parent::__construct($page); $this->layreshdr = $layreshdr; $this->layrespri = $layrespri; $this->layressec = $layressec; $this->layrester = $layrester; $this->layresftr1 = $layresftr1; $this->layresftr2 = $layresftr2; } public function get_css_linked() { return array("sidewide.css"); } public function get_content() { $layreshdr = $this->get_section("layreshdr", $this->layreshdr); $layrespri = $this->get_section("layrespri", $this->layrespri); $layressec = $this->get_section("layressec", $this->layressec); $layrester = $this->get_section("layrester", $this->layrester); $layresftr1 = $this->get_section("layresftr1", $this->layresftr1); $layresftr2 = $this->get_section("layresftr2", $this->layresftr2); // The content for the layout is just the content of the modules // passed into the layout and inserted into the layout sections. return <<<EOD <div id="layres"> $layreshdr <div class="layresmaj"> $layrespri $layressec $layrester <-- layresmaj --> </div> $layresftr1 $layresftr2 <!-- layres --> </div> EOD; } }
The more you work with any large web application, the more you’ll turn up special considerations that you didn’t originally think about. Fortunately, it’s relatively easy to adapt the techniques with large-scale PHP introduced in this chapter for many different situations. Indeed, this is one of the most important tests of a good architecture: can it adapt to new situations without undue contortions? This section presents some examples of special situations in large web applications with solutions using the techniques from this chapter.
In large web applications, modules frequently need to
function or appear in a different manner on different pages. For
example, suppose you would like a module to support default, compact,
and mid-size presentations, as illustrated in Chapter 4. Example 7-11 demonstrates a
solution wherein the class of the module’s containing div
becomes parameterized. To do this, you
specify a data member for the module named $class
and define setter methods that change its value. The module picks up
the proper CSS when the class
attribute of its containing div
is
set to the value stored in the $class
data member.
For more extensive variations requiring more significant changes to the HTML markup or JavaScript for a module, you can manage those variations in a similar manner using data members to control how the HTML markup or JavaScript is generated. It helps to have a nicely encapsulated class for the module and a well-defined interface for selecting the variations.
class PopularNewCars extends Module { protected $class; ... public function __construct($page, ...) { parent::__construct($page); $this->class = "default"; // Set up the module using other arguments from the constructor. ... } public function set_mode_default() { $this->class = "default"; } public function set_mode_compact() { $this->class = "compact"; { public function set_mode_midsize() { $this->class = "midsize"; } public function get_content() { return <<<EOD <div id="nwcpop" class="$this->class"> ... </div> EOD; } }
In large web applications, multiple instances of the same
module often need to appear on the same page. For example, suppose you
have a paginator (containing links for paginating lists across
multiple pages) that you would like to place at both the top and bottom
of a list of search results. Example 7-12 presents one
solution using the method set_instance
that lets you supply a unique
identifier by which to identify each instance of the module. The module
appends this unique ID to a base ID for the module to keep all IDs on
the page unique.
Now, because there may be multiple instances of the module on the
same page, you should also scope the CSS for the module by class name
instead of ID (see Chapter 4, Scoping within a module). To target a specific instance of
the module on a page for smaller amounts of additional CSS (e.g.,
different margins for different instances), you can use the get_css
method for the
page to specify the CSS based on the module’s ID, or you can add CSS
scoped to the specific page in your sitewide or sectional
CSS file (see Chapter 4, Scoping at the page level).
class Paginator extends Module { protected $class; protected $ident; ... public function __construct($page, ...) { parent::__construct($page); $this->class = "pagint"; $this->ident = $this->class."def"; // Set up other members for configuring the paginator state based // on other arguments passed into the constructor for the module. ... { public function set_instance($instance) { $this->ident = $this->class.$instance; } public function get_content() { return <<<EOD <div id="$this->ident" class="$this->class"> ... </div> EOD; } }
Earlier, in the PictureSlider
class in Example 7-6, you saw an example
that used PHP to generate dynamic JavaScript for the Picture Slider
module based on several member variables set within the class. The
get_css
and get_js
methods defined
by the base classes for pages and modules provide a consistent interface
for the generation of dynamic CSS and JavaScript, respectively, at
either the page or module level, as needed.
One of the reasons that the page and module classes we’ve discussed in this chapter work well for large web applications is that they are very extensible. We’ve seen that this is true when defining hierarchies of classes—once you have a class for one type of page or module, it’s relatively easy to extend it for another. Another example of extensibility is the ability to create modules within other modules, which is especially useful for user interface components. The paginator mentioned previously in Multiple Instances of a Module is a good example.
Example 7-13 illustrates the
creation of two instances of the Paginator module within the New Car
Search Results module. The creation of a module within another module
proceeds exactly as within a page. Just remember that the first
parameter passed to each module is the $page
member of the enclosing module (as
opposed to $this
, which for pages is
the page itself, but for modules is the module instance).
class NewCarSearchResults extends Module { ... public function get_content() { // Create a paginator to appear at the top of the search results. $mod = new Paginator($this->page, ...); $mod->set_instance("pri"); $pgnpri = $mod->create(); // Create an additional paginator module to appear at the bottom. $mod = new Paginator($this->page, ...); $mod->set_instance("sec"); $pgnsec = $mod->create(); // Call upon a private method to build the actual list of results. $results = $this->get_results(); return <<<EOD <div id="nwcsrs"> $pgnpri $results $pgnsec </div> EOD; } }