Caching Dynamic Twitter Signature Images with PHP

Welcome to fifth and final part of this series on creating a dynamic Twitter signature image in PHP. In the last segment, I showed you how to implement custom PHP exceptions as an error-handling mechanism in your signature image application. Today we’re going to wrap up our signature image application by adding a caching feature. This is a two-fold solution that both boosts performance and overcomes a pitfall in the Twitter API.

As a result of heavy usage, Twitter has begun limiting the number of API requests that it will serve to a single IP address within a given amount of time.  This limitation is generally around 50 requests per hour.  While this is usually not a problem, if you begin using your signature image on high-traffic sites such as some forums, you may find that Twitter will start cutting off your requests.  At first that may seem confusing, but keep in mind that regardless of where the image actually appears, the request always originates from the IP address of the web server where the script is located.

To circumvent this pitfall, we can add a caching feature to our Twitter signature image application.  Accomplishing this is a fairly straightforward process.  The Twitter user feed will be saved in a cache and served locally, and the cache will be updated at intervals, drastically reducing the number of direct requests to the Twitter API.  Since the Twitter feed is a small XML-formatted text file, the cache does not require much space and the local server load is nearly negligible.

Setting up caching

To begin caching we’ll need to add another folder in the directory where our Twitter script resides.  This folder will be used for storing the cached user feeds.  You can name it anything you like, but for simplicity I just named it "cache."

class SignatureImage

{

    private $screen_name;

    private $profile_image;

    private $status_text;

    private $local_avatar;

    private $cache_expires = 900;

The next step involves making updates to the SignatureImage class.  The first of these changes will be to add a new properly called $cache_expires.  This variable holds the value that tells the script how often the cache should be updated.  This value, in seconds, determines how often the script will allow a new request to be made to the Twitter API. 

A value of 900 seconds (or 15 minutes) is a good place to start.  Most Twitter feeds are not going to be updated more frequently than 15 minutes, so there’s no need to fetch the feed more often than that.  You can increase or decrease this number as necessary based on the performance of your own script and the number of different users actively using it.  More users may require a longer wait time while less users may require less.  Using a value of 900 seconds, the script can support up to 12 users making 4 requests per hour.

    public function __construct($name, $bg_image, $adir, $cdir)

    {

        try {

            if (!$name) {

                throw new SignatureImageException(‘You must provide a user name.’);

            }

            $this->fetchUserInfo(strtolower($name), $cdir);

            $this->fetchAvatar($this->profile_image, $adir);

            $this->renderImage($bg_image);

        }

        catch (SignatureImageException $e) {

            $e->getImage();

        }

        catch (Exception $e) {

            die($e->getMessage());

        }

    }

Next, you’ll need to update the SignatureImage constructor to allow an additional parameter for specifying the location of the cache directory.  I’ve named this parameter $cdir.  I’ve also passed this value to the fetchUserInfo method call.  We’ll be adding this change later. 

Finally, I’ve added the use of PHP’s strtolower() function to ensure that the user name is always passed to the fetchUserInfo method in lowercase form.  This will prevent caching duplicate user feeds, as you’ll see later.

new SignatureImage($name, ‘banners/my_banner.jpg’, ‘avatars’, ‘cache’);

Once you’ve made all of these changes to the SignatureImage class constructor, you’ll need to update your source call to the SignatureImage class to include the added parameter.

{mospagebreak title=Implementing caching}

Now you need to make changes to the functional portions of the SignatureImage class.  In order to implement caching we’ll need to rewrite part of the fetchUserInfo method used for retrieving the user’s Twitter feed information.  Let’s look at the method in its current form.

    private function fetchUserInfo($name)

