1 | 2

6 - 9   [9]

Viva PEAR::Cache_Lite

Also relates to PHP

With the latest release of the PEAR::Cache the Cache_DB extension has been removed. I first came across Cache_DB in the PHP Cookbook last year and ran several tests to integrate it as a principle caching mechanism in the Content Management System. However, the class offered no noticeable benefit in performance, and when several database calls were being executed for a single page, it did not make sense to use a cache per query. So as an alternative I decided to test drive the PEAR::Cache_Lite package instead. As its nomenclature suggests, this is an excellent lightweight package and offers the perfect solution for filling the caching void in content retrieval. One of the package extensions is <a href="http://pear.php.net/manual/en/package.default.cache-lite.cache-lite-function.cache-lite-function.php" title="Current End User Documentation on Cache_Lite_Function">Cache_Lite_Function</a>, a caching class for functions. This allows for several database calls and the template system to be wrapped in a single method for caching:


class SS_Content {

  // [..snip..]
  
  function fetch($action, $caching = NULL)
  {
    $this->action = $action;
    
    if ($caching === NULL) {
      if (isset($this->_caching)) {
        $caching = $this->_caching;  
      }
    }
    
    if ($caching === SS_CACHING_ON) {
      return $this->retrieveCache();
    }
    else {
      return $this->retrieve();  
    }   
  }
  
  function retrieveCache() {    
    require_once "Cache/Lite/Function.php";

    $options = array(
      'cacheDir'     => CACHE_DIR,
      'lifeTime'     => CACHE_EXP
    );
    $objCache = new Cache_Lite_Function($options);
    return $objCache->call(
      $GLOBALS['_SS_Content_Obj'] . '->retrieve');           
  }
  
  function retrieve()  { 
  
    // this is where the content is built
    // eg database retrieval and content 
    // creation with a template system
  
  }
  
  // [..snip..]
}

This is only a raw example, since within its actual context, SS_Content is actually an abstract class serving specific modular extensions. But it shows how simple Cache_Lite is to use - create the object and invocate the <a href="http://pear.php.net/manual/en/package.default.cache-lite.cache-lite-function.call.php" title="End user documentation on the call method of Cache_Lite_Function">Cache_Lite_Function::call()</a> method. There is one area of caution when using Cache_Lite in an OOP environment like this. The method will look for an object in the global name space equating to the name preceding the accessor operator in the parameter string of the method. How this is achieved is dependant on the application and naming conventions. One solution is to register the variable name of the object in a global variable during instantiation of the class (as demonstrated above).

Melon Fire provides a terse but detailed introduction to Cache_Lite, Cache_Lite_Function and its counterpart Cache_Lite_Output.

Posted on Jun 20, 2004 at 03:40:35. [Comments for Viva PEAR::Cache_Lite- 0]

Enforce Referential Integrity With DataObjects

Also relates to PHP and Databases

The PEAR::DB_DataObject package is rapidly becoming an integral part of my development arsenal. Building small scale content management becomes surprisingly simple, producing code in the CMS API layer that is concise and clear. Here is a little walk through comparing this package with its uncle - PEAR::DB - for batch deletions.

Take a simple shopping cart that includes customer and order entities as part of a wider database. These are inevitably linked in a one-to-many relationship on the customer_id field. As part of a spring clean, we want to remove all customer records that do not have an associated order record (i.e. all customers that have not placed any orders), while enforcing referential integrity. A typical DML method to achieve this is a LEFT JOIN as follows:


SELECT
  customers.customer_id
FROM
  customers LEFT JOIN orders
ON
  customers.customer_id = orders.customer_id
WHERE
  orders.customer_id = NULL  

The following snippet uses PEAR:DB to perform the batch deletion, based on the result of the above SQL:


$db =& DB::connect(ss_dsn());
$query = "SELECT customers.customer_id "
       . "FROM customers LEFT JOIN orders "
       . "ON customers.customer_id = orders.customer_id "
       . "WHERE orders.customer_id IS NULL";
$result = $db->getAll($query);
$prh = $db->prepare('DELETE FROM customers WHERE customer_id = !');
$sth = $db->executeMultiple($prh, $result);

Here, the getAll() method is used to return the set of results as an array of arrays, so they can be plugged directly into the executeMultiple() method with a prepared statement. The same result could be achieved by walking through the result set deleting each record in turn. For example:


$result = $db->query($query);
while ($result->fetchInto($record, DB_FETCHMODE_OBJECT)) {
  $db->execute($prh, array($record->customer_id));
}

