Chapter 14. Caching: making it faster – Zend Framework in Action

Chapter 14. Caching: making it faster

This chapter covers
  • How caching works
  • Introducing the Zend_Cache component
  • Using Zend_Cache frontend classes
  • Choosing what to cache and for how long

Caching refers to taking a computationally expensive task, such as a database query or intense mathematical calculation, and storing the result so that next time the result can be quickly retrieved from the cache instead of repeating the task. This chapter will explain the benefits of caching, show you how to take advantage of these benefits using Zend Framework’s Zend_Cache, and guide you through the process for choosing appropriate cache settings for your application.

On one project, Steven’s client was told that the shared hosting account they were using was consuming too many resources and that, if the issue wasn’t resolved, the account would be suspended. The site served over 40,000 members, many of whom visited the site on a daily basis. Having the account suspended would have been a disaster. The site was also slowing to a crawl, and database and memory errors had started to appear.

On analyzing the site’s load, Steven discovered that the database was causing the load issues and slowness. He investigated further and found that one query was causing the problem. A piece of code on the site added together one field from all rows in a table and returned the result. At first, the task ran fine, but as the number of rows increased, first into the thousands, then tens of thousands, this query became slower and slower. This code was on every page, and it was obvious just how the load issue had come about.

Assuming there were 1,000 visitors per day, and each visitor went to 10 pages, the query was being run 10,000 times per day. As most visitors would hit the site around the same time each morning, most of the 10,000 queries would be performed in a short period of time, say over an hour or two. No wonder the site was falling over!

The solution was simple. The result of the query was stored in cache, and the cache was set to refresh hourly. The query was now running only once per hour instead of several thousand times per hour. Instantly the server load was reduced to almost nothing, and the site performed better than ever. Ultimately, caching saved the cost of upgrading the hosting account (or of losing it, for that matter).

14.1. Benefits of Caching

The core benefit of caching is to reduce resource usage and deliver content more quickly. Reducing resource usage means you can serve more users from a lower-cost hosting account, and delivering content more quickly results in a much better user experience.

Caching a database request means that your application spends less time connecting to the database, which translates into more resources available for database operations that can’t be cached. It also reduces page-rendering time, which frees up your web server resources to deliver more pages.

Caching gives you great leverage. Where normally an increase in traffic would mean an increase in load-intensive tasks such as database requests, the only increase with caching is in checking and retrieving the cached data. The number of database requests (or other computationally intensive tasks) will stay the same regardless of the increase in traffic.

Caching doesn’t only apply to database queries. Zend_Cache is very flexible and can be used for everything from database queries to function calls. You can apply it to any load-intensive operation in your application, but it’s important to understand how caching operates so that you know when and how to apply it.

14.2. How Caching Works

Caching is an economical way to increase the speed of your application and reduce server load. For most PHP applications, the most resource-intensive and time-consuming task is performing database operations.

Listing 14.1 shows typical code that might be used to retrieve information from a database using Zend_Db_Table.

Listing 14.1. Retrieving database data without caching
Zend_Loader::loadClass('Products');
$productTable = new Products();
$products = $productTable->fetchAll();

This is standard Zend Framework code, where we load the Product model and call fetchAll() to retrieve a list of all products in the database. A flow diagram showing the overall process is shown in figure 14.1.

Figure 14.1. A database request without caching

In most applications, there will be multiple database queries—tens or even hundreds. Every request to the database uses processor time and memory. If you have many visitors on your site at a time, your server resources can very quickly be consumed. This is common in high-traffic situations or on low-budget servers where resources are minimal.

Listing 14.2 shows how Zend_Cache’s caching abilities can be added to the previous code example.

Listing 14.2. Retrieving database data with caching

A Zend_Cache cache object is created using the factory() method, which returns a frontend cache object that is attached to a backend. In this case, we use the “core” frontend class (Zend_Cache_Core) and the “file” backend (Zend_Cache_Backend_File), which means that the cached data is stored to files on disk.

To store data to a cache, we need a unique name , and we use the load() method to retrieve the data from the cache . If the data isn’t in the cache (or has expired), we can perform our resource-intensive operation and use save() to store the results to the cache .

