Caching Result Sets in PHP: Object Interaction Within a Caching System

In this article, we work directly with a standalone caching class, showing how it interacts with other objects. We will work with aggregation and composition to achieve our goals, which include implementing a time expiry based caching mechanism.

Here you have it. This is the fourth part of the series “Caching result sets in PHP.” Through the previous article, we went a long way, by developing an extensible result set caching class, which is aimed specifically at reducing one of the most common workloads often associated with database-driven websites: repetitive query execution.

We arranged that, after performing regular SELECT statements, the obtained result set is stored in a simple cache file for faster retrieval. While the logic involved in caching result sets has been utilized for quite a long time, the different approximations for tackling this issue are certainly numerous.

However, when we’re working on large projects, an object-oriented solution probably is best for managing all of the problems that arise, due mainly to the huge size of applications. And definitely, a caching system is yet another piece within the development environment that can be easily addressed through the object-oriented paradigm.

According to this concept, I’ve hopefully demonstrated that a caching class may be created without the need to get too deep into complex programming definitions. Although following the practical approach may lead quickly to developing simple applications, I firmly recommend being armed with the proper theory and then using it to deploy well-structured projects.

It seems that we’re not very close to working directly with a caching class. Well, I don’t think so. If we step back to the previous article, certainly we’ve developed such a class. However, it’s rather conceived as a programming object that interacts with at least two additional objects, and not as a standalone class.

Therefore, we need to complete the scenario, not only showing how the class may be correctly implemented, but how the additional classes interact with each other.

Are you feeling inclined to work with aggregation and composition? All right, let’s get started.

{mospagebreak title=The first link in the caching process: looking at the “Cache” class}

In order to get a better understanding of the interaction process between classes, let’s remind ourselves of how the “Cache” class looks, since it’s the first relevant structure within the caching system. To refresh our memories, here is the complete definition for this class:

class Cache{
            var $mysql;  // instance of MySQL object
            var $result; // instance of Result object
            var $expiry; // cache expire time in seconds
            var $cacheFile; // cache file
            var $data; // result set array
            // constructor
            function Cache(&$mysql,$expiry=86400,$cacheFile=’default_cache.txt’){
                        $this->mysql=&$mysql;
                        (is_int($expiry)&&$expiry>0)?$this-
>expiry=$expiry:$this->mysql->isError(‘Expire time must be a
positive integer’);
                        $this->cacheFile=$cacheFile;
                        $this->data=array();
            }
            // if cache is valid, perform query and return a
result set. Otherwise, get results from cache file
            function query($query){
                        // check if query starts with SELECT
                        if(!preg_match(“/^SELECT/”,$query)){
                                   $this->mysql->isError(‘Invalid
query. Must start with SELECT’);
                        }
                        if(!$this->isValid()){
                                   $this->result=$this->mysql-
>query($query);
                                   $this->data=$this->write();
                        }
                        else {
                                   $this->data=$this->read();
                        }
            }
            // write cache file
            function write(){
                        if(!$fp=fopen($this->cacheFile,’w')){
                                   $this->mysql->isError(‘Error
opening cache file’);
                        }
                        if(!flock($fp,LOCK_EX)){
                                   $this->mysql->isError(‘Unable
to lock cache file’);
                        }
                        while($row=$this->result->fetchRow()){
                                   $content[]=$row;
                        }
                        if(!fwrite($fp,serialize($content))){
                                   $this->mysql->isError(‘Error
writing to cache file’);
                        }
                        flock($fp,LOCK_UN);
                        fclose($fp);
                        unset($fp,$row);
                        return $content;
            }
            // read cache file
            function read(){
                        if(!$content=unserialize
(file_get_contents($this->cacheFile))){
                                   $this->mysql->isError(‘Error
reading from cache file’);
                        }
                        return $content;
            }
            // determine cache validity based on a time expiry
trigger
            function isValid(){
                        if(file_exists($this->cacheFile)
&&filemtime($this->cacheFile)>(time()-$this->expiry)){
                                   return true;
                        }
                        return false;
            }
            // fetch cache row
            function fetchRow(){
                        if(!$row=current($this->data)){
                                   return false;
                        }
                        next($this->data);
                        return $row;
            }
            // fetch all cache rows
            function fetchAll(){
                        if(count($this->data)<1){
                                   $this->mysql->isError(‘Error
accessing cache data’);
                        }
                        return $this->data;
            }
            // count cache rows
            function countRows(){
                        if(!$rows=count($this->data)){
                                   $this->mysql->isError(‘Error
counting cache rows’);
                        }
                        return $rows;
            }
}

So far, so good. Since the class has been explained in detail over the third part of the series, I won’t stop long on it. Let’s take a brief look at some of its most important features, before jumping directly into the source code for the other classes.

As we’ve seen before, the “Cache” class allows you to store an entire result set in a cache file for a given time period. Once the expiry has been reached, a new query is run for getting “fresh” data, and then the cache generation is carried out again. As long as the cached data is valid, it will be read from the file, being available for some kind of further processing.

Asides from the regular methods to handle programmatically the cached data, the class exposes some additional methods for fetching either single rows or multiple rows, as well as the ability to count rows.

Indeed, these features don’t present any difficulties. However, there are some interesting things within the class definition. Note that database connectivity tasks, query execution, and error handling are delegated to different classes. This implicitly means that there must be other objects that are passed to the “Cache” class for carrying out these specific operations.

Essentially, we can see that all of the database work is done by a MySQL object, which is passed to the class by its constructor. Therefore, it’s time to take a look at the MySQL abstraction class, responsible for connecting to MySQL, selecting databases and running queries.

{mospagebreak title=The second link in the caching process: overview of the “MySQL” class}

Building a MySQL wrapping class is something that has been done many times, and probably you’ve been working with existing packages or developing your own MySQL class. Keeping this idea in mind, I won’t explain how to build such an application. Instead, I’ll show a rather simple version of a regular MySQL abstraction class, but one that will be useful for aggregation by the “Cache” class.

The structure of the “MySQL” wrapping class looks like this:

class MySQL{
            var $conId; // connection identifier
            var $host; // MySQL host
            var $user; // MySQL username
            var $password; // MySQL password
            var $database; // MySQL database
            // constructor
            function MySQL($options=array()){
                        // validate incoming parameters
                        if(count($options)>0){
                                    foreach($options as
$parameter=>$value){
                                               (!empty($value))?
$this->{$parameter}=$value:$this->isError(‘Invalid parameter
‘.$parameter);
                                   }
                                   // connect to MySQL
                                   $this->connectDB();
                        }
                        else {
                                   $this->isError(‘No connection
parameters were provided’);
                        }
            }
            // connect to MYSQL server and select database
            function connectDB(){
                        if(!$this->conId=mysql_connect($this-
>host,$this->user,$this->password)){
                                    $this->isError(‘Error
connecting to the server’);
                        }
                        if(!mysql_select_db($this-
>database,$this->conId)){
                                    $this->isError(‘Error
selecting database’);
                        }
            }
            // perform query
            function query($query){
                        if(!$this->result=mysql_query
($query,$this->conId)){
                                   $this->isError(‘Error
performing query ‘.$query);
                        }
                        // return new Result object
                        return new Result($this,$this->result);
            }
            // display errors
            function isError($errorMsg){
                       trigger_error($errorMsg.’ ‘.mysql_error
());
                       exit();
            }
}

As you can see, the above listed class is not rocket science. It presents the classical methods for connecting to MySQL, selecting a database, running queries and so forth. Also, I’ve opted to build a method that handles potential errors in a generic way, which provides a centralized point for managing errors within the client code.

However, as I’ve mentioned before, it’s much better having a separate class that carries out error handling, whether you’re working with PHP 4 or PHP 5 (where exceptions are well supported). Unfortunately, error handling is out of the scope of this series, so, for the moment, we’ll settle for having an “isError()” method.

All right, since the class is very understandable, it should be clear how it’s aggregated by the “Cache” class. Here, we’re using the power of aggregation to make the caching class work properly.

But, I mentioned that composition would play a relevant role in the developing process. So, where does composition take place? Well, if we take a look at the “query()” method, we see that an instance of a “Result” object is created. Certainly, this new object composes the “MySQL” object.

What’s more, we’re using the Factory Pattern at a very basic level, since the instantiation of a “Result” object is rather decoupled from the client code. Now, all of the classes nicely fit each other.

Having a “Result” object dynamically created directly implies the existence of the corresponding class. Thus, let’s jump into the next section to see the definition for the “Result” class.

{mospagebreak title=The third link in the caching process: a quick look at the “Result” class}

Very often it is much better to delegate the responsibility for manipulating database result sets to a separated class, rather that assigning these tasks to the same class that connects to MySQL. That’s the primary reason for the existence of the “Result” class. However, don’t be concerned about it. It only takes care of fetching data, counting table rows and other result-related operations.

Here is the proper list for the “Result” class:

class Result{
            var $mysql; // instance of MySQL object
            var $result; // result set
            // constructor
            function Result(&$mysql,$result){
                        $this->mysql=&$mysql;
                        $this->result=$result;
            }
            // fetch row
            function fetchRow(){
                        return mysql_fetch_array($this-
>result,MYSQL_ASSOC);
            }
            // count rows
            function countRows(){
                        if(!$rows=mysql_num_rows($this->result)){
                                   $this->mysql->isError(‘Error
counting rows’);
                        }
                        return $rows;
            }
            // count affected rows
            function countAffectedRows(){
                        if(!$rows=mysql_affected_rows($this-
>mysql->conId)){
                                   $this->mysql->isError(‘Error
counting affected rows’);
                        }
                        return $rows;
            }
            // get ID from last inserted row
            function getInsertID(){
                        if(!$id=mysql_insert_id($this->mysql-
>conId)){
                                   $this->mysql->isError(‘Error
getting ID’);
                        }
                        return $id;
            }
            // seek row
            function seekRow($row=0){
                        if(!mysql_data_seek($this->result,$row)){
                                   $this->mysql->isError(‘Error
seeking data’);
                        }
            }
}

Although the methods belonging to the above class might be appended to the MySQL class, it’s much more efficient having a separated structure that accomplishes all of the row-related operations. So, as you can see, this class allows us to fetch rows, count result rows, and even get the ID from the last inserted record.

Of course, more methods may be eventually added, useful for improving the functionality within the class. But, as I said before, we’ll keep it simple.

By this point, we’ve covered in detail each class that interacts with “Cache.” The next step is putting all of the pieces together and demonstrating a practical example.

{mospagebreak title=Chaining the links: putting the classes together}

Until now, we’ve covered in detail the different classes needed to make the “Cache” class work. The next thing to do is to put all the pieces together and implement a practical example. First, let’s list the complete source code for each class, and then implement the example. The full code listing is as follows:

// class MySQL
class MySQL{
            var $conId; // connection identifier
            var $host; // MySQL host
            var $user; // MySQL username
            var $password; // MySQL password
            var $database; // MySQL database
            // constructor
            function MySQL($options=array()){
                        // validate incoming parameters
                        if(count($options)>0){
                                   foreach($options as $parameter=>$value){
                                               (!empty($value))?$this->{$parameter}=$value:$this->isError(‘Invalid parameter ‘.$parameter);
                                   }
                                   // connect to MySQL
                                   $this->connectDB();
                        }
                        else {
                                   $this->isError(‘No connection parameters were provided’);
                        }
            }
            // connect to MYSQL server and select database
            function connectDB(){
                        if(!$this->conId=mysql_connect($this->host,$this->user,$this->password)){
                                    $this->isError(‘Error connecting to the server’);
                        }
                        if(!mysql_select_db($this->database,$this->conId)){
                                    $this->isError(‘Error selecting database’);
                        }
            }
            // perform query
            function query($query){
                        if(!$this->result=mysql_query($query,$this->conId)){
                                   $this->isError(‘Error performing query ‘.$query);
                        }
                        // return new Result object
                        return new Result($this,$this->result);
            }
            // display errors
            function isError($errorMsg){
                       trigger_error($errorMsg.’ ‘.mysql_error());
                       exit();
            }
}
// class Result
class Result{
            var $mysql; // instance of MySQL object
            var $result; // result set
            // constructor
            function Result(&$mysql,$result){
                        $this->mysql=&$mysql;
                        $this->result=$result;
            }
            // fetch row
            function fetchRow(){
                        return mysql_fetch_array($this->result,MYSQL_ASSOC);
            }
            // count rows
            function countRows(){
                        if(!$rows=mysql_num_rows($this->result)){
                                   $this->mysql->isError(‘Error counting rows’);
                        }
                        return $rows;
            }
            // count affected rows
            function countAffectedRows(){
                        if(!$rows=mysql_affected_rows($this->mysql->conId)){
                                   $this->mysql->isError(‘Error counting affected rows’);
                        }
                        return $rows;
            }
            // get ID from last inserted row
            function getInsertID(){
                        if(!$id=mysql_insert_id($this->mysql->conId)){
                                   $this->mysql->isError(‘Error getting ID’);
                        }
                        return $id;
            }
            // seek row
            function seekRow($row=0){
                        if(!mysql_data_seek($this->result,$row)){
                                   $this->mysql->isError(‘Error seeking data’);
                        }
            }
}
// class Cache
class Cache{
            var $mysql;  // instance of MySQL object
            var $result; // instance of Result object
            var $expiry; // cache expire time in seconds
            var $cacheFile; // cache file
            var $data; // result set array
            // constructor
            function Cache(&$mysql,$expiry=86400,$cacheFile=’default_cache.txt’){
                        $this->mysql=&$mysql;
                        (is_int($expiry)&&$expiry>0)?$this-
>expiry=$expiry:$this->mysql->isError(‘Expire time must be a
positive integer’);
                        $this->cacheFile=$cacheFile;
                        $this->data=array();
            }
            // if cache is valid, perform query and return a
result set. Otherwise, get results from cache file
            function query($query){
                        // check if query starts with SELECT
                        if(!preg_match(“/^SELECT/”,$query)){
                                   $this->mysql->isError(‘Invalid
query. Must start with SELECT’);
                        }
                        if(!$this->isValid()){
                                   // read from MySQL
                                   $this->result=$this->mysql-
>query($query);
                                   $this->data=$this->write();
                        }
                        else {
                                   // read from cache file
                                   $this->data=$this->read();
                        }
            }
            // write cache file
            function write(){
                        if(!$fp=fopen($this->cacheFile,’w')){
                                   $this->mysql->isError(‘Error
opening cache file’);
                        }
                        if(!flock($fp,LOCK_EX)){
                                   $this->mysql->isError(‘Unable
to lock cache file’);
                        }
                        while($row=$this->result->fetchRow()){
                                   $content[]=$row;
                        }
                        if(!fwrite($fp,serialize($content))){
                                   $this->mysql->isError(‘Error
writing to cache file’);
                        }
                        flock($fp,LOCK_UN);
                        fclose($fp);
                        unset($fp,$row);
                        return $content;
            }
            // read cache file
            function read(){
                        if(!$content=unserialize
(file_get_contents($this->cacheFile))){
                                   $this->mysql->isError(‘Error
reading from cache file’);
                        }
                        return $content;
            }
            // determine cache validity based on a time expiry
trigger
            function isValid(){
                        if(file_exists($this->cacheFile)
&&filemtime($this->cacheFile)>(time()-$this->expiry)){
                                   return true;
                        }
                        return false;
            }
            // fetch cache row
            function fetchRow(){
                        if(!$row=current($this->data)){
                                   return false;
                        }
                        next($this->data);
                        return $row;
            }
            // fetch all cache rows
            function fetchAll(){
                        if(count($this->data)<1){
                                   $this->mysql->isError(‘Error
accessing cache data’);
                        }
                        return $this->data;
            }
            // count cache rows
            function countRows(){
                        if(!$rows=count($this->data)){
                                   $this->mysql->isError(‘Error
counting cache rows’);
                        }
                        return $rows;
            }
}

Although the code is rather lengthy, keep in mind that each class can be included as a separated file, when they’re required. Having a wealth of classes that do the hard work for us, implementing a result set caching system is as simple as this:

// connect to MySQL
$db=new MySQL(array(‘host’=>’host’,'user’=>’user’,'password’=>’password’,
‘database’=>’databasename’));
// instantiate a new Cache object, valid for 24 hours (time
expiry triggered caching)
$cache=&new Cache($db,86400);
// perform query and store results in cache file
// if cache is not valid, force a new cache generation
$cache->query(‘SELECT * FROM users’);
// loop over rows and display results
while($row=$cache->fetchRow()){
            echo $row['firstname'].’&nbsp;’.$row['firstname'].’<br />’;
}
// count rows
echo ‘Total number of rows ‘ .$cache->countRows();

The above example obtains a result set from MySQL, by getting rows from a sample “users” database table, and stores the data in the cache file for 24 hours. All of the requests that occur during that time will fetch data directly from the cache, avoiding query executions.

When the time expiry is reached, the data is again retrieved from MySQL and stored in the cache file. Since the “Cache” class allows you to perform additional tasks, we’ve counted the total number of rows, as you can see in the example.

Say that we want to fetch all of the rows at once. The process is accomplished like this:

// get all rows
$rows=$cache->fetchAll();
// display data or do something else
foreach($rows as $row){
            echo $row['firstname'].’&nbsp;’.$row
['firstname'].’<br />’;

}

Isn’t that simple? Definitely. The “Cache” class is very flexible, as you’ll probably agree. Due to its extensible nature, it might be improved by adding some other methods, useful for manipulating cached data.

Wrapping up

Finally, we’re finished. In this fourth part of the series, we’ve successfully made all of the required classes work together, implementing a time expiry based caching mechanism, useful for reducing the workload associated with multiple query execution.

Since the object-oriented solution has proven to be efficient in caching result sets, in the next part of the series we’ll port the whole code that we’ve worked with to PHP 5. Certainly, the topic is really exciting, so get ready for the next challenge. See you soon!

[gp-comments width="770" linklove="off" ]
antalya escort bayan antalya escort bayan