Ok, now let us walk through the process using DB_DataObject. The approach is quite different. One of DataObject's main features is to make the DML virtually transparent, and rather than working with field values stored in arrays, we are working directly with the entities mapped onto objects.

  1. Create an instance of the customer object
  2. All customers records need to be examined, so set a select condition to retrieve all records
  3. Create a result set by executing the find() method
  4. Iterate over each customer and test for the existence of a link by attempting to create a linked orders object
  5. If the test fails, remove the customer

Here is the snippet:


$customer =& DB_DataObject::factory('customers');
$customer->whereAdd('user_id > 0');
$customer->find();
while ($customer->fetch()) {
  if(!$customer->getLink('customer_id', 'orders', 'customer_id')) {
    $customer->delete();
  }
}

Removing the SQL from this layer makes the code considerably less error prone, especially if the relationship is threefold or greater. This approach can be readily adapted to the case of a single deletion. To ensure referential integrity will not be violated when removing a record, SQL statements must be executed against all entity classes holding a relationship with the current record. With DataObjects the database queries are executed internally, and all the current layer needs to do is test for link objects. Multiple relationships can actually be stored in a links.ini configuration file, and the integrity test achieved by simply calling the getLinks() method.

The release of PHP 5 Release Candidate 1 sees a dramatic step forward for PHP into Object Orientated design and development. PEAR::DB_DataObjects complements this nicely.

Posted on Mar 27, 2004 at 04:51:18. [Comments for Enforce Referential Integrity With DataObjects- 0]

PEAR HTTP_Session Package

Also relates to PHP

A few months ago I discussed a method I have adopted for PHP session management. It is a simple process that uses a forced server redirect the first time a user arrives at a page to confirm cookies are enabled for session storage. This allows a message to be displayed to the user if cookies are disabled rather the application failing at the next step (eg after login/registration submission).

I have recently been updating my object-oriented PHP CMS with PEAR packages, to reduce the coding and maintenance I have to perform. The HTTP_Session module is no exception. This is a lightweight interface for all the inbuilt session management functions and as a class it can be readily extended with added functionality. The PHP CMS only stores a session variable for login details to persist the administrator across multiple HTTP requests, so I only really have need for the get and set methods.


HTTP_Session::start();

$user =& new CMS_User;

if (HTTP_Session::get("user")) {
  $user->loggedIn = TRUE;
} 
elseif ($user->requestLogIn()) {
  HTTP_Session::set("user", $user->getDetails());
}

At the same time, I was keen to extend the class with the cookie test described above.


class CMS_Session extends HTTP_Session
{
  [..]
  
  function test()
  {
    if (HTTP_Session::isNew()) {
      if (HTTP_Session::detectID() == NULL) {
        if (!isset($_GET[HTTP_Session::name()])) {         
          $sess_id = substr(SID,(strrpos(SID,"=")+1));
          $redirect_url = CMS . CMS_REDIRECT_QUERY . $sess_id;
          header("Location: " . $redirect_url);
          exit; 
        }
        else {
          $old_id = $_GET[HTTP_Session::name()];
          HTTP_Session::destroy();
          HTTP_Session::start("CMS", $old_id);
          HTTP_Session::destroy();
          throw_error(
            new CMS_Session_Error(CMS_ERROR_COOKIES_DISABLED)
                     );
        }
      }
    }
    return true;    
  }
  
  [..]
}

This method is called statically following CMS_Session::start() on the login page. Since the file serves the welcome page for the CMS as well as login, HTTP_Session::isNew() is tested to allow users with an established session to skip straight out of the method. If the session is new and a session ID can not be detected (all users on first call to the page), the forced redirect will take place with a GET query string. On reloading, if the session ID still can not be detected, the user does not have enabled cookies (the GET query string confirms it is a reload). Therefore, the session can be destroyed and an error displayed to the user - in this example by throwing a CMS_Session_Error.

Note that the built-in SID constant does not need to be passed in the query. Where the session does not persist (cookies disabled) a second session will be created following the redirect. Passing the query simply allows both sessions to be destroyed - Store the (original) session ID from the GET query, destroy the new session, regenerate the old session with the original session ID and destroy that. This is not really necessary, since lingering invalid sessions can be cleared out regularly from the temporary directory or session database using a cron job.

A further extension to HTTP_Session can accomodate applications running on virtual (shared) servers by hash encoding the session data to both scramble the stored information and prevent the packet from being intercepted and changed during tranmission.


function setEnv($var, $val)
{
  $str = $var . "-" . $val;
  $str = base64_encode($str . "," .
                       md5($str . CMS_SESSION_HASH));                    
  HTTP_Session::set($var, $str);       
}