A flow diagram showing the overall process with caching in place is shown in figure 14.2.

Figure 14.2. A database request using caching

In this way, the query is run once (depending on how long your cache is set to last), and the cache is able to serve out the data hundreds, thousands, or millions of times without having to interact with the database again until the cache expires.

Caching relies on two principles:

  • The unique identifierWhen the cache system checks to see if a cache result already exists, it uses the unique identifier to check. It’s very important to ensure that your unique identifiers are indeed unique; otherwise you’ll have two separate items using the same cache and conflicting with each other. The best way to ensure this is to have the caching code for that identifier only once in your code, such as in a function or method, and call it from multiple places if necessary.
  • The expiry timeThis is the expiry time for a cache, after which its contents are regenerated. If the cached data changes infrequently, you might set the expiry time to 30 days. If something changes frequently but you have a high-traffic site, you’d choose to set the expiry time to 5 minutes, 30 minutes, or an hour. Choosing the right expiry time is discussed in section 14.4.

Figure 14.3 shows how a caching system determines whether or not to run the intensive task or to load the result from cache.

Figure 14.3. The decision-making process of a caching system

A caching system uses the unique identifier to check for an existing cache result. If a result does exist, it will check to see if the result has expired. If it hasn’t expired, the cached result is returned—this is known as a cache hit. If there is no existing cache result or the existing cache result has expired, this is known as a cache miss.

Now that we’ve looked at how caching works, let’s look at how to add caching to an application using Zend_Cache.

14.3. Implementing Zend_Cache

Implementing Zend_Cache is very simple. Once you discover how easy it is, you’ll want to use it everywhere!

The Zend_Cache options are divided into two main parts, the frontend and the backend. The frontend refers to the operation that you’re caching, such as a function call or a database query. The backend refers to how the cache result is stored.

As we saw in listing 14.2, implementing Zend_Cache involves instantiating the object and setting the frontend and backend options. Once this is done, you perform the cache check. Each frontend does this in a different way, but essentially you ask if there is an existing cache; if there isn’t you’ll proceed into the code required to generate the result to be stored in cache. You then command Zend_Cache to store the result.

Listing 14.3 shows an example of using Zend_Cache to cache a database query using a model.

Listing 14.3. Example usage of Zend_Cache
Zend_Loader::loadClass('Zend_Cache');
$frontendOptions = array(
  'lifetime' => 60 * 5, // 5 minutes
  'automatic_serialization' => true,
);
$backendOptions = array(
   'cache_dir' => BASE_PATH . '/application/cache/',
   'file_name_prefix' => 'zend_cache_query',
   'hashed_directory_level' => 2,
);
$query_cache = Zend_Cache::factory('Core', 'File', $frontendOptions,
  $backendOptions);

$cacheName = 'product_id_' . $id;
if(!($result = $query_cache->load($cacheName))) {
   Zend_Loader::loadClass('Product');
   $productTable = new Product();
   $result = $productTable->fetchRow(array('id = ?' => $id));
   $query_cache->save($result, $cacheName);
}

The frontend options control how the cache operates. For example, the lifetime key determines how long the cached data is to be used before it’s expired. The backend options are specific to the type of cache storage that is used. For Zend_Cache_Backend_File, information about the directory (cache_dir) and how many directory levels to use (hashed_directory_level) are important.

The rest of the code is similar to that in listing 14.2, only this time the cache name is specific to the product ID, and the cached data is only related to a single product. Let’s look in detail at the Zend_Cache frontends and the configuration options available.

14.3.1. Zend_Cache Frontends

All of the frontends extend Zend_Cache_Core, but you don’t instantiate Zend_Cache_Core or any of the frontends. Instead, you use the Zend_Cache::factory() static method. The four arguments for this method are $frontendName (string), $backendName (string), $frontendOptions (associative array), and $backendOptions (associative array). The command is as follows:

  $cache = Zend_Cache::factory(
     $frontendName,
     $backendName,
     $frontendOptions,
     $backendOptions
  );

Each frontend and backend has its own options that affect its operation. The core frontend options are shown in table 14.1.

