Another PHP Master Page Architecture

PHP is a popular server-side scripting language used by a substantial portion of web sites. It is an extremely flexible language that allows web sites to be organized in a huge variety of ways. PHP does not promote one architectural design over another, so web site authors have each come up with their own design patterns. Some are better than others, but many leave a lot to be desired.

In this article, I plan to layout a working architecture for a PHP-based web site based on the concept of a master page. Some server-side platforms, notably ASP.NET, have built-in support for master page architecture. The general idea is that the master page contains those elements common to every web page in a site, such as headers, footers and navigation. The master page also includes a content container which will hold the content of whatever web site page is loaded.

While there are many published approaches to creating master pages in PHP, the architecture I present here has several advantages:

  • SEO Friendly URLs – The URLs seen by browsers and search engines have a clean, readable style without dependence on GET parameters. For example, the page listing the features for a product called Widget may have a URL like http://www.somecompany.com/products/Widget/features.
  • Concealed file extensions of server scripts – Even though the server-side script files have an extension of .php, the extension will never appear in the URLs. This way, if someday, something better than PHP comes along, the server-side scripts can be changed without affecting the URLs.
  • Common navigation framework – The navigation elements are defined only once, in the master page file.
  • Better separation of navigation and content – In some master page implementations, every content page has includes for a header, footer, navigation file, etc. The architecture presented here does the opposite, by having the master page include content pages. In this way, the content files have no references to navigation, header or footer include files.
  • Minimalist content files – The content files contain only content relevant to the corresponding page; nothing else. In some cases, the content files may be completely free of any PHP code.
  • Isolation of included files – The web root folder contains the master page file, an .htaccess file and folders for images, CSS files and javascript files. Everything else is located outside of the web root. This reduces the chance of php include files being unintentionally accessible directly from a URL.

Design Overview

The design of the PHP Master Page Architecture is based on some file and folder organization and naming conventions, a few Apache server configuration settings and an “intelligent” master page file. The remainder of this article discusses the implementation details using a working prototype website as an example.

Directory Structure

In the master page architecture, there are two primary directory trees involved. One contains the master PHP file as well as files directly referenced by the browser, while the other contains PHP files included by the master PHP file. The root of the directory tree containing the master PHP file is hereafter designated by {webroot}, while the one containing PHP include files is designated by {include}. The following table describes the structure of each of these directory trees:

Folder Description
{include} The base include folder as configured in php.ini
{include}/header/ The root include folder for page headers
{include}/header/… Other header include folders matching the folder structure of the site’s URLs
{include}/content/ The root include folder for page contents
{include}/content/… Other content include folders matching the folder structure of the site’s URLs
{webroot} The folder configured as the Apache DocumentRoot for the web site
{webroot}/image/ A folder containing images used on the web pages (may contain subfolders if desired)
{webroot}/script/ A folder containing javascript files used in the web pages (may contain subfolders if desired)
{webroot}/style/ A folder containing CSS files used in the web pages (may contain subfolders if desired)

File Organization and Naming

The master PHP file (named master.php) is located in the {webroot} folder. For any given page in the website, up to three include files are used; one for the contents of the <head> section of the HTML page, one for the main content of the page and another for any content to be displayed in a javascript popup. The include files are located in folders partially matching the URL path and are named according to the last portion of the URL path. For example, the page referenced by the URL http://www.somecompany.com/products/Widget/features is assembled from the following include files:

  • {include}/header/products/Widget/features.php
  • {include}/content/products/Widget/features.php
  • {include}/popup/products/Widget/features.php

If the header include file does not exist, then a standard header is used. The content include file must exist, or a 404 page is shown. The popup include file is optional, and is only necessary if the page contains content to be displayed in a javascript popup window.

No specific file naming convention is enforced for image, javascript or CSS files. Even the folders defined in the previous section for these files types are recommendations. You can choose whatever folder names you like and optionally use subfolders. I do not recommend mirroring the URL folder structure of your site under the {webroot} folder as this may interfere with the proper operation of the master page architecture.

Web Server Configuration

The following sections describing web server configuration assume the use of an Apache web server with a standard PHP installation.