    {

        $url        = "http://twitter.com/statuses/user_timeline/{$name}.xml?count=1";

        $xml = $this->curlRequest($url);

        if ($xml === false) {

            throw new SignatureImageException(‘User feed unavailable.’);

        }

        $statuses = new SimpleXMLElement($xml);

        if (!$statuses || !$statuses->status) {

            throw new SignatureImageException(‘Invalid user channel.’);

        }

       foreach ($statuses->status as $status) {

            $this->status_text   = (string) $status->text;

            $this->profile_image = (string) $status->user->profile_image_url;

            $this->screen_name   = (string) $status->user->screen_name;

            break;

        }

    }

To begin, we’ll need to add another parameter to this function as well for passing in the location of the local cache folder.

    private function fetchUserInfo($name, $cache)

    {

        $url        = "http://twitter.com/statuses/user_timeline/{$name}.xml?count=1";

Next, we need to add some logic.  Currently, the function will begin by retrieving the user feed data from the Twitter API.  Since we want this to be performed through our caching mechanism, we’ll need to add a few If blocks to determine when and if a Twitter API call should be implemented.

        if ($xml === false) {

            throw new SignatureImageException(‘User feed unavailable.’);

        }

We’ll be replacing the above section of code with the code below.

        $cache      = preg_match(‘#^(.*?)/$#i’, $cache) ? $cache : "{$cache}/";

       $cache_file = $cache . md5($url) . ‘.xml';

        if (@filemtime($cache_file) + $this->cache_expires > time()) {

            $xml = @file_get_contents($cache_file);

        } else {

            $xml = $this->curlRequest($url);

            if ($xml !== false) {

                file_put_contents($cache_file, $xml);

            }

        }

        if ($xml === false && file_exists($cache_file)) {

            $xml = file_get_contents($cache_file);

        } elseif ($xml === false) {

            throw new SignatureImageException(‘User feed unavailable.’);

        }

Here is the entire caching logic in its entirety.  Let’s take a look at this one piece at a time and see how it works.

        $cache      = preg_match(‘#^(.*?)/$#i’, $cache) ? $cache : "{$cache}/";

       $cache_file = $cache . md5($name) . ‘.xml';

This first piece of code establishes some ground work.  The first line will examine the contents of the $cache parameter (the local cache folder name).  It uses a regular expression to check and add a trailing slash if necessary.  The second line creates a file name that will be used to store or retrieve the cached user feed.

{mospagebreak title=Understanding the logic}

The idea is to grab a user’s XML feed data and store the information locally in a file named for the user feed (identified by the URL rather than the user’s name).  Since this leads to extremely long file names, the MD5 function creates a shorter, more consistent naming pattern and also adds a bit of privacy.

If you’re wondering why I’ve named the file based on the URL instead of the user name, take a look at the fetchAvatar function.  This data must also be cached.  The only data that is common between the two functions is the user feed URL.  This ensures that the cached feed and the cached avatar have the same naming pattern.

If you recall, we added the strtolower() function call to the constructor when passing the user name value into the fetchUserInfo function.  This ensures that the user name passed into this function is always in lower case form.  The reason is the use of the MD5 function above.  Take a look at the following two URLs.

http://twitter.com/statuses/user_timeline/WindowsGuru.xml?count=1

http://twitter.com/statuses/user_timeline/windowsguru.xml?count=1

While both of these URLs appear to (and in fact do) point to the same user feed location, due to their difference in case they will produce two different MD5 values.  This means that the same user data could be stored in two separate cache files, making it nearly impossible to load from a cache properly.  By forcing the case of the user name before constructing the URL, the MD5 function will always return the same for any given user name, regardless of the case the end-user uses when supply the name.

        if (@filemtime($cache_file) + $this->cache_expires > time()) {

            $xml = @file_get_contents($cache_file);

        } else {

            $xml = $this->curlRequest($url);

            if ($xml !== false) {

                file_put_contents($cache_file, $xml);

            }

        }

The next piece of logic performs the caching magic.  If the creation time on the cache file plus the cache expiration time is greater than the current system time (meaning the cache file is not older than the expiration time), the file contents are retrieved using the file_get_contents() function.  Otherwise, we make a request to the Twitter API using our curlRequest method and store the file contents to a local cache file.

You may be looking at this logic and thinking to yourself that a piece is missing.  The entire logic here relies on the ability to read the time stamp on the local cache file.  But what if that file doesn’t exist yet?  Won’t the logic fail? 

The short answer is yes, relying on that exclusively would create a problem.  However, you’ll notice that I’ve added the @ symbol in front of my call to the filemtime() function to suppress any errors that may occur of the time cannot be read.  Since filemtime() simply returns false in the event of an error, this provides a shortcut that pushes the logic into the Else block anyway.  The key here is that I’ve suppressed any error from being displayed in the browser.

I have intentionally ignored a third scenario here in which a local file exists but cannot be read.  Since the chances of this are pretty slim and this isn’t a mission critical application, I didn’t take the time to worry about it.  You could add another If block based around the result of the file_get_contents() method that will make a call to curlRequest in the event that it returns false, which would force a new user feed to be downloaded if the local copy were ever unreadable.

        if ($xml === false && file_exists($cache_file)) {

            $xml = file_get_contents($cache_file);

        }

        if ($xml === false) {

            throw new SignatureImageException(‘User feed unavailable.’);

        }

The final piece of logic checks to see if any user feed data exists at this point in the script.  If not, and a local copy exists (assuming a CURL request failed for whatever reason), the file is loaded locally even if the cached version is outdated.  This prevents the script from bombing out if the Twitter API should be down for any reason.

If at this point a user feed still does not exists (meaning it could not be downloaded and there was no usable local copy), the second If block with throw an exception explaining that the user feed was not available.

{mospagebreak title=Finishing up the application}

Now that caching has been added, the Twitter Signature Image application is now complete and fully operational.  The only thing left to do is upload it to a web server and start using it.  However, you may wish to have some idea of how many times your image is being displayed.  So let’s take a minute to quickly add a database logging system.  (The remaining part of this article is completely optional).

First, you’ll need to create a MySQL database in which to store your information.  You can simply import the SQL code below, using phpMyAdmin to create one.

CREATE DATABASE `tlog`;

USE `tlog`;

 

CREATE TABLE `tlog` (

  `name` VARCHAR(25) NOT NULL PRIMARY KEY,

  `counter` INT(10) UNSIGNED NOT NULL

) engine=MyISAM default charset=latin1;

This will create a database with two fields.  One will store a Twitter user name, and the second will store a simple hit count.

Adding this to your application is extremely simple.  The signature image is only displayed if the SignatureImage class is correctly executed without error.  Because of this, we’ll place our logging code in the SignatureImage class destructor (since the destructor is only executed if the class is correctly created.)

    public function __destruct()

    {

        @mysql_connect(‘localhost’, ‘user’, ‘password’);

        @mysql_select_db(‘tlog’);

        $query = "INSERT INTO tlog VALUES (LOWER(‘{$this->screen_name}’), 1)"

               . " ON DUPLICATE KEY UPDATE counter = counter + 1";

        @mysql_query($query);

This code works very efficiently only executing a single query:

INSERT INTO tlog VALUES (LOWER(‘someusername’), 1)

  ON DUPLICATE KEY UPDATE counter = counter + 1";

This insert query inserts a user name into the database table with a value of 1 for the counter field.  The magic of its simplicity is in the ON DUPLICATE KEY UPDATE portion of the query.  If the user name already exists in the table, the counter field is incremented instead of a new row being inserted.

        if (file_exists($this->local_avatar)) {

            unlink($this->local_avatar);

        }

    }

Finally, we add a line to delete the local avatar image.  Since this image is downloaded every time the code is executed, the local image can be deleted to prevent the avatar directory from becoming unnecessarily bloated.

Thanks for tuning in for this article series.  I hope you had fun creating your own Twitter Signature Image application and that you learned a bit about PHP 5’s exceptions as a means of error handling.  Take a moment to download my fully commented source code for this series below.  You’ll be able to see how I’ve pieced together my files along with full phpDocumentor compatible comments.  Until next time, keep coding!

 

[gp-comments width="770" linklove="off" ]

antalya escort bayan antalya escort bayan Antalya escort diyarbakir escort