Table 14.1. Core frontend options of Zend_Cache

Option

Description

caching By default this is set to true, so you likely won't have to change it, but you can set it to false if you want to temporarily turn off caching for testing. This is an alternative to commenting out the cache test.
lifetime This is the amount of time in seconds before the cache expires and the result is regenerated. By default, it’s set to 1 hour (3600 seconds). You can set it to null if you want the cache to last forever.
logging If you set this to true, the caching will be logged using Zend_Log. By default it’s set to false. If set to true, there will be a performance hit.
write_control By default, this is set to true, and the cache will be read after it’s written to check that it hasn’t become corrupted. You can disable this, but it’s good to have some extra protection against corruption, so we recommend leaving it on.
automatic_serialization If set to true, this will serialize the cache data (refer to the PHP manual for the serialize function). This allows you to store complex data types such as arrays or objects. If you’re only storing a simple data type such as a string or integer, you do not need to serialize the data and can leave this as the default value of false.
automatic_cleaning_factor The automatic cleaning facility will clean up expired caches when a new cache result is stored. If you set it to 0, it won’t clean up expired caches. If set to 1, it will clean up on every cache write. If you set it to a number higher than 1, it will clean up randomly 1 in x times, where x is the number you enter. By default, it’s set to 10, so it will randomly clean up 1 in 10 times a new cache result is stored.

Each frontend is designed to give you caching capability at various levels of your application. The following subsections describe the individual frontends and their remaining options. Please note that all examples assume that $backendName, $frontendOptions, and $backendOptions have already been defined. This will allow you to interchange your own options with each frontend.

The frontends are listed in table 14.2

Table 14.2. The cache frontends

Name

Description

Core The core of all frontends but can also be used on its own. This uses the Zend_Cache_Core class.
Output This uses an output buffer to capture output from your code and store it in the cache. It uses the Zend_Cache_Frontend_Output class.
Function This stores the result of procedural functions. It uses the Zend_Cache_Frontend_Function class.
Class This stores the result of static class or object methods. It uses the Zend_Cache_Frontend_Class class.
File This stores the result of loading and parsing a file. It uses the Zend_Cache_Frontend_File class.
Page This stores the result of a page request. It uses the Zend_Cache_Frontend_Page class.

The most basic frontend you’ll use is Zend_Cache_Core.

Zend_Cache_Core

This is the base class for all frontends, but we can access it directly if needed. This is most useful for storing variables, such as strings, arrays, or objects. All of the frontends convert the cache result into a variable for storing in this way. The simplest use of Zend_Cache_Core is shown in listing 14.4.

Listing 14.4. Simple use of Zend_Cache_Core
$frontendName = 'Core';
$cache = Zend_Cache::factory(
   $frontendName, $backendName, $frontendOptions, $backendOptions
);
if (!($data = $cache->load('test'))) {
   // perform computationally intensive task here
   $cache->save($data, 'test');
}

One great use of Zend_Cache_Core is for storing the result of database calls, because there is no specific Zend_Cache_Frontend_* class to do so. An example of this is shown in listing 14.5.

Listing 14.5. Using Zend_Cache_Core to store database results
$cacheName = 'product_' . $productId;
if(!$result = $cache->load($cacheName)) {
   Zend_Loader::loadClass('Product');
   $productTable = new Product();
   $result = $productTable->fetchRow(array('id = ?' => $productId));
   $cache->save($result, $cacheName);
}

As you can see, we’ve added $cacheName. This allows you to set the unique identifier using variables and use it for loading and saving the cache. Be aware that the identifier used in this example is very simple because we are only fetching the row based on one condition. If you were performing a query with multiple conditions, you’d need to devise a means of generating a unique identifier to suit the purpose. If the identifier you choose is not unique enough, you might accidentally use the same identifier for two different queries, which could cause errors.

 

Setting a unique identifier

In many cases, you can generate a unique identifier using this code:

md5(serialize($conditions));

This converts the conditions, which may be an array, into a single unique string. You can join all of your conditions into one array for this. The serialize() function produces a single string that can be passed to md5(). The md5() function implements the MD5 algorithm, which produces a 32-character string representation of the serialized data.

