http://rithiur.anthd.com/tutorials/conditionalget.php
Copyright © 2007 - 2008 to Rithiur
line numbers: [ On, Off ]
Style [ Php Style, Custom Style, Phpedit Style, Plain Style ]
  1. <?php
  2.  
  3. define('PAGE_TITLE', 'Implementing support for Conditional Get with PHP');
  4. include '../includes/header.php';
  5.  
  6. ?>
  7.  
  8. <h2>Implementing support for Conditional Get with PHP</h2>
  9.  
  10. <p>
  11.  Conditional Get is a method for web browsers to use cached versions of a web
  12.  page instead of downloading it, if it hasn't been modified since last visit.
  13.  The purpose of this feature is to improve the general performance of web
  14.  browsing, when you don't need to repeatedly download the same content over
  15.  and over again. In addition this will also reduce the bandwidth consumed by 
  16.  web pages.
  17. </p>
  18.  
  19. <p>
  20.  Conditional Get is mostly used just for static files, such as images and
  21.  stylesheets. You can't really cache dynamic pages, because the content of the
  22.  page changes on every request. For normal files, you don't even need to do
  23.  anything special, since most server software automatically handles conditional
  24.  get for these static files. However, because PHP pages are generated dynamically
  25.  on each page request, it's not possible for server to automatically determine
  26.  when was the last time the content changed
  27. </p>
  28.  
  29. <p>
  30.  Of course, most of the time it's not even possible to support conditional get,
  31.  as the contents are dynamic, but sometimes you need to create almost static
  32.  files with PHP. These could include, for example, images or stylesheets that
  33.  are generated or modified by PHP, even though they don't change very often. In
  34.  these cases, you will need to write support for conditional get yourself in
  35.  PHP. This tutorial will show you how to do that.
  36. </p>
  37.  
  38. <h2>What you need to do</h2>
  39.  
  40. <p>
  41.  Supporting conditional get with PHP is not particularly hard. The idea is that
  42.  you send browsers two different headers: "Last-Modified" and "ETag". The
  43.  "Last-Modified" header should contain the last time the file was modified and
  44.  "ETag" should contain a unique indentifier for that version of themodified file.
  45.  To put it simply, the contents of "ETag" should change whenever the page changes.
  46.  The "ETag" field is defined as something that only the origin server can
  47.  understand, so basically it can contain anything you want, as long as it just
  48.  changes with the content. A simple way to achieve this could be, for example,
  49.  using md5 of the last modification date. The value should be inside quotes,
  50.  even though most browsers will simply treat the value of the header as a string.
  51. </p>
  52.  
  53. <p>
  54.  Sending the headers are naturally done using <code>header()</code> function.
  55.  The Last-Modification date should contain the date in GMT according to 
  56.  format specified in <a href="http://www.ietf.org/rfc/rfc2616.txt">rfc2616</a>.
  57.  In other words, the proper format to use in <code>gmdate()</code> is
  58.  <code>"D, d M Y H:i:s \G\M\T"</code>. Although, in PHP 4.3.11 and newer
  59.  versions you can also just use: <code>"r"</code>
  60. </p>
  61.  
  62. <p>
  63.  The biggest problem for you is to determine the last modification date. This
  64.  depends on what you are actually doing. With RSS feeds, for example, you
  65.  usually want to use the last time an item was added to the RSS feed. With
  66.  other things, you just need design your system so that there is a way for you
  67.  to determine the last modification date for the content you wish to support
  68.  Conditional Get.
  69. </p>
  70.  
  71. <p>
  72.  Once you have been able to determine the last modification date, you can send
  73.  the proper headers like this (assuming you have the last modification date as
  74.  unix timestamp in <code>$time</code>):
  75. </p>
  76.  
  77. <code class="box">$lastmod = gmdate('D, d M Y H:i:s \G\M\T', $time);
  78. $etag = '"' . md5($lastmod) . '"';
  79. header("Last-Modified: $lastmod");
  80. header("ETag: $etag");</code>
  81.  
  82. <p>
  83.  Now the browser gets the proper headers, which it will also send in on the next
  84.  request it makes to the same page.
  85. </p>
  86.  
  87. <p>
  88.  To actually support the conditional get, we need to check if the browser sent
  89.  the appropriate headers and see if they match to the once we are about to send.
  90.  The headers which contain the last modification date and the etag are
  91.  "If-Modified-Since" and "If-None-Match". These are available to the PHP script
  92.  in <code>$_SERVER['HTTP_IF_MODIFIED_SINCE']</code> and
  93.  <code>$_SERVER['HTTP_IF_NONE_MATCH']</code>. What you need to do, is to check
  94.  if these two variables are set, and if they are whether they match the
  95.  <code>$lastmod</code> and <code>$etag</code> which were previously set. If they
  96.  are, you should send a 304 header instead of the page contents.
  97. </p>
  98.  
  99. <p>
  100.  The checking can be achieved be using code like:
  101. </p>
  102.  
  103. <code class="box">$ifmod = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
  104.     ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] == $lastmod
  105.     : null;
  106.  
  107. $iftag = isset($_SERVER['HTTP_IF_NONE_MATCH'])
  108.     ? $_SERVER['HTTP_IF_NONE_MATCH'] == $etag
  109.     : null;
  110.  
  111. if (($ifmod || $iftag) && ($ifmod !== false && $iftag !== false))
  112. {
  113.     header('HTTP/1.0 304 Not Modified');
  114.     die;
  115. }</code>
  116.  
  117. <p>
  118.  The previous code will check if either of the headers is set and if the ones
  119.  that are set matches, it will send the 304 header and end the page execution.
  120. </p>
  121.  
  122. <p>
  123.  The gain maximium benefit from supporting the conditional get, you should try
  124.  to do the checking as early in the page processing as possible and kill the
  125.  script if 304 header is sent. This way, it won't do unnecessary processing that
  126.  is required to output the actual page. In RSS feeds this would mean that only
  127.  build the actual feed once you checked for the conditional get.
  128. </p>
  129.  
  130. <p>
  131.  And that is all you need to do to support conditional get.
  132. </p>
  133.  
  134. <h2>Example Function</h2>
  135.  
  136. <p>
  137.  Here is an example function that I use myself to easily provide the support
  138.  where I need:
  139. </p>
  140.  
  141. <code class="box">// Function for checking conditional get
  142. function conditionalget ($lastmod)
  143. {
  144.     $lastmod = gmdate('D, d M Y H:i:s', intval($lastmod)) . ' GMT';
  145.     $etag = '"' . md5($lastmod) . '"';
  146.  
  147.     // ETag is sent even with 304 header
  148.     header("ETag: $etag");
  149.  
  150.     $ifmod = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
  151.         ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] == $lastmod
  152.         : null;
  153.  
  154.     $iftag = isset($_SERVER['HTTP_IF_NONE_MATCH'])
  155.         ? $_SERVER['HTTP_IF_NONE_MATCH'] == $etag
  156.         : null;
  157.  
  158.     // If either matches and neither is a mismatch, send not modified header
  159.     if (($ifmod || $iftag) && ($ifmod !== false && $iftag !== false))
  160.     {
  161.         header('HTTP/1.0 304 Not Modified');
  162.         die;
  163.     }
  164.  
  165.     // Last-Modified doesn't need to be sent with 304 response
  166.     header("Last-Modified: $lastmod");
  167. }</code>
  168.  
  169. <p>
  170.  Use of the function is very simple. Just call it with the last modification
  171.  time and it will handle sending all the headers and sending the appropriate
  172.  304 header, if needed. Note that it will end the page execution if 304
  173.  header is sent. Also, naturally as the script deals with headers, this needs
  174.  to be done before any content is sent to the browser.
  175. </p>
  176.  
  177. <h2>Information about the HTTP1.1 standard</h2>
  178.  
  179. <p>
  180.  One small thing to note is that the implementation of conditional get this way
  181.  is not exactly according to the HTTP1.1 protocol standard. However, it is what
  182.  I would call "Good Enough". This should work with most browsers, and at least
  183.  I don't personally know any browser that doesn't work with the above example.
  184. </p>
  185.  
  186. <p>
  187.  The problem with the standard is that handling of the "If-Modified-Since" and
  188.  "If-None-Match" headers are quite a bit more complex. The modification date
  189.  header should not be treated as string and actually few other date strings
  190.  should be accepted as well. However, the HTTP1.1 standard does suggest that
  191.  browsers should send the modification date header exactly as received, because
  192.  servers tend to do exact string comparisons. That, however, is not enforced.
  193. </p>
  194.  
  195. <p>
  196.  The "If-None-Match" header may actually contain multiple etags from different
  197.  cached versions of the page or a literal "*". The proper way to handle the
  198.  header is to check if any of the tags sent matches the tag that would be sent
  199.  by the page. However, it appears that browsers will treat the ETag field as
  200.  literal string instead, and send it exactly as it is, even if it is malformed.
  201.  Thus, equal string comparison as in the example above is "good enough" way to
  202.  handle the header.
  203. </p>
  204.  
  205. <p>
  206.  You might be wondering why both of these headers are used, when it should be
  207.  logical that only one of them is enough. Ironically, this is because the
  208.  HTTP1.1 standard suggests that both of them are sent whenever possible. Also,
  209.  this is because theoretically it should offer better compatibility, because it
  210.  will also work on browsers that support only one of these headers.
  211. </p>
  212.  
  213. <?php include '../includes/footer.php'; ?>
line numbers: [ On, Off ]
Style [ Php Style, Custom Style, Phpedit Style, Plain Style ]
http://rithiur.anthd.com/tutorials/conditionalget.php
Exec time: 0.0577 seconds
Lines: 213