| home / programming / phpanth3 / 1 | [previous] |
|
|
Having seen how to disable client side caching and deal with server side caching, it’s time to look at a mechanism that allows us to take advantage of client side caches in a way that can be controlled from within a PHP script. This approach will only work if you are running PHP with an Apache Web server, because it requires use of the function getallheaders to fetch the HTTP headers sent by a Web browser. This function only works with Apache.
If you’re using PHP version 4.3.0+ on Apache, HTTP headers are available with the functions apache_request_headers and apache_response_headers . The function getallheaders has become an alias for the new apache_request_headers function.
The mechanism for dealing with Web browser caches is again HTTP. A number of headers are involved in instructing Web browsers and proxy servers whether to cache a page; the situation is further complicated by the fact that some are only available with HTTP 1.1.
A simple but very handy tool for examining request and response headers is LiveHttpHeaders , which is an add-on to the Mozilla browser. It’s worth knowing exactly what headers your script is sending, particularly when you’re dealing with HTTP cache headers.
From the point of view of keeping it simple, we’ll concentrate here on the HTTP 1.0 caching headers only, namely Expires, Last-Modified , and If-Modified-Since , as well as HTTP status code 304 (Not Modified).
Those headers available with HTTP 1.1, such as Cache-Control and ETAG , are intended to provide an advanced mechanism that can be used in conjunction with a Web session’s state; in other words, the version of a given page displayed to a visitor who’s not logged in may differ vastly from that displayed to a logged-in user. The HTTP 1.1 headers were added primarily to allow the caching of such pages.
The header that’s easiest to use is the Expires header, which sets a date (presumably in the future) on which the page will expire. Until that time, Web browsers are allowed to use a cached version of the page.
An example:
Example 5.15. 6.php
<?php
/**
* Sends the Expires HTTP 1.0 header.
* @param int number of seconds from now when page expires
*/
function setExpires($expires)
{
header('Expires: ' .
gmdate('D, d M Y H:i:s', time() + $expires) . 'GMT');
}
// Set the Expires header
setExpires(10);
// Display a page
echo 'This page will self destruct in 10 seconds<br />';
echo 'The GMT is now ' . gmdate('H:i:s') . '<br />';
echo '<a href="' . $_SERVER['PHP_SELF'] .
'">View Again</a><br />';
?>
The setExpires function sets the HTTP Expires header to a future time, defined in seconds. The above example shows the current time in GMT and provides a link that allows you to view the page again. Using your browser’s button, you might tell the browser to refresh the cache. Using this link, you’ll notice the time updates only once every ten seconds.
HTTP dates are always calculated relative to Greenwich Mean Time (GMT). The PHP function gmdate is exactly the same as the date function, except it automatically offsets the time to GMT, based on your server’s system clock and regional settings.
When a browser encounters an Expires header, it caches the page. All further requests for the page that are made before the specified expiry time use the cached version of the page; no request is sent to the Web server.
The Expires header has the advantage of being easy to implement, but for most cases, unless you’re a highly organized person, you won’t know exactly when a given page on your site will be updated. Because the browser will only contact the server after the page has expired, there’s no way to tell browsers that the page they’ve cached is out of date. You also lose some knowledge of the traffic to your Website, as the browser will not make contact with the server when requesting a page that has been cached.
A more useful approach is to make use of the Last-Modified and If-Modified-Since headers, both of which are available in HTTP 1.0. Technically, this is known as performing a conditional GET ; whether you return any content is based on the condition of the incoming If-Modified-Since request header.
Using this approach, you need to send a Last-Modified header every time your PHP script is accessed. The next time the browser requests the page, it sends an If-Modified-Since header containing a time; your script can then identify whether the page has been updated since the time provided. If it hasn’t, your script sends an HTTP 304 status code to indicate that the page has not been modified, and exits before sending the body of the page.
Providing a simple example of conditional GETs is tricky, but PEAR::Cache_Lite is a handy tool to show how this works. Don’t get confused though; this is not meant to show server side caching; it simply provides a file that’s updated periodically.
Here’s the code:
Example 5.16. 7.php (excerpt)
<?php
// Include PEAR::Cache_Lite
require_once 'Cache/Lite.php';
// Define options for Cache_Lite
$options = array(
'cacheDir' => './cache/'
);
// Instantiate Cache_Lite
$cache = new Cache_Lite($options);
// Some dummy data to store
$id = 'MyCache';
// Initialize the cache if first time the page is requested
if (!$cache->get($id)) {
$cache->save('Dummy', $id);
}
// A randomizer...
$random = array(0, 1, 1);
shuffle($random);
// Randomly update the cache
if ($random[0] == 0) {
$cache->save('Dummy', $id);
}
// Get the time the cache file was last modified
$lastModified = filemtime($cache->_file);
// Issue an HTTP last modified header
header('Last-Modified: ' .
gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
// Get client headers - Apache only
$request = getallheaders();
if (isset($request['If-Modified-Since'])) {
// Split the If-Modified-Since (Netscape < v6 gets this wrong)
$modifiedSince = explode(';', $request['If-Modified-Since']);
// Turn the client request If-Modified-Since into a timestamp
$modifiedSince = strtotime($modifiedSince[0]);
} else {
// Set modified since to 0
$modifiedSince = 0;
}
// Compare time the content was last modified with client cache
if ($lastModified <= $modifiedSince) {
// Save on some bandwidth!
header('HTTP/1.1 304 Not Modified');
exit();
}
echo 'The GMT is now ' . gmdate('H:i:s') . '<br />';
echo '<a href="' . $_SERVER['PHP_SELF'] .
'">View Again</a><br />';
?>
Remember to use the “View Again” link when you run this example (clicking usually clears your browser’s cache). If you click on the link repeatedly, eventually the cache will be updated; your browser will throw out its cached version and fetch a new page rendered by PHP.
In the above example we used PEAR::Cache_Lite to create a cache file that is updated randomly. We ascertain the file modification time of the cache file with this line:
$lastModified = filemtime($cache->_file);
Technically speaking, this is a hack, as PEAR::Cache_Lite intends its $_file member variable to be private. However, we must use it to get the name of the cache file so that we can fetch its modification time.
Next, we send a Last-Modified header using the modification time of the cache file. We need to send this for every page we render, to cause visiting browsers to send us the If-Modifed-Since header upon every request.
// Issue an HTTP last modified header
header('Last-Modified: ' .
gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
Use of the getallheaders function ensures that PHP gives us all the incoming request headers as an array. We then need to check that the If-Modified-Since header actually exists; if it does, we have to deal with a special case caused by older Mozilla browsers (below version 6), which appended an (illegal) extra field to their If-Modified-Since headers. Using PHP’s strtotime function, we generate a timestamp from the date the browser sent us. If there is no such header, we set this timestamp to zero, forcing PHP to give the visitor an up-to-date copy of the page.
// Get client headers - Apache only
$request = getallheaders();
if (isset($request['If-Modified-Since'])) {
// Split the If-Modified-Since (Netscape < v6 gets this wrong)
$modifiedSince = explode(';', $request['If-Modified-Since']);
// Turn the client request If-Modified-Since into a timestamp
$modifiedSince = strtotime($modifiedSince[0]);
} else {
// Set modified since to 0
$modifiedSince = 0;
}
Finally, we check to see whether the cache has been modified since the last time the visitor received this page. If it hasn’t, we simply send a Not Modified response header and exit the script, saving bandwidth and processing time by instructing the browser to display its cached copy of the page.
// Compare the time the content was last modified with cache
if ($lastModified <= $modifiedSince) {
// Save on some bandwidth!
header('HTTP/1.1 304 Not Modified');
exit();
}
If you combine the Last-Modified approach with time values that are already available in your application (e.g. the time of the most recent news article, or expiry times from the server side caching system we saw in the last solution), you should be able to take advantage of Web browser caches and save bandwidth, while being able to gather your site’s traffic information and improve its perceived performance.
Be very careful to test any caching performed in this manner, though; if you get it wrong, you may cause your visitors to have permanently out of date copies of your site.
Caching Tutorial for Web Authors and Webmasters: http://www.mnot.net/cache_docs/
This article represents the definitive discussion of Web caching.
Issuing Correct HTTP Headers: http://perl.apache.org/docs/general/correct_headers/correct_headers.html
This tutorial provides a useful discussion of HTTP headers in Perl, which can be readily applied to PHP.
HTTP 1.1 RFC 2616 on Cache Control: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
Here, you’ll find a precise description of HTTP 1.1 cache control headers.
Output Buffering, and how it can Change Your Life: http://www.zend.com/zend/art/buffering.php
Zeev Suraski gives a short tour of what can be done with PHP’s output buffering in this great article.
Output Buffering with PHP: http://www.devshed.com/Server_Side/PHP/OutputBuffering/
this article provides another look at PHP’s output buffering, with notes on using it to capture PHP errors.
Caching PHP Programs with PEAR: http://www.onlamp.com/pub/a/php/2001/10/11/pearcache.html
Sebastian Bergmann introduces PEAR::Cache.
| home / programming / phpanth3 / 1 | [previous] |
Created: March 11, 2003
Revised: January 2, 2004
URL: http://webreference.com/programming/phpanth3