The MD5 algorithm creates what’s called a one-way hash. The same input will result in the same hash value, and the chance of two different input values producing the same hash is extremely slim. This makes it a pretty good means of reducing a long string to a smaller, unique value.

The sha1() function is an alternative that uses the SHA1 algorithm. It’s similar to MD5 in that it produces a one-way hash, but the result is 40 characters, meaning the chances of two different values resulting in the same hash is even slimmer.

When setting your own unique identifier, it’s important to follow the rules for what characters it can contain. The identifier can’t start with “internal-”, because this is reserved by Zend_Cache. Identifiers can only contain a–z, A–Z, 0–9, and _. Any other characters will throw an exception and abort your script.

 

As you can see in listing 14.5, if there is a cache hit, the $result variable is filled with the cached result and can be treated as if it were directly returned by the database query. If there is a cache miss, the database query is performed and populates the $result value again before saving the result to the cache. Either way, the $result value can be treated exactly the same from here on in.

 

Note

When storing objects in a cache (such as database query results), you’ll need to ensure you have loaded the appropriate class (such as Zend_Db_Table_Row) before reading the object from the cache. Otherwise the object can’t be properly reconstructed, and your application will most probably fail because the properties and methods you expect won’t be there. If you are using autoloading, the class will be automatically loaded for you when it is needed. See section in Chapter 3 for more information on autoloading.

 

You might find it useful to place the caching code inside a method of a model, as shown in listing 14.6.

Listing 14.6. Using cache code inside a model
class Product extends Zend_Db_Table_Abstract {

   protected $_name = 'product';
   protected $_primary = 'id';

   public function fetchRowById($id) {
      Zend_Loader::loadClass('Zend_Cache');
      $frontendOptions = array (
         //...
      );
      $backendOptions = array (
         //...
      );
      $queryCache = Zend_Cache::factory(
         'Core', 'File', $frontendOptions, $backendOptions
      );
      $cacheName = 'product_id_' . $id;
      if(!($result = $queryCache->load($cacheName))) {
         $result = $this->fetchRow(array('id = ?' => $id));
         $queryCache->save($result, $cacheName);
      }
      return $result;
    }
}

By moving the cache code to the fetchRowById() method, we now have a central place to manage our cache options. For example, we could change the lifetime in one place rather than many, fix a bug with the unique identifier, disable caching for debugging, or clear the cache.

If you need to get a row from the Product table with a specific ID, you can now use this code:

  $productTable->fetchRowById($id);

You can place this code at multiple points and not have to worry about caching every time, because it’s handled for you.

Although you could use Zend_Cache_Core to cache output from your code, Zend Framework comes with a class called Zend_Cache_Frontend_Output designed just for that.

Zend_Cache_Frontend_Output

This frontend uses output buffering to cache output from your code. It captures all output, such as echo and print statements, between the start and end methods. It uses a simple identifier to determine whether a cache result exists. If not, it will execute the code and store the output.

There are no additional frontend options for Zend_Cache_Frontend_Output, as you can see in listing 14.7.

Listing 14.7. Usage of Zend_Cache_Frontend_Output
if (!($cache->start('test'))) {
   echo 'Cached output';
   $cache->end();
}

As you can see, if the start() method returns false (no valid cache was found), it will cache the output. If start() returned true, Zend_Cache_Frontend_Output will output whatever was stored in the cache. You’ll have to be careful that you don’t use the same unique identifier for different output in separate areas of your code.

If you want to store the result of a function, you can use the Zend_Cache_Frontend_Function class.

Zend_Cache_Frontend_Function

This frontend stores the result of a function call. It’s able to distinguish one function call from another by comparing the input values. If the input values match an existing cache result, it’s able to return that rather than performing the function operation again.

The frontend options for Zend_Cache_Frontend_Function allow you to control the caching of individual functions from one point. The main benefit of this is that if you decide you don’t want to cache a particular function, you don’t have to change every call to that function; you simply change the option.

This may seem a little bit confusing, but we’ll explain it in more detail shortly. Table 14.3 shows the additional frontend options for Zend_Cache_Frontend_Function.