function getEnv($var)
{
  $str = HTTP_Session::get($var);
  $str = base64_decode($str);   
  list($details,$session_hash) = split(",",$str);   
  if (md5($details.CMS_SESSION_HASH) == $session_hash) {
  list($var, $val) = explode("-", $details);
  return $val;
}

This is a simple encoding example that could be used for immutable session data. (For example user login details) A copy of the session variable is retrieved in the get method, to ensure any manipulation is not performed directly on the value of the session variable.

HTTP_Session also offers a range of pre-built functionality including database storage and idle and expiration time management.

Posted on Mar 07, 2004 at 20:49:56. [Comments for PEAR HTTP_Session Package- 1]

MySQL Schema Via PEAR::DB_DataObject

Also relates to PHP and MySQL

I have been updating some of my PEAR classes and stumbled upon the DB_DataObject package. I have actually been trying to implement a data modelling layer within my PHP CMS project for some time, and this package is a comprehensive collection of methods to achieve just that abstraction. Configuration was relatively painless and with a set of data objects ready to go I have been testing their application in the CMS API.

In the original interface, I was parsing initialisation files (parse_ini_file) to load the table schema from which the appropriate form or table model could be constructed utilising a mix of PEAR's HTML packages and custom classes. The main problem with this was that changes in the MySQL table schema had to be manually updated in the ini file.

When a set of data objects are created with DB_DataObject::createTables(), an ini file is also created that records the database schema. This file is parsed internally when a new data object is instantiated for a particular entity. The table schema is instantly available by calling DB_DataObject::table().


$obj =& DB_DataObject::factory('data_object');
$dbo_schema = $obj->table();
echo "<pre>";
foreach($dbo_schema as $fld_name => $fld_type)
{
  printf("%-20s", $fld_name);
  if ($fld_type & DB_DATAOBJECT_INT) echo "int ";
  if ($fld_type & DB_DATAOBJECT_STR) echo "str ";  
  if ($fld_type & DB_DATAOBJECT_DATE) echo "date ";  
  if ($fld_type & DB_DATAOBJECT_TIME) echo "time ";  
  if ($fld_type & DB_DATAOBJECT_BOOL) echo "bool ";  
  if ($fld_type & DB_DATAOBJECT_TXT) echo "txt ";  
  if ($fld_type & DB_DATAOBJECT_BLOB) echo "blob ";  
  if ($fld_type & DB_DATAOBJECT_NOTNULL) echo "not null ";  
  if ($fld_type & DB_DATAOBJECT_MYSQLTIMESTAMP)
     echo "timestamp ";  
  echo "\n";
}
echo "</pre>";

The returned schema contains key/value pairs where the key is the field (attribute) name, and the value is the datatype(s) as defined when the ini file is created. The datatype value is conditionally tested against the named constants with the bitwise & operator to determine the actual types defined. This works fine for a table model, where the schema fields and a generalised data type is all I require [the table model uses DOM scripting to allow column reordering based on general data types]. However, for the form model, I need to know the maximum allowed size for fields, default values and other metadata to compose accessible and usable form controls. For this the obvious solution is to fall back to the MySQL layer itself, and run a "SHOW COLUMNS" SQL query.

A PEAR::DB object is already a persistant property of the instantiated data object, so if this can be retrieved a standard DB::getAll() query can be run.


$db =& $obj->getDatabaseConnection();
$mysql_schema = $db->getAll("SHOW COLUMNS FROM "
                              . $obj->tableName());
echo "<pre>";
foreach($mysql_schema as $field)
{
  printf("%-8s %-25s %-10s %-6s %-20s\n",
         $field[0], $field[1],
         (empty($field[2]) ? "[ NOT NULL ]" : ""),
         $field[3], $field[4]);  
}
echo "</pre>";

DB_DataObject::getDatabaseConnection() returns a reference to the PEAR::DB object. To keep the code snippet general DB_DataObject::tableName() is called to retrieve the table name within the SQL query. Example output of this code is:


id      smallint(5)   unsigned  [ NOT NULL ]   PRI
name    varchar(80)             [ NOT NULL ]
url     varchar(100)                           MUL
version float(3,1)              [ NOT NULL ]          1.0
amended timestamp(14)
added   timestamp(14)

This provides all the data I need to create a corresponding set of form controls. If the CMS model is amending an entity, loading the data is just a case of calling $obj->get(id), and the data can be updated with $obj->update() once it has been passed through a data validator, and the new values assigned to the object's properties.

Next is to put the SQL builder aspect of DB_DataObject to test with some of the more complex triple join queries currently in the CMS model…

Posted on Feb 28, 2004 at 00:52:09. [Comments for MySQL Schema Via PEAR::DB_DataObject- 1]

Breadcrumbs Trail

[ Home ] -> TW Blog -> PEAR
Site Map

The Severn Solutions website achieves the following standards:

[ XHTML 1.0 ] [ CSS 2 ] [ WAI AA ] [ Bobby AA ]

Page compiled in 0.277 seconds