Using Globalsphp with Zend Cache

Outside of security, the most important aspect of any web based application is performance. It's one thing to code for a small intranet application that will only be used by 10-50 people. That's not going to be taxing on any server you run it on. However, when your shiny new web 2.0 application goes online and thousands of people realize what a genius you really are, performance is going to be a real issue in a hurry. All of a sudden you are going to be looking for ways to do more at once, do things faster and off-load processing. One common strategy is caching. If you've been working in programming more than 10 minutes, in most all programming cases, it means storing the results of some processor intensive processes for reuse. The easiest one that comes to mind is the results of a SQL query. If your home page has a SQL query that takes two seconds to execute, having 2 users and causing that to execute once per minute is not a big deal. However, having 1,000 users and causing that query to execute 500 times per minute is a big deal. Especially if in that minute, the data is not going to change. If the data isn't likely to change then you've needlessly hit your database 499 times. When your numbers start getting up to 10,000 users and higher, all of a sudden, this is a serious problem.

Lucky for us, this is an easy problem to solve using Zend_Cache. Zend_Cache is a simple frontend for caching. By simple, I mean simple to implement and easy to use. The magic that happens behind the scenes is anything but simple. To keep this at a level that everybody can use, we will only be using some of the simple options.

One of the biggest benefits of Zend_Cache, outside of enhancing the performance of your application is that it can use multiple backend storage devices. To keep things simple, we are going to be using file based storage but it comes with backend drivers for;

• Zend Platform

Like everything else in Zend Framework, you can always extend it if you need a storage device that is not listed. Since the API is common among all devices, you can easily develop using "File" and then switch to APC or Zend Platform for production.

Before we dive into the code, let's take a look at the different options we have to play with.

Zend_Cache is divided into frontends and backends. The methods and properties available on each frontend differ based on the actual frontend you are using. Which frontend you use will depend upon what you are actually trying to cache. Zend Framework ships with the following frontends:

• Zend_Cache_Core. Core is the most basic caching. This is the one we will be using. You can store any serialized string. This is by far the most common use of Zend_Cache.

• Zend_Cache_Frontend_Output. Output uses PHP's output buffering to capture the output to be cached. Because of this, Output is the easiest cache frontend to use. With a few lines of code, you can easily begin caching output in existing sites. Output can be used to cache specific portions of a page.

• Zend_Cache_Frontend_Page. Similar to Output, Page is used to cache the output of a page. Unlike Output though, it will cache the entire page instead of selective portions of the page. You don't really want to put too much code above the check for the cache. You really only want the code necessary to instantiate the cache. If the page is in the cache then everything will be returned form there. If not, then you will continue your normal page coding.

• Zend_Cache_Frontend_Function. Function is used to cache the output of afunc-tion. When setup properly, you make the function call to your cache controller instead of your user defined function call. If the output is in the cache and still fresh, the output is returned from the cache, otherwise, all of the parameters are passed to the original function and the results are then cached. It takes a bit more to implement this but for functions that are time intensive, this is the best way to cache their results.

• Zend_Cache_Frontend_Class. Class, as it's name implies, caches an entire class at a time. It differs from Function in that it allows calls to be made to static methods as well as normal ones. Other than that, it acts similar to Function in that the cache controller stands in proxy of the actual class. If the results can be returned from cache then they are, if not, the class method is invoked and the results are cached.

• Zend_Cache_Frontend_File. File uses the modification time on the master file to determine whether the cache is fresh or not. The example that the manual gives is a good one. If you have a configuration object that is populated by parsing an XML file, you could use File to store the resulting config object. This would save you re-parsing the XML each time until the config file itself was changed.

While we have fewer default choices on the backend, they provide you with the basics that most any web application will need to cache data.

• Zend_Cache_Backend_File. This is the most common and the one we will be using in our examples. You provide Zend_Cache with a directory to use to store cache files. Each time you cache something, it is stored in a file in that directory. This is the easiest to understand and use; however is it not the best choice on high traffic sites as your file system may become the bottleneck.

• Zend_Cache_Backend_Sqlite. Sqlite is an interesting choice for a backend, I'll have to admit. In a lot of cases, what is being cached is database output. Storing the cache in another database, even a lightweight one, doesn't save you that much. However, it is available to you and should you want to use the concepts employed in it, you could easily write a MySQL backend for storing long-term pages, etc.

• Zend_Cache_Backend_Memcached. Now we start moving into the really high performance backends. The Memcached backend utilizes your existing memcached server to store caches. Since memcached stores everything in memory, this will give you much better performance than File at the cost of complexity.

• Zend_Cache_Backend_Apc. Alternative PHP Cache as it's name implies, utilizes the native caching mechanism in APC to store your cache. This is an excellent high-performance choice if you already have APC installed.