Table 14.3. Additional frontend options for Zend_Cache_Frontend_Function

Option

Description

cacheByDefault By default this is true, meaning all functions passed through Zend_Cache_Frontend_Function will be cached and you’ll need to set nonCachedFunctions to turn off individual functions. If set to false you’ll need to set cachedFunctions to enable caching for particular functions.
cachedFunctions If cacheByDefault is turned off, you can define the functions you’d like to cache here, as an array.
nonCachedFunctions If cacheByDefault is turned on (the default setting) you can disable caching of functions by adding them here as an array.

Listing 14.8 shows an example of a normal call to a computationally intensive function without using caching.

Listing 14.8. Example function call without caching
function intensiveFunction($name, $animal, $times)
{
   $result = '';
   for ($i = 0; $i < $times; $i++) {
      $result .= $name;
      $result = str_rot13($result);
      $result .= $animal;
      $result = md5($result);
   }
   return $result;
}
$result = yourFunction('bob', 'cat', 3000);

To cache this function, you’d use Zend_Cache_Frontend_Function’s call() method. Listing 14.9 shows Zend_Cache_Frontend_Function applied to listing 14.8.

Listing 14.9. Example function call with caching
function intensiveFunction($name, $animal, $times)
{
   $result = '';
   for ($i = 0; $i < $times; $i++) {
      $result .= $name;
      $result = str_rot13($result);
      $result .= $animal;
      $result = md5($result);
   }
   return $result;
}

Zend_Loader::loadClass('Zend_Cache');
$frontendOptions = array (
   ...
);
$backendOptions = array (
   ...
);
$queryCache = Zend_Cache::factory(

    'Function', 'File', $frontendOptions, $backendOptions
);
$result = $cache->call('intensiveFunction', array('bob', 'cat', 3000));

This is exactly the same as calling call_user_func_array() except that the result will be cached. If you decided you no longer wanted to cache this function, you could set it in the options rather than having to change all of your code back to the original intensiveFunction() call:

  $nonCachedFunctions = array('intensiveFunction');

If you want to enable caching again, just remove it from the array.

If you change the input for your function, Zend_Cache_Frontend_Function will treat this as a unique identifier:

  $result = $cache->call('yourFunction', array('alice,' 'dog', 7));

Zend_Cache_Frontend_Function will cache this separately from the previous function call. This means you don’t have to create your own unique identifier.

Please note that you can’t use Zend_Cache_Frontend_Function for calling static methods of classes. Instead, you’ll need to use Zend_Cache_Frontend_Class.

Zend_Cache_Frontend_Class

This frontend is similar to Zend_Cache_Frontend_Function, but it’s able to store the result of the static methods of a class, or the result of non-static methods of an object.

The additional frontend options for Zend_Cache_Frontend_Class are shown in table 14.4.

Table 14.4. The additional frontend options for Zend_Cache_Frontend_Class

Option

Description

cachedEntity Set this to either the class (for static methods) or the object (for non-static methods). This is required for each cache call.
cacheByDefault By default this is true, meaning all methods passed through Zend_Cache_Class_Function will be cached and you’ll need to set nonCachedMethods to turn off individual functions. If set to false, you’ll need to set cachedMethods to enable caching for particular functions.
cachedMethods If cacheByDefault is turned off, you can define the methods you’d like to cache here, as an array.
nonCachedMethods If cacheByDefault is turned on (the default) you can disable caching of methods by adding them here as an array.

When calling the method through the cache, treat the cache object as if it were the original class or method you were calling. For example,

  $result = $someObject->someMethod(73);

becomes

  $result = $cache->someMethod(73);

Or for a static method,

  $result = someClass::someStaticMethod('bob');

becomes

  $result = $cache->someStaticMethod('bob');

The Zend_Cache_Frontend_Class frontend uses the class or object and the method input as a unique identifier, so you don’t have to create one.

The next useful frontend is Zend_Cache_Frontend_File.

Zend_Cache_Frontend_File

This frontend caches the result of parsing a particular file. Essentially, it’s able to determine whether the file has changed and uses that to determine whether or not it needs to be reparsed. Parsing can be anything really; what you’re caching is the code that is dependent upon the contents of a particular file. This is essentially the same as Zend_Cache_Core except that the cache expires if the file changes rather than after a fixed time period.

There is one additional frontend option for Zend_Cache_Frontend_File, which is shown in table 14.5.

Table 14.5. The additional frontend option for Zend_Cache_Frontend_File

Option

Description

master_file This is required and must contain the complete path and filename of the file you’re caching.

Once you have defined the master file, you can use Zend_Cache_Frontend_File as shown in listing 14.10.

Listing 14.10. Example usage of Zend_Cache_Frontend_File
$filename = 'somefile.txt';
$cacheName = md5($filename);
if (!($result = $cache->load($cacheName))) {
   $data = file_get_contents($filename);
   $result = unserialize($data);
   $cache->save($result, $cacheName);
}

The $filename value here is fed into the frontend options. We then use an MD5 digest of $filename as the unique identifier. You may want to cache different operations on a file; for example, you may load in an XML file and search the contents in one area, while directly outputting the contents in another area. In order to cache both operations, you’ll need to use a different unique identifier for each area.

If the file somefile.txt ever changes, Zend_Cache_Frontend_File will notice (based on the modification time) and the code will be run again. Otherwise the result of the code (not the data from the file itself) will be returned.

Zend_Cache_Frontend_Page

The Zend_Cache_Frontend_Page frontend is like Zend_Cache_Frontend_Output, except that it caches the output based on the $_SERVER['REQUEST_URI'] variable and optionally the user-submitted data contained with the $_GET, $_POST, $_SESSION, $_COOKIE, and $_FILES variables. You initiate it by calling the start() method, and it will self-store when the page has been rendered. You can save all page-code execution if the input variables match an existing cache result.

The additional frontend options for Zend_Cache_Frontend_Page are shown in table 14.6.

Table 14.6. The additional frontend options for Zend_Cache_Frontend_Page

Option

Description

debug_header This is set to false by default, but if you set it to true, it will output “DEBUG HEADER : This is a cached page!” Unfortunately, you can’t change this message, but it will at least let you check to be sure the output is the cached page rather than the original.
default_options This can get really complex, but thankfully in most situations you can leave the default settings alone. If you really must dig deeper, you can set the associative array with the following options.
  cache By default, this is true and the page will be cached if all other conditions are met. If you set it to false, the page won’t be cached.
  cache_with_get_variables By default, this is false, which means that if there are any variables in $_GET, the page will be rerendered. If you set it to true, the page will still be loaded from cache. Be careful setting this to true if the $_GET variables change the contents of the page.
  cache_with_post_variables Same as cache_with_get_variables, but with $_POST. cache_with_session_variables Same as cache_with_get_variables, but with $_SESSION. cache_with_files_variables Same as cache_with_get_variables, but with $_FILES. cache_with_cookie_variables Same as cache_with_get_variables, but with $_COOKIE.
  make_id_with_get_variables This is set to true by default, which includes $_GET in the automatic unique identifier generator. If you set it to false, the generator will use the other variables. Be careful with this one; if the $_GET variables change the output of the page, you may have cache conflicts if you don’t set it to true.
  make_id_with_post_variables Same as make_id_with_get_variables, but with $_POST. make_id_with_session_variables Same as make_id_with_get_variables, but with $_SESSION. make_id_with_files_variables Same as make_id_with_get_variables, but with $_FILES. make_id_with_cookie_variables Same as make_id_with_get_variables, but with $_COOKIE.
regexps This is a very powerful feature. Since you’ll most likely only have one instance of the frontend handling all of your pages, you may want to treat some pages differently than others. This option is an associative array, the key is the regular expression you use to define the page to apply the options to, and the value is an array just like default_options above. The regular expression is run on $_SERVER['REQUEST_URI'], so you can use whatever you like, but in most cases it will be a simple expression to match controllers or controller and action combinations.

The most basic implementation of Zend_Cache_Frontend_Page looks like this:

  $cache = Zend_Cache::factory('Page', 'File', $frontendOptions,
    $backendOptions);
  $cache->start();

Note that once $cache->start() is called and a valid cache exists, your application will end once the cached page has been output.