PHP Configuration

The only change needed in addition to the standard PHP set up is the addition of the {include} folder. To do this, add the {include} folder to the include_path variable in the PHP.ini file. If no such variable is defined then add the variable as follows:

include_path="{include}"

An alternative method for setting the include_path variable is to set it in the .htaccess file in the {webroot} folder. This method works when it is impossible or undesirable to change the global include_path variable in PHP.ini. This configuration can be accomplished with the following line in the .htaccess file:

php_value include_path="{include}"

Apache Configuration

Most of the Apache configuration necessary for the master page architecture will be set up in the .htaccess file located in the {webroot} folder. Many of the configuration settings will rely upon the mod_rewrite apache module. It may be necessary to enable the use of this module in the main Apache configuration file (conf/httpd.conf). The use of this module requires the FollowSymLinks option. Since we also wish to disallow displaying a directory listing for folders, the Option setting will appear as:

Options -Indexes +FollowSymLinks

Next, the DirectoryIndex setting will be configured so the master.php file is loaded as the default home page for the web site as follows:

DirectoryIndex master.php

When a URL is given that does not match an existing page, we wish to display a special content page. This is done with the ErrorDocument setting as follows:

ErrorDocument 404 /404

In order to use the mod_rewrite module, the rewriting engine must be enabled as follows:

RewriteEngine on

Finally, the URL rewriting rules will be defined. These will remap all web page URLs to the master page. The first rule maps all URLs ending with a forward slash to the master.php page, and sets the contentpage parameter to the URL path with “index” appended. This will cause the master page to load the index file corresponding to that URL path.

RewriteRule ^(.*)/$ /master.php?contentpage=$1/index [L]

The second rule includes a couple of conditions to check whether the specified URL matches an existing file or folder under the {webroot} folder. This way, any images, javascript files and CSS files under the {webroot} will not have their URLs remapped. Any other URLs will be remapped to the master.php file with the contentpage parameter set to the URL path. Additionally any GET parameters from the original URL will be appended to the remapped URL.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /master.php?contentpage=$1&%{QUERY_STRING} [L]

The Master Page

There is quite a lot to the structure of the master page file, so I will break up the discussion of it into digestible portions. The first half of the file is predominantly PHP code while the latter half is mainly HTML.

The master.php file begins with an opening php tag followed by a definition of the character encoding for the file. This helps the browser correctly interpret any non-ASCII characters found in the resulting HTML.

<?php
    header('Content-type: text/html; charset=utf-8');

Next is the definition of a PHP function used for resolving the path to included PHP files. Given a relative path to a PHP file, this function will attempt to find the file relative to the {webroot} and each folder listed in the include_path, in that order. If such a file is found, the absolute server path to the file is returned. If the file cannot be found, the function returns false.

/**
 * Check if a file exists under the webroot or an include path
 * And if it does, return the absolute path.
 * @param string $filename - Name of the file to look for
 * @return string|false - The absolute path if file exists, false if it does not
 */
function findRealPath($filename)
{
    // Check for absolute path
    if (realpath($filename) == $filename)
    {
        return $filename;
    }

    // Otherwise, treat as relative path
    $paths = explode(PATH_SEPARATOR, get_include_path());
    foreach ($paths as $path)
    {
        if (substr($path, -1) == DIRECTORY_SEPARATOR)
        {
            $fullpath = $path.$filename;
        }
        else
        {
            $fullpath = $path.DIRECTORY_SEPARATOR.$filename;
        }

        if (file_exists($fullpath))
        {
            return $fullpath;
        }
    }

    return false;
}

The remaining PHP code before the beginning of the HTML content contains those instructions to be executed when the master page loads. The first block of code, shown below, is used to define the root of the website (as seen by the browser). I find this useful when testing a web site on my desktop computer, as I generally have multiple web sites set up, each as a virtual directory. For instance, I have this master page example web site set up under a virtual directory named masterpage. I access this in the browser via http://localhost/masterpage/. The reason this is important is for resolving absolute paths to things like image files. I can’t use the path /image/example.jpg to reference the example.jpg file in the image folder under {webroot}. Instead, I have to use the path /masterpage/image/example.jpg. Later, when I move the web site to the production server, where it will be accessible via http://www.somecompany.com/, I want the site root to be set to ‘/’. Relative paths are unaffected by this, so it is only needed when resources are referenced with absolute paths.

$siteRoot = '/';
if (findRealPath('siteroot.php'))
{
    // Define an alternative root of the website
    // (useful when only a portion of the site will use the master page)
    require_once('siteroot.php');
}

The code shown above defaults the site root to ‘/’. If an include file named siteroot.php can be located, it will be included at this point. The assumption is that this include file will override the value of $siteRoot as shown here:

<?php
    // Override the site root for development testing
    $siteRoot = '/masterpage/';

The primary purpose of the next section of code is to determine the path to the content page. The URL rewriting set up in the .htaccess file will normally set a GET variable named contentpage to the relative path of the content page PHP file (except without the file extension). The path is relative to the {include}/content folder.

If no contentpage variable is specified, the master page will default to the index page. If the specified PHP content page file exists under the {webroot} folder, then that page will be loaded without using the master page. If the specified content page cannot be found, then it will try treating the content page path as a folder and look for an index file in that folder. If that fails, then the 404 error page will be used as the content page.

// Initialize default content page
$pagename = 'index';
if (isset($_GET['contentpage']))
{
    // If the contentpage variable is set, then use it for the page name
    $pagename=$_GET["contentpage"];
}

if (file_exists($pagename.'.php'))
{
    // URL refers to a page existing under the webroot, so just display page w/o using master page
    require_once($pagename.'.php');
}
else
{
    // Look for a PHP file matching the desired page name under the include/content folder
    if (!findRealPath("content/$pagename.php"))
    {
        // The page name might represent a folder, so look for the index.php file in such
        // a folder under the include/page folder
        if (findRealPath("content/$pagename/index.php"))
        {
            // Page name is a folder, so change page name to the index file in that folder
            $pagename = $pagename.'/index';
        }
        else
        {
            // Failed to find the page file, so display the 404 content page instead
            $pagename = '404';
        }
    }
?>

The remainder of the master.php file defines the HTML markup common to all pages in the web site. The first portion represents the HTML header. Here the DOCTYPE, opening <html> tag and <head> section are defined. The <head> tag contains a reference to the site’s main style-sheet. There is also a little PHP code to include any page-specific header content. If no such content file exists, then the default header content in the {include}/header/index.php file will be used instead.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
    <head>
        <meta http-equiv="Content-Type" content="text/html; utf-8">
        <link href="<?php echo $siteRoot; ?>style/site.css" rel="Stylesheet" type="text/css" />
        <?php
            // If a header file exists for the target content page, then use it. Otherwise
            // use the default header file
            if (!findRealPath("header/$pagename.php"))
            {
                require_once("header/index.php");
            }
            else
            {
                require_once("header/$pagename.php");
            }
        ?>
    </head>

To facilitate use of the onLoad handler (or any other handlers) for the <body> tag, PHP code like that shown below can be used to check for the existence of certain variables, and when present, set the value of the handler to the text of the variable. The variables would be defined when needed in the header content file for the page.

<body <?php if (isset($bodyLoad)) echo "onLoad=\"$bodyLoad\""; ?>>

The remainder of HTML will be specific to how you want your web site pages to be structured. There are only a couple of required items. One is that it should have a <div> tag containing the PHP code:

require("content/$pagename.php");

This <div> should be placed at whatever point is desired to have page-specific content. The other required item is only necessary if any of the pages in your website might have javascript-driven popup windows. In order for the content of such windows to appear in the proper z-order, it is best for the content to be defined in a hidden <div> after all other page content. See the <div> with id=”popupDiv” below for an example. This <div> contains PHP code to load the content from the {include}/popup/$pagename.php if that file exists. Additional javascript code is needed to make use of this popup content.

        <div id="fullWidthDiv">
            <div id="outerDiv">
                <div>
                    PHP Master Page Example Website
                </div>
                <div id="middleDiv">
                    <div id="navigationDiv">
                        <ul>
                            <li><a href="<?php echo $siteRoot; ?>">Home</a></li>
                            <li>
                                <a href="<?php echo $siteRoot; ?>products">Products</a>
                                <ul>
                                    <li><a href="<?php echo $siteRoot; ?>products/widget">Widget</a></li>
                                    <li><a href="<?php echo $siteRoot; ?>products/gizmo">Gizmo</a></li>
                                </ul>
                            </li>
                            <li>
                                <a href="<?php echo $siteRoot; ?>about">Company</a>
                                <ul>
                                    <li><a href="<?php echo $siteRoot; ?>contact">Contact</a></li>
                                </ul>
                            </li>
                        </ul>
                    </div>
                    <div id="contentDiv">
                        <?php
                            // Inserts the real page content here
                            require("content/$pagename.php");
                        ?>
                    </div>
                </div>
                <div id="footerDiv">
                    Copyright &copy; <?php echo date('Y'); ?> Some Company
                </div>
                <div id="popupDiv" class="hiddenStyle">
                    <?php
                        // Some content pages may include content designed as a javascript popup window
                        // By defining this content last, it will appear on top in the z-order. The
                        // hiddenStyle class will ensure this content is initially hidden.
                        if (findRealPath("popup/$pagename.php") != false)
                        {
                            include("popup/$pagename.php");
                        }
                    ?>
                </div>
            </div>
        </div>
    </body>
</html>
<?php
    }

Content Pages

Now for the good part… While the master page is fairly complex with a mix of PHP and HTML, the page content files are extremely simple by comparison. Since structural HTML markup is defined in the master.php file, the page content files need only define the truly content-related HTML. Here is the entire page content file for the home page of the example website:

<h1>Home Page</h1>
<p>This is the main content page for the PHP Master Page Architecture example.</p>
<p>The navigation menu to the left is common to all pages, and can be used to
navigate to each page of the site.</p>

While this is a trivial example, it goes to show just how simple the page content files can be. Although these are technically PHP files, the <?php ?> tags can be omitted if no PHP code is used.

Combination File/Folder Pages

URLs ending with a slash are interpreted as folders. The URL rewriting in the .htaccess file will automatically pass the path of the folder with “index” appended as the contentpage parameter to the master.php file. So the URL http://www.somecompany.com/products/ will be internally redirected to http://www.somecompany.com/master.php?contentpage=products/index. An interesting scenario arises though when the same URL is entered without the trailing slash. In this case, the URL rewriting in the .htaccess file will internally redirect it to http://www.somecompany.com/master.php?contentpage=products. Inside the master.php file, code will decide to load one of two possible content files. If the file {include}/content/products.php exists, it will be loaded. Otherwise, the file {include}/content/products/index.php will be loaded.

While it might seem a little strange to have the URLs …/products/ and …/products load different content pages, there may be some scenarios where this is useful. What gets a little trickier though is when both of these URLs load the same content page ({include}/content/products/index.php). The problem stems from how the browser determines the “working directory” for URLs as shown here:

URL Working Directory
http://www.somecompany.com/products/ http://www.somecompany.com/products/
http://www.somecompany.com/products http://www.somecompany.com/

Since the working directory is used to resolve relative paths to resources such as images, this can pose a problem when the same content file is evaluated with different working directories. The easiest method to overcome this problem is to use absolute paths for resources instead. This problem only arises for the index files in the content folders, and only when there is not a PHP file in the parent folder with the same name as the child folder.

Header Files

HTML <head> sections typically contain a <title> tag, and sometimes <meta>, <style>, <script> and other tags. Each page in the website can have its own unique <head> section. This is done by creating a PHP file with the same name and relative path as the content page PHP file, but under the {include}/header folder instead of {include}/content. If no such file exists, then the default header for the website will be used (located at {include}/header/index.php). Here is what the default header file looks like for the example website:

<title>Master Page Example</title>
<meta name="Description" content="An example content page for the PHP master page architecture." />
<meta name="Keywords" content="master page, PHP" />

Pop-Up Content Files