• Zend_Cache_Backend_ZendPlatform. Since Zend Platform is built by Zend, you would expect tight integration with it. I'm glad that Zend put support for other high performance options like memcached and APC into the Zend_Cache as well. For production servers, this (of course) is my favorite. It's high-performance, easy to manage and tightly integrated. The only downside is the same one that all of the high-performance ones suffer from, more moving parts.

For our little demo application, the Core frontend and the File backend make a great pair. You will get a feel for the concepts involved but not get bogged down in the details of the more complicated pieces.

Before we begin though, we need to look at three key concepts that are important.

• Key Identifier. It does you no good to store something in the cache that you can't retrieve. To retrieve it you have to give it a unique identifier. This is used by the backend to identify the cache and make it retrievable. Your key identifier can be anything you want but it has to be unique and it has to be a string.

• Lifetime. Each item stored in the cache has a lifetime. The lifetime of a cache, always expressed in seconds, determines how long the cache is considered fresh. Cache's are not automatically discarded after their lifetime. However, the next time a request comes in for an expired cache, Zend Framework will refresh the cache and start the clock over again.

• Conditional Execution. The Zend_Cache_Core::get() method returns a false on a cache miss (no cache or expired cache). This allows you to wrap your code in an if() statement. Your code only executes if there was a cache miss. As you will see in our examples, this is an important concept for us as we are caching SQL results.

Ok, enough talk, let's start implementing. The first thing we need to do is add code to Globals.php to return our cache controller. Since we don't really know when and where we will use it yet, it's best just to handle it in Globals.php. (Besides, it wouldn't make much sense to put it in this chapter if it weren't going in Globals.php). So here's what we need to add to Globals.php.

Fist, let's include the Zend_Cache class.

Zend_Loader::loadClass('Zend_Cache'); Next, we need a container for the cache controller. private static $_cache = null;

Place that just below the private static $_db line.

Now, we need the static function. Place this belowthe getDBConnection() function.