If we wanted to turn off caching for a particular controller, we’d add the following in the $frontendOptions:

  'regexps' => array(
     '^/admin/' => array(
      'cache' => false,
     ),
  ),

This turns off page caching for the entire admin section of the site. If you don’t know much about regular expressions, you should be able to get by with putting ^ at the beginning of each entry. If you need to know more, research regular expressions or “regex.”

If we wanted to turn off caching for admin, but keep the products action cached, we’d do this:

  'regexps' => array(
     '^/admin/' => array(
        'cache' => false,
     ),
     '^/admin/products/' => array(
      'cache' => true;
     ),
  ),

The last rule will always be followed if in conflict with previous rules, so this will work as expected. If the ^/admin/products/ line was above the ^/admin/ line, the products action wouldn’t be cached because the ^/admin/ line would overrule it.

Now you know about all of the frontends, so let’s look at the Zend_Cache backend classes.

14.3.2. Zend_Cache Backends

The backends define the way in which the cache data is stored. In most cases, Zend_Cache_Backend_File is the best and easiest backend to use. It uses simple files to store the data. Simple file operations are generally much faster than accessing a database, or in some cases performing resource-intensive tasks. This makes files perfect for cache storage.

The additional options for Zend_Cache_Backend_File are shown in table 14.7.

Table 14.7. The additional options for Zend_Cache_Backend_File

Option

Description

cache_dir This is the full path where the cache files are stored. By default, it’s set to /tmp/ but we prefer to set it to a path within our application so that we can easily view and manage cache files manually if we need to.
file_locking This will use an exclusive lock to offer some protection against cache corruption. It’s turned on by default, and there is little reason to turn it off, although there may be a very minor performance improvement in situations where file locking isn’t supported.
read_control By default this is turned on, and it adds a control key (digest or length) that is used to compare the data that is read from cache to ensure it matches the data that was stored.
read_control_type This sets the read control type. By default, it uses crc32(), but it can be set to use md5() or strlen().
hashed_directory_level Some file systems struggle when there is a large number of files in one directory. This can be a hassle when you’re listing files or accessing via FTP and so on. Also, it can bog down statistics and similar applications. In order to protect against this, you can set the hashed_directory_level, which causes the backend to create multiple directories and subdirectories for the cache files to be stored in, so that there are fewer files in each directory. By default, it’s set to 0, which means all files are in the one directory, but you can set it to 1 or 2 (or more) levels of subdirectories, depending on how many files you expect. 1 or 2 are probably the safest options.
hashed_directory_umask This uses chmod() to set the permissions for the directories it creates. By default, it’s set to 0700.
file_name_prefix This sets a prefix for all cache files that are created. By default, it’s set to zend_cache, which is good if you’re using a generic /tmp/ directory for storage, but we prefer to use a prefix that describes the items that will be cached with this backend, such as “query” or “function”. This allows us to know which files are related to which cache activity.

There are a few other backends, listed in table 14.8, but explaining them is beyond the scope of this book because they’re much more specialized and only change the system that stores the cache data. If you’re experienced with some of these other systems, you should be able to easily adapt your knowledge of Zend_Cache_Backend_ File to suit. Please note that you may lose some functionality in choosing alternative backends.

Table 14.8. The additional Zend_Cache_Backend classes

Option

Description

Zend_Cache_Backend_Sqlite This uses an SQLite database for storage.
Zend_Cache_Backend_Memcached This uses a Memcached server for storage.
   
Zend_Cache_Backend_Apc This uses Alternative PHP Cache.
Zend_Cache_Backend_ZendPlatform This uses the Zend Platform.

Once you have your head around how to add caching to your code, you have to decide where you’ll add caching to get the optimal results.

14.4. Caching at Different Application Levels

While caching is an amazingly powerful tool, it can be used incorrectly. The most important decisions you’ll have to make are deciding what to cache and how long the cache should last for.

14.4.1. Choosing What to Cache

You have to be very careful when caching to be sure you’re not caching something that must be run each time. For example, if you have rand(), time(), or database inserts or updates in the cache code, these won’t be executed if there is valid cache data. This can be very unfortunate if you rely on these occurring further down in your code.

