Chapter 7. Large-Scale PHP

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.

Modular Web Pages

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.

Example 7-1. Creating a modular web page
<?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.

Generating Pages in PHP

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.

Example 7-2. Implementing a modular web page
<?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.

Note

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

Working with Pages

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.

Public Interface for the Page Class

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.

Structure and assembly

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.

CSS management

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:

  1. CSS links specified by get_css_common (for the global CSS files)

  2. CSS links specified by the page class (see get_css_linked)

  3. Embedded CSS specified by the page class (see get_css)

  4. CSS links added by modules (see get_css_linked in Public Interface for the Module Class)

  5. 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.

JavaScript management

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:

  1. JavaScript links specified by get_js_common (for the global JavaScript files)

  2. JavaScript links specified by the page class (see get_js_linked)

  3. Embedded JavaScript specified by the page class (see get_js)

  4. JavaScript links added by modules (see get_js_linked in Public Interface for the Module Class)

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

Abstract Interface for the Page Class

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.

CSS management

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.

JavaScript management

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.

Dynamic data management

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.

Headers, footers, and content

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.

General page information

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.

Implementation of the Page Class

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.

Example 7-3. Implementation of the Page class
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());
   }
}

Extending the Page Class

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.

Defining a sitewide page class

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.

Example 7-4. Implementing a sitewide page 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.
   ...
}

Defining sectional page classes

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.

Defining page-specific classes

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.

Working with Modules

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.

Public Interface for the Module Class

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.

Abstract Interface for the Module Class

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.

CSS management

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.

JavaScript management

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.

Content for the 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).

Implementation of the Module Class

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.

Example 7-5. Implementation of 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()
   {
   }
}

Extending the Module Class

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.

An Example Module: Slideshow

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.

The Picture Slider and Picture Viewer modules in a slideshow
Figure 7-1. The Picture Slider and Picture Viewer modules in a slideshow

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.

Example 7-6. PictureSlider and PictureViewer classes for the slideshow
<?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.

Example 7-7. PictureSlider JavaScript object for the slideshow
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.

Example 7-8. Creating the modules for a slideshow
<?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();
   }

   ...
}
?>

Layouts and Containers

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.

Example 7-9. Implementation of the Layout class
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.

Example 7-10. Implementing a layout class
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;
   }
}

Special Considerations

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.

Handling Module Variations

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.

Example 7-11. Handling variations of the Popular New Cars module
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;
   }
}

Multiple Instances of a Module

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

Example 7-12. Handling multiple instances of the Paginator module
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;
   }
}

Dynamic JavaScript and CSS

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.

Implementing Nested Modules

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

Example 7-13. Creating nested paginators in the New Car Search Results module
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;
   }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset