Implementing jQuery UI in a Web Application

A lot of people thought I was crazy for wanting to develop my own PHP framework, and 1400 hours of discovery and development later, they may have a point. At the end of the day, though, I have an extremely powerful and flexible framework for making database-driven web applications. Where even I drew the line, however, was with javascript. I made the decision to use jQuery because its design philosophy seems similar to my own.

My plan is to integrate jQuery into my PHP framework, but I need to learn a lot more about how jQuery works and refactor some of my core structural components to accommodate it. In the meantime, I can start to implement jQuery in individual applications.

Downloading jQuery

To enable jQuery requires only one javascript file, available from jquery.com. I chose to copy down the latest minimized version (jquery.min.js) to my web server to have as a local file. Almost everything is driven through events.

To have all the interface elements available, both javascript and stylesheets are required, available as themes from http://jqueryui.com/themeroller. I again downloaded the relevant files to my web server: When you select a theme from the gallery, there is an option to download the package (and you can fine tune which parts of a theme you want or need). Themes are also customizable.

When you download a theme, there a number of directories and files, but there are only two important ones: the theme folder (which contains the base stylesheet and all the supporting images) and jquery-ui javascript file.

Making jQuery Available

Now, you need only reference the downloaded javascript and stylesheets. In the applications I build, I tend to use abstract application modules that handle the common tasks (like loading libraries and content necessary for most of the modules in the application).

Partial Listing: rsrc/master/ApplicationModule

abstract class ApplicationModule extends IWModule
{
   ...
   
   function pageDidLoad($page)
   {
      $page->addJavascript('js/jquery.min.js');
      $page->addJavascript('js/jquery-ui-1.8.14.custom.min.js');
      $page->addStylesheet('css/smoothness/jquery-ui-1.8.14.custom.css');
   }
   
   ...
}

The rendered page for any ApplicationModule subclass has a header block with the appropriate references.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
   "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<link rel="stylesheet" type="text/css" href="/pod/lib/css/public.css" />
<link rel="stylesheet" type="text/css" href="/pod/lib/css/editor.css" />
<link rel="stylesheet" type="text/css" 
   href="/pod/lib/css/smoothness/jquery-ui-1.8.14.custom.css" />
<script src="/pod/lib/js/jquery.min.js"
    type="text/javascript" charset="utf-8"></script>
<script src="/pod/lib/js/jquery-ui-1.8.14.custom.min.js"
    type="text/javascript" charset="utf-8"></script>
</head>
<body>

...

</body>
</html>

Now, my application is ready for some jQuery UI goodness (to be presented in later articles).

SEO-friendly URLs

Search Engine Optimization (SEO) is a discipline that helps web sites stand out to internet search engines. Making a web site friendly to search engines involves favoring text over images, identifying important keywords of interest in searches, and using heading tags to emphasize key features of individual pages. One aspect that can implemented in the Istarel Workshop Application Framework (IWAF) is SEO-friendly URLs.

That implementation involves the Redirect model object class, its associated RedirectFactory class, and the ApplicationDelegate.

Redirect Model Object

Each redirect is represented by a tuple in the underlying application database, and each tuple has two key columns: public_path and application_module. The public_path is the SEO-friendly URL and the application_module is the module invoked by the framework to do actual work. There are also two secondary columns (key and value) that are used to define a specific tuple to be used by the application application_module (but are often null when no such data is needed).

Listing: /rsrc/model/application/Redirect.php

class Redirect extends DefaultRedirect
{
   function url()
   {
      $url = new IWURL($this->application_module);
      if ($this->key) $url->setParameter($this->key, $this->value);
      return $url;
   }
}

?>

Redirect Factory

The RedirectFactory class implements methods to support the creation and deletion of Redirect objects. These methods are invoked in two places. The ApplicationDelegate uses findApplicationModule() to determine the correct application module to invoke given a page request. Model object classes use their didSave() and didDelete() delegate methods to call createRedirect() and deleteRedirect().

Listing: /rsrc/model/application/RedirectFactory.php

<?php

class RedirectFactory extends DefaultRedirectFactory
{
    public static function findApplicationModule($url)
    {
        return RedirectFactory::retrieveRedirect($url, 'public_path');
    }
    
    static function createRedirect($path, $module, $key = null, $value = null)
    {
        // Does a redirect already exist for the public path?
        $counter = new ORMCounter('redirect');
        $counter->addCriteria(new ORMCriterion('public_path', $path));
        
        // If an entry already exists, nothing needs to be done
        if (1 == $counter->one()) return;
        
        // Create a new redirect entry
        $redirect = new Redirect;
        $redirect->setPublicPath($path)
                 ->setApplicationModule($module)
                 ->setKey($key)
                 ->setValue($value);
        $redirect->save();
    }
    
    public static function removeRedirect($public_path)
    {
        $delete = new ORMDelete('redirect');
        $delete->addCriteria(new ORMCriterion('public_path', $public_path));
        $delete->execute();
    }
}

?>

Application Delegate

