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;
}

Application Security: Model Object

The AppUserFactory model object plays a simple but crucial role in application security: the ApplicationSecurityDelegate passes credentials from the Login form to the AppUserFactory to retrieve the associated user record. If such retrieval is impossible, then the AppUserFactory returns nothing. If successful, an AppUser object is returned.

Listing: rsrc/model/application/AppUserFactory.php

<?php

class AppUserFactory extends DefaultAppUserFactory
{
   static function authenticatedUser($data = null)
   {
      // If no credentials provided, authentication fails
      if (! $data) return false;
      
      // If no password provided, authentication fails
      if (! isset($data['password']) or ! $data['password']) return false;
      
      // Retrieve an AppUser object using the credentials
      $email = strtolower($data['email']);
      $login = AppUserFactory::retrieveAppUser($email, 'lower(email)');
      
      // If no AppUser object could be created, authentication fails
      if (! $login) return false;
      
      // If the password does not match, authentication fails
      if ($login->password != md5($data['password'])) return false;
      
      // Return the authenticated user
      return $login;
   }
}

?>

Application Security: Login Page

Most web applications require some level of security. Here, for example, I have an area where I update the categories, articles, and other content that is displayed on the web site.

Model-View-Controller

A classic Model-View-Controller approach is used to implement initial security (that is, to let a user log on). The model is the AppUserFactory class that represents the user table in the underlying PostgreSQL database. The view is the LoginPage application module where the user presents credentials: most commonly, a username (or email address) and a password. The controller is the ApplicationSecurityDelegate object, which manages the interaction with the AppUser model object class and handles the results of successful (and unsuccessful) authentication.

Login: the View

The Login application module uses a LoginWorkflow (which is provided in the Istarel Workshop Application Framework). A LoginWorkflow object manages two states: IWMode::MODE_EDIT (edit) and IWMode::MODE_LOGIN (login). The edit state is presented as a form, so the Login application module needs to provide the elements that comprise the form.

Listing: rsrc/Login.php

<?php

class Login extends IWModule
{
   function requiredWorkflow() { return 'IWLoginWorkflow'; }
    
   function pageClass() { return 'LoginPage'; }
   function pageTitle() { return 'Login'; }
        
   function formElements()
   {
      return array(
         'header'   => new IWHeaderElement('Login Information'),
         'email'    => new IWFormElement('Email:', new IWTextElement(30, 100)),
         'password' => new IWFormElement('Password:', new IWPasswordElement(10, 12)),
         'uri'      => new IWHiddenElement(IWRequest::getValue('uri', null))
      );
   }
}

?>

The last element of the form captures the user's intended destination. If an individual comes directly to the login page, the value is null. If an individual tried to navigate directly to a secure module (either by typing the URL directly into her browser or from a bookmark), then that URL is encoded in the query string if authentication failed (as would be the case if the user was not logged in before attempting to go there).

Page Object

Every application module that can render content has an associated Page object, which in turn uses a template file as the basis for how its components are rendered.

Listing: rsrc/page/LoginPage.php

<?php

class LoginPage extends XHTML11Page
{
   function initialize()
   {
      $this->addStylesheet('css/login.css');
   }
   
   function templateFile()
   {
      return APPL_RSRC_DIR . 'page/html/login.html';
   }
   
   function expectedComponents()
   {
      return array('head', 'header', 'form', 'footer');
   }
}

?>

Listing: rsrc/page/html/login.html

<?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}
<body>

<div id="wrapper">
   <div id="header">{header}</div>
   <div id="content">{form}</div>
   <div id="footer">{footer}</div>
</div>

</body>
</html>