static public function getCache() {

self::$_cache = Zend_Cache::factory( 'Core', 'File', array('lifetime'=>600, 'automatic_serialization'=>true), array('cache_dir'=>"c:\web\htdocs\cache"));

return self::$_cache;

As you can see, we are using the Core frontend and the File backend. The first array we are passing in is the array of frontend options. We passed in a lifetime of 600 seconds (10 minutes for those of you not yet awake) and the automatic_serialization parameter. This way the Cache object will handle the serialization for use relieving us of one more task. The second array is the array of backend options. The File backend takes several options but the only one we want to change is the cache_dir.

A lot of developers have something generic like tmp that they stuff everything into. That's a good idea of you get charged by the directory. However, since most hosts don't charge by the directory, it's a good idea to segregate your different working files by directory. In most applications I build I have a directory for sessions, one for temporary work space, one for the cache and if I'm working with a templating engine like Smarty, one for the template cache. Do yourself a favor and segregate your files. It comes in handy when you want to blow away just one type of files. (i.e. in our case, the cache).

Now, we've got the plumbing installed, let's hook it up. For our sample application we are only going to cache one thing, the list of searches we've already done. So, open your HistoryController and let's dive in.

public function listSearchesAction() {

if (!$results=$cache->load('searchList')) { $db = Globals::getDBConnection();

$sql = 'select wp.id as wp_id, wpa.id as wpa_id, wpa.search_date, wp.url from web_property_analysis wpa, web_property wp where wpa.web_property_id = wp.id order by url, search_date;'; $results = $db->fetchAll($sql); $cache->save($results,'searchList'); $this->view->history = $results; } else {

$this->view->history = $results; } // if($results = $db->fetchAll($sql)) } // public function listAction()

Now as you can see, most of that is exactly as we've already written. However, there are a few subtle changes. The first line for example; in order for us to do anything with the cache, we need an instance of the cache controller.

Next, we attempt to load the last results from the cache:

The parameter for $cache->load() is the name of the cache or the cache key. In this particular case, the cache key is pretty simple, it's a description of the select statement we are caching. However, you will quickly realize that the cache key can be tricky. The cache key has to be unique throughout the application. Once you get into larger applications, this is going to be more difficult. In many cases, when I'm caching the results of a SQL statement, the easiest thing to do is to take an md5 hash of the SQL statement. This makes for an ugly but unique and easy way to reproduce a cache key.

The if statement above will fail for two reasons. First, if the SQL results have not yet been cached, of course it will fail. The second reason it will fail is if the cache is no longer fresh. (I must resist the urge to make a "expired cache's smell bad" joke here). Since we set our lifetime to 600 seconds, if 601 seconds have passed since the cache was built then load() will fail.

In both cases, when load fails, !$results=$cache->load('searchList') will return true (it's a double negative thing. If you still don't get it, read it again you'll have that "aha!" moment). Since the result of that is true if load() fails, we will execute the code inside of the if(). This will execute our SQL and save the cache results using the save() method. $cache->save() requires two parameters:

The contents we are wanting to cache. This must be serializable.

• The cache key to use. Remember this has to be easy to recreate as we need to use it on load.

Save takes an optional third parameter of an array of tags, we'll get to that in a minute as it's an important concept to understand.

If perchance the cache has a hit instead of a miss, the results are unserialized into $result. From there we process it normally. Either way, at the end of the if() statement, we have a valid results array stored in $results.

Now, if you have been paying attention, you will recognize that the whole if() statement thing is an example of the third key concept of caching that we discussed a few pages ago, conditional execution. Ok, I know some of you were hoping that it was more difficult than that but it's really not. It means what it says, on cache miss, execute the code to build the cache. On cache hit, skip all that stuff.

Ok, I promised I'd talk about tagging. Tagging is an important concept in caching with Zend_Cache. The idea is similar to tagging found elsewhere on the web. You give a cache one or more semantic tags that are meaningful to you. Just like you would not give each post in a blog a unique tag, you don't want to give each cache item a unique tag. Tagging is a way of grouping your cache together. Really, there is only one reason you want to do this - deleting! If you tag all of your caches properly and group them logically, you can invalidate specific areas of your cache with a single command without resorting to clearing it all.

Let's take a simple example, not related to our example application but easily understood; a blog that you are caching with Zend_Cache. At its most primitive, a blog has posts and comments. Since comments are related to posts, it would make sense to cache them with the post. If our caching is working properly, when a comment is added, it won't show up until the cache has expired. In many cases you will want a long lifetime on blog posts and comments, but need to clear it often. You may need to clear it just after the post is made but less frequently as time goes on and the post gets buried by other posts.

There are two ways we can deal with this. First, we can, upon the post of a comment, clear the entire blog cache and rebuild them all from scratch. On a popular blog, this is as bad as having no cache at all since it's constantly being rebuilt.

The other option is to employ a cache tagging scheme that allows us to identify the portions of the cache that the comment will affect and invalidate just those parts. This is where tagging comes in. In our little blog example, we could use a tag of post_id_12 for the cache of the post and the cache of its comments. The save command would look like this:

$cache->save($post,$cacheKeyPost,array('post_id_12'));

$cache->save($comments,$cacheKeyComments,array('post_id_12'));

Now both the post and the comments are tagged with post_id_12. Should either of them change, we can issue a single command and clear the relevant portion of the cache.

$cache->remove('post_id_12'); If we want to remove multiple tags at once, we can use the clean() method:

$cache->clean(

Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('post_id_12', 'post_id_13', 'post_id_14'));

There are also two other modes that clean can be used in. The first is to remove everything in the cache. For those times when your user clicks the "Clear the Cache" button (you don't really have one of those, do you?) or you feel that everything needs to be rebuilt, call clean with the Zend_Cache::CLEANING_MODE_ALL parameter and the entire cache will be deleted.

// clean all records $cache->clean(Zend_Cache::CLEANING_MODE_ALL);

Additionally, you can tell Zend_Cache to physically remove all the files in your cache that have expired or been invalidated.

// clean only outdated $cache->clean(Zend_Cache::CLEANING_MODE_OLD);

A good tagging scheme can make your cache a joy to work with. A bad one will cause you more pain than it's worth. When you begin to work with caching, stop, take some time and think through your tagging scheme. Trust me on this one, it's time well spent.

The one final thing we need to do with our caching system is to invalidate our cache if we add a new search. We will use the clean() method described above and the tag that we specified, even though we only have a single cache at the moment. You must resist the urge to clear the entire cache unless you've got a real good reason to. So, the HOW in this case is easy:

Globals::getCache()->remove('searchList');

That's to the magic of fluent interfaces, we can do it all in one line. The real question is WHERE. WebProperty->save() is the obvious answer. Since any change to any web property would mean that our cache is out of date, we need to invalidate it at that point. So we open WebProperty.php and place this as the last line of the save() method. Now let's go test it. The easiest way to test for a cache hit or miss is to put a simple echo statement in the IF statement in the HistoryController.php. This however, is so very unprofessional. We could also deploy the Zend _Log but that's really overkill for what we need. No, the best thing to do in this particular case is just to watch the cache directory. If you've already tested the caching then the cache file will already exist. If it's been more than 10 minutes since you tested it then you will have an invalid cache file in your cache directory. So, using the magical incantation for whatever OS you are using, navigate to the cache directory.

Now fire up a browser go to the main page and analyze a URL. If you had a cache, this action should have removed it. That's the easiest way to invalidate a cache; totally get rid of it. Now you know that the next time you execute a search, it rebuilds the cache. Since we've mentioned it, let's do that. Log in if you haven't already and then click on the "List Searches" link. If everything is working as perfectly as it should, you should now see a new cache.

When it comes down to it, that's really all there is to caching, each call to the cache is either a hit or a miss and you need to act accordingly; and when the data changes, invalidate your cache.

Was this article helpful?

0 0

Post a comment