Finally, the ApplicationDelegate object has a modifyPageRequest($url) method. This allows the application to translate the SEO-friendly URL into a proper application module invocation.

Partial Listing: /rsrc/ApplicationDelegate.php


function modifyPageRequest($url)
{
    if (! $url) $url = $this->startingPoint();
    
    // If the request is a proper view, no need to modify the page request
    if ($this->requestExists($url)) return $url;
    
    if (in_array($url, $this->forbiddenViews())) return $this->startingPoint();
    
    // Remap an SEO-friendly page request, if applicable
    if (! $module = RedirectFactory::findApplicationModule($url)) return $url;
    
    // Build and return url
    $url = $module->application_module;
    if ($module->key) $_GET[$module->key] = $module->value;

    return $url;
}

Handle Bad Page Requests in a Web Application

A common error in web applications is failed navigation. In some cases, the user has mistyped a URL; in other cases, the internal navigation of a web site is flawed. In (hopefully) rare cases, users are attempting some shenanigans.

Documenting Bad Requests

Whatever the cause of a bad request, the Istarel Workshop Application Framework (IWAF) traps those failures and invokes ApplicationDelegate->handleBadRequest(). Since I cannot know ahead of time what bad requests might be out there, I begin by logging them and then periodically reviewing the log file.

Partial Listing: /rsrc/ApplicationDelegate.php

<?php

function handleBadRequest($page_request)
{
    if (IN_PRODUCTION)
    {
        $fm = IWFileManager::defaultFileManager();
        
        $log_file = str_replace('rsrc/', '', APPL_RSRC_DIR) . 'log/bad-requests.log';
        $message  = date('Y-m-d h:i:s') . ' -- ' . $page_request . "\n";
        
        $fm->appendFile($log_file, $message);
        
        header('Location: ' . APPL_ROOT_DIR . $this->startingPoint());
        exit();
    }
}

?>

All this does is let me document any problems. For example, when I started logging the bad requests for Big Nerd Ranch, there were (generally) three different kinds of page request failures.

First, people were trying to reach the Big Nerd Ranch blog and forums by using URLs like http://www.bignerdranch.com/forums. Not an unreasonable guess, but incorrect: The forums and blog are found via subdomains of bignerdranch.com.

Second, people were trying URLs to classes, but not quite using the correct offering names. For example, the popular Cocoa class at Big Nerd Ranch is reached via http://www.bignerdranch.com/classes/cocoa_i, but many people were trying some variation of http://www.bignerdranch.com/classes/cocoa. Again, not a bad try, but completely inscrutable to the web application.

Third, there are obnoxious malcontents who think they can choose a clever URL and gain illegal access to code or data.

Apache's mod_rewrite

For URL with any kind of pattern to them, Apache's mod_rewrite is an excellent way to redirect a bad request before it becomes a bad request. My base mod_rewrite implementation simply ensures that the application framework handles all script-based requests.

RewriteEngine On
RewriteRule !\.(js|ico|gif|jpg|png|css|pdf|xml)$ index.php

The forum and blog requests very much represent a simple pattern. Namely, if the request begins with some variation of "forum" or "blog", then I should redirect the use to the appropriate subdomain. To be a bit more versatile, I will also ignore the case of any request (hence the bracketed NC). Whenever you intend to redirect a user, you want append [L] to the rewrite rule, which tells Apache: Do not apply any later rules!

RewriteEngine On
RewriteRule ^(phpbb|forum|board) http://forums.bignerdranch.com [NC,L]
RewriteRule ^blog http://weblog.bignerdranch.com [NC,L]
RewriteRule !\.(js|ico|gif|jpg|png|css|pdf|xml)$ index.php

Specific Redirects

One module I always build into my applications is a redirect facility. This enables me to have modules reachable via SEO (and user) friendly URLs. So, instead of http://www.bignerdranch.com/OfferingView?id=1 (other URLs might have far more complex query string parameters), the Cocoa page at Big Nerd Ranch is reached via http://www.bignerdranch.com/classes/cocoa_i.

I could use mod_rewrite here as well, putting a series of specific rewrite rules in place to handle mistaken URLs, where [PT] tells Apache to pass the result through to the next handler.

RewriteEngine On
RewriteRule cocoa_i$ classes/cocoa_i [NC,PT]
RewriteRule !\.(js|ico|gif|jpg|png|css|pdf|xml)$ index.php

Instead, however, for these specific kinds of redirects, I create a redirect entry in the administrator modules for the application.

Forbidden Requests

Finally, I like to utilize the forbiddenViews() method of the ApplicationDelegate, which returns an array of page requests that the framework should cause to silently die and simply return the user to the designated starting point for the application (typically the home page).

Partial Listing: /rsrc/ApplicationDelegate.php

<?php

function forbiddenViews()
{
    return array('track.php');
}

?>

Though not shown here, a page request can also be modified by the ApplicationDelegate, which allows for more sophisticated handling of inappropriate requests. That modifyPageRequest() method is also how I make use of the data from the redirect module to invoke the appropriate application module.