Page content to be shown in javascript-driven pop-up windows needs to be defined separately than the normal page content. The reason for this is such content needs to appear at different positions in the HTML document. The pop-up content typically appears at the end of the HTML document so when it is shown, it will appear above all other content. The master.php page encloses the inclusion of this pop-up content within a hidden <div>. It is up to the content page implementer to provide the javascript and event handlers to make this <div> visible when needed. The two product pages (Widget and Gizmo) in the example website contain pop-up windows shown when the product thumbnail image is clicked. The pop-up shows an enlarged image of the product. Both product pages share a common javascript file as shown here:

function changeStyle(id, newValue)
{
    document.getElementById(id).style.display = newValue;
}

function openPopup()
{
    if (document.getElementById)
    {
        changeStyle('popupDiv', 'inline');
        return false;
    }
    else
    {
        return true;
    }
}

function closePopup()
{
    changeStyle('popupDiv', 'none');
    return false;
}

openPopup() sets the display style of the <div> to ‘inline’ to show it, while closePopup() sets the display style of the <div> back to ‘none’ to hide it. In order to use this javascript, it must be included via a script statement in the <head> section. Shown below is the header file for the Gizmo product page where the ../script/popup.js file is included:

<title>Gizmo Page</title>
<meta name="Description" content="All about our Gizmo product." />
<meta name="Keywords" content="master page, PHP, Gizmo" />
<link href="../style/gizmo.css" rel="Stylesheet" type="text/css" />
<script type="text/javascript" src="../script/popup.js"></script>

The first portion of the Gizmo content page file is shown below to demonstrate the use of the javascript function openPopup() to display the pop-up window. The onClick event handler is configured on the <div> containing thumbnail image of the Gizmo product to call the openPopup() function.

<h1>Gizmo Page</h1>
<div id="imageDiv" onClick="return openPopup();">
    <img src="../image/gizmo_small.jpg" border="0" />
</div>

The actual pop-up content for the Gizmo product is very simple. It includes a title heading, a larger image of the Gizmo product, and a <div> containing the word “Close” which can be clicked to close the pop-up window. This <div> has its onClick handler configured to call the javascript function closePopup();

<h2>Gizmo Close-Up</h2>
<img src="../image/gizmo_big.jpg" border="0" />
<div id="closeDiv" onClick="return closePopup();">
    <h2>Close</h2>
</div>

Example Web Site

The complete set of files for the example website is available here for download. The example is configured to work for sites where the site root is under a ‘/masterpage/’ folder. To change this to a different location, edit the file ‘include/siteroot.php’, or delete the file to set the site root to ‘/’.

License

PHP Master Page Architecture Example Website
Designed and Developed by Daniel Brannon
Copyright © 2011 OSoSLO
http://ososlo.com/

Permission is hereby granted, free of charge, to any person obtaining a copy of this example website (the “Website”), to use the Website without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Website, and to permit persons to whom the Website is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Website.

THE WEBSITE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE WEBSITE OR THE USE OR OTHER DEALINGS IN THE WEBSITE.

About Daniel Brannon

Daniel Brannon founded OSoSLO in 2009 to provide software tools and services for business and technology professionals.
This entry was posted in Development Architecture, PHP. Bookmark the permalink.

3 Responses to Another PHP Master Page Architecture

  1. Richard Poeling says:

    Thank you for the great article. It gave me a lot to think about. I have a question for you, that perhaps you may know the answer. I’ve been struggling for the past couple hours to get this to work on a web hosting site. I believe they are running PHP as a CGI. I have a server at home as well and I’m running PHP as an Apache module. I’ve got your sample running now on both, but there is one thing that the web hosting site version does that I don’t like. It doesn’t show the original URL that whatever I clicked on is linked to. So for example when the master page is displayed for the first time and I hover over the Contact item, it shows me that if I were to click on the link the link is supposed to be: http://www.vgehts.com/masterpage/contact
    but when I click on the link it shows in the address bar:
    http://www.vgehts.com/masterpage/master.php?contentpage=contact

    I don’t have this problem on my home server. It shows in the address bar exactly as I would expect: http://www.vgehts.com/masterpage/contact

    Do you know what setting might be causing this problem?

    Thanks.

Leave a Reply