When you’re using Zend_Cache_Frontend_Page, for example, and you’re storing request details in the database for statistical purposes, you’ll need to perform the database operations before calling $cache->start(). Otherwise, only one entry will be made for each expiry period, invalidating your statistics.

But there are some times when you’ll want to cache things like rand(). For example, if you’re displaying three random products on your home page as product highlights, you can probably cache it for five minutes or so without it affecting the user experience. In fact, you might want to cache it for 24 hours so that each day there are three new “products of the day.”

14.4.2. Optimal Cache Expiry

One of the trickiest parts of caching is choosing the expiry time for the items you’re caching. You can set up multiple cache objects anywhere in your code for various uses and assign different expiry times to each. It’s a good idea to put these into a common place if possible, so that the caching is applied automatically for you, as described previously, with $productTable->fetchRowById(). This will ensure that all of your cache options, such as the expiry time, are consistent, and it allows you to easily change the options if you need.

When you’re choosing an expiry time, it comes down to the type of data you’re caching and how often it’s likely to change, as well as how much traffic you expect. If the data changes often, such as users’ comments, you may have to set the cache to five minutes. At most, the data will only be five minutes old. If you have a high-traffic web site, this will still offer a considerable performance improvement because you may receive a few hundred requests in five minutes.

If you have data that does not change frequently, such as product details, you may want to set the cache to seven days. You will, however, have to clear the cache using Zend_Cache_Core’s clean() or remove() methods when the price changes.

In some situations, where your load-intensive task takes a long time, there may be a second request for the information before the first request has completed and stored the result in cache. In this case, the load-intensive task will run again for the second request and for any additional requests until a result is stored in the cache. In high-traffic situations, this can bring your site to a grinding halt each time the cache expires. To avoid this, you should run the load-intensive task and replace the data in the cache when the data changes, and set the cache’s lifetime to null so that it is always valid. This will ensure that the data is always read from the cache when it is needed, and the load-intensive task runs only when you want it to.

There is some data you can cache for a long period of time, such as the output of an intensive mathematical function, where the same input will always produce the same output. In this case, you could set the expiry to 365 days, or longer.

If you make a change to the expiry of a cache at any time, it will take effect immediately.

14.5. Cache Tags

When you save data to cache, you can attach an array of tags. These tags can then be used to clear out caches containing a specific tag. The code for this is as follows:

  $cache->save($result, 'product_56', array('jim', 'dog', 'tea'));

If you ever need to, you can clear the cache programmatically; for instance, if you cache a product for seven days and wish to force a cache refresh to reflect an immediate price change. Cache cleaning can be very specific, down to a unique identifier:

  $cache->remove('product_73');

Or it can be very broad:

  $cache->clean(Zend_Cache::CLEANING_MODE_OLD);
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);

These two commands clean out old caches, or all caches (every cache will then need to be recreated).

You can also clean out caches that have specific tags attached:

  $cache->clean(
     Zend_Cache::CLEANING_MODE_MATCHING_TAG,
     array('dog', 'salami')
  );

Remember to use the same configuration options for the cache object when cleaning that you did when you created the cache data.

14.6. Summary

In this chapter, we covered how to implement caching in your application using frontends and backends. We covered the details of each frontend so that you can make the best decision about which frontend is most appropriate for each part of your application. We covered one backend option, but you might like to explore some of the other backends to see if they are more appropriate for your needs.

Caching can be a powerful way to improve the performance of your application, and Zend_Cache is an excellent tool for the job, but it can take some thought to ensure that information is cached properly. As your application evolves and your traffic patterns change, you may find that you need to add to or adjust your cache settings to resolve new performance issues.

If you are lucky enough to deal with an extremely high-traffic application, you will be able to combine your use of Zend_Cache with other performance-improving technology, such as static content servers and load-balancing server clusters.

Now that we know how to ensure our application performs well as traffic grows, it’s time to turn our attention to the rest of the world, which represents a very large market. There are many languages other than English, so we’ll look at how Zend Framework’s internationalization and localization features can be used to broaden the appeal of your application worldwide.