{"id":2152,"date":"2014-02-10T11:25:58","date_gmt":"2014-02-10T09:25:58","guid":{"rendered":"http:\/\/www.nyamsprod.com\/blog\/?p=2152"},"modified":"2014-04-26T11:36:13","modified_gmt":"2014-04-26T09:36:13","slug":"tutorial-how-to-cache-a-resource-using-php","status":"publish","type":"post","link":"https:\/\/nyamsprod.com\/blog\/tutorial-how-to-cache-a-resource-using-php\/","title":{"rendered":"Tutorial : How to cache a resource using PHP"},"content":{"rendered":"<div class=\"message warning\">\n<p><strong>Attention:<\/strong> Les informations de ce billet sont susceptibles d'&ecirc;tre obsol&egrave;tes car vieux de plus 2 ans.<\/p>\n<p><strong>Warning: <\/strong> The information you are reading may be obsolete, this post was published more than 2 years ago.<\/p>\n<\/div><p>In this tutorial I&#8217;ll show you how to add a smart cache system to a resource using PHP. This tutorial can be adapted to any scripting language as long as you adapt the functions too, since the logic will remain the same.<\/p>\n<h2>Starting point<\/h2>\n<p>The tutorial uses a very basic script:<\/p>\n<ul>\n<li>A user sends some informations to a server;<\/li>\n<li>Using this information, a script generate some data;<\/li>\n<li>The data is then returned to the client encoded in Json format;<\/li>\n<\/ul>\n<p>So we have 3 functions:<\/p>\n<ul>\n<li><code>filterInput<\/code> that filter incoming parameters from the client request;<\/li>\n<li><code>fetchData<\/code> that fetch the data using the developer business logic;<\/li>\n<li><code>outputEscapedJson<\/code> that encode the data to be Json outputted correctly;<\/li>\n<\/ul>\n<p>For the purpose of this tutorial we will assume that those functions have been tested and they work \ud83d\ude42 !<\/p>\n<h3>PHP default behavior<\/h3>\n<p>So the script is really as simple as the code below:<\/p>\n<pre><code class=\"language-php\">&lt;?php\r\n$params = filterInput($_GET);\r\n$data = fetchData($params);\r\necho outputEscapedJson($data);<\/code><\/pre>\n<p>This code will work, but you have to keep in mind that PHP made some assumptions when generating the response:<!--more--><\/p>\n<ul>\n<li>PHP sent a <code>HTTP Status Code<\/code> equals to 200 meaning that the client request was OK;<\/li>\n<li>PHP sent a <code>Content-Type<\/code> header specifying that your where delivering HTML content. Your information will be understood by the client (ie: the browser) as being HTML rather than Json;<\/li>\n<\/ul>\n<p>To fix the second issue we will take control of the HTTP Response produced by PHP.<\/p>\n<h3>Introducing PHP <code>header<\/code> function<\/h3>\n<p>To do so, we will use the <code>header<\/code> function from PHP which manipulate HTTP response headers and status code. Using this function you can virtually manipulate any HTTP header (including cookies). So let&#8217;s improve our code:<\/p>\n<pre><code class=\"language-php\">&lt;?php\r\n$params = filterInput($_GET);\r\n$data = fetchData($params);\r\n$json = outputEscapedJson($data);\r\n\r\n\/\/final Response header\r\nheader(\"Content-Type: application\/json; charset=utf-8\");\r\nheader(\"Content-Length: \".strlen($json));\r\ndie($json);<\/code><\/pre>\n<p>We&#8217;ve included 2 headers:<\/p>\n<ul>\n<li>the <code>Content-Type<\/code> that indicate that we are sending UTF-8 generated Json;<\/li>\n<li>the <code>Content-Length<\/code> that indicate the size of the content.;<\/li>\n<\/ul>\n<p>By default when everything is OK, PHP sends a HTTP Status Code equals to 200. So we don&#8217;t need to explicitly change this status.<\/p>\n<p>Now that the browser knows that the response content type will be Json, let&#8217;s cache our resource.<\/p>\n<h2>Adding Server Side Cache<\/h2>\n<p>There are many ways to implement a cache on the server but for simplicity I&#8217;ll implement a file based cache system since PHP provides built-in functions to easily implement this type of cache. But keep in mind that whatever cache system you choose the logic will remain the same:<\/p>\n<ul>\n<li>We need to check if the result is already present in the cache;<\/li>\n<li>If the answer is yes we will use the cache data;<\/li>\n<li>Otherwise, we generate the result and save it in the cache for future use;<\/li>\n<\/ul>\n<p>To do so we will add a new function:<\/p>\n<ul>\n<li><code>fetchCacheFilePath<\/code> which returns the cache file attached to the user <code>$params<\/code>.<\/li>\n<\/ul>\n<pre><code class=\"language-php\">&lt;?php\r\n$params = filterInput($_GET);\r\n$cache = fetchCacheFilePath($params);\r\n\r\n\/\/Server Cache\r\nif (! file_exists($cache)) {\r\n    $data = fetchData($params);\r\n    $json = outputEscapedJson($data);\r\n    file_put_contents($cache, $json);\r\n}\r\n\r\nheader(\"Content-Type: application\/json; charset=utf-8\");\r\nheader(\"Content-Length: \".filesize($cache));\r\nreadfile($cache);<\/code><\/pre>\n<p>Notice how we have change the <code>Content-Length<\/code> header, now the information is taken directly from the file.<\/p>\n<p>Adding a cache system has also introduced several problems, what happens when:<\/p>\n<ul>\n<li>the cache can not saved the data (ie: the file can not be created by <code>file_put_contents<\/code>);<\/li>\n<li>the cache data is corrupted (ie: the file is readable but for unknown reason <code>readile<\/code> does not work);<\/li>\n<li>the data is present but can not be read (ie: the file exists but is not be readable by your webserver);<\/li>\n<\/ul>\n<p>Luckily for us there are HTTP Status Code for that. So let&#8217;s introduce them:<\/p>\n<ul>\n<li>The 500 status code happens when an unexpected condition occurs, so if you can&#8217;t save the data or can&#8217;t output it, we will use this status code;<\/li>\n<li>The 403 Status code means that the server understood my request but is refusing (in our cases not able to) fullfil the request because the resource is not readable;<\/li>\n<\/ul>\n<p>So the code becomes:<\/p>\n<pre><code class=\"language-php\">&lt;?php\r\n$params = filterInput($_GET);\r\n$cache = fetchCacheFilePath($params);\r\n\r\n\/\/Server Cache\r\nif (! file_exists($cache)) {\r\n    $data = fetchData($params);\r\n    $json = outputEscapedJson($data);\r\n    \/\/The cache can not be generated\r\n    if (! file_put_contents($cache, $json)) {\r\n        header(\"HTTP\/1.1 500 Internal Server Error\");\r\n        die;\r\n    }\r\n}\r\n\r\n\/\/Resource accessibility\r\nif (! is_readable($cache)) {\r\n    header(\"HTTP\/1.1 403 Forbidden\");\r\n    die;\r\n}\r\n\r\n\/\/Final response header\r\nheader(\"Content-Type: application\/json; charset=utf-8\");\r\nheader(\"Content-Length: \".filesize($cache));\r\nif (! @readfile($cache)) {\r\n    header(\"HTTP\/1.1 500 Internal Server Error\");\r\n    header(\"Content-Length: 0\");\r\n    die;\r\n}\r\n<\/code><\/pre>\n<p><strong>Of note:<\/strong><\/p>\n<ul>\n<li>For the last 500 status code, I&#8217;m adding the <code>Content-Length<\/code> header equal to <code>0<\/code> to replace the previously declare header;<\/li>\n<li>To be totally complete I&#8217;ll advise the developer to add an event dispatcher system to this process so that when errors like those documented here happen he(she) is informed by mail for example;<\/li>\n<\/ul>\n<p>This cache works but it has a problem, it never expires!!<\/p>\n<p>This could be problematic, if what you store can changed in time, we need to add expiration logic so that our cache remains &#8220;fresh&#8221;. Let do that by moving the <code>file_exists<\/code> function into a more appropriate function <code>isCacheValid<\/code>.<\/p>\n<p>This will result in the code below:<\/p>\n<pre><code class=\"language-php\">&lt;?php\r\ndate_default_timezone_set('UTC');\r\n\r\n\/**\r\n* Check to see if the cache is still valid\r\n*\r\n* @param string $cache cache file path\r\n* @param string $ttl a string representing the relative formats supported by the parser used for strtotime() and DateTime.\r\n*\r\n* @return boolean\r\n*\/\r\nfunction isCacheValid($cache, $ttl)\r\n{\r\n    if (! file_exists($cache)) {\r\n        return false;\r\n    }\r\n    \/\/check if the file needs to be refreshed or not ?\r\n    $last_modified = new DateTime('@'.filemtime($cache));\r\n    $expire = new DateTime('-'.$ttl, new DateTimezone('UTC')));\r\n    return $last_modified &gt; $expire;\r\n}\r\n\r\n$ttl = '1 DAY'; \/\/duration of the server cache\r\n$params = filterInput($_GET);\r\n$cache = fetchCacheFilePath($params);\r\n\/\/Smarter Server Cache\r\nif (! isCacheValid($cache, $ttl)) {\r\n    $data = fetchData($params);\r\n    $json = outputEscapedJson($data);\r\n    if (! file_put_contents($cache, $json)) {\r\n        header(\"HTTP\/1.1 500 Internal Server Error\");\r\n        die;\r\n    }\r\n    <code>\/\/because the file might have been regenerated\r\n    clearstatcache(true, $cache);<\/code>\r\n}\r\n\r\nif (! is_readable($cache)) {\r\n    header(\"HTTP\/1.1 403 Forbidden\");\r\n    die;\r\n}\r\n\r\nheader(\"Content-Type: application\/json; charset=utf-8\");\r\nheader(\"Content-Length: \".filesize($cache));\r\nif (! @readfile($cache)) {\r\n    header(\"HTTP\/1.1 500 Internal Server Error\");\r\n    header(\"Content-Length: 0\");\r\n    die;\r\n}<\/code><\/pre>\n<p><strong>Of note:<\/strong><\/p>\n<ul>\n<li>Since we are refreshing according to time it is best practice to indicate your current Timezone with <code>date_default_timezone_set<\/code>. To make sure that the DateTime objects are all compared on the same settings;<\/li>\n<li>We&#8217;ve added the <code>clearstatcache<\/code> function because the cache file can be regenerated even if it already existed;<\/li>\n<\/ul>\n<p>Server side speaking, your cache is finished and all is well. But what if you could improve even more your cache logic using your client&#8217;s application cache features. This would make your script even more awesome, let&#8217;s do that!!<\/p>\n<h2>Adding Client Side Cache on a HTTP compliant applications (ie: a browser)<\/h2>\n<p>If implemented correctly this cache will reduce your application bandwidth by caching the resource on the client software.<\/p>\n<p>To use the HTTP protocol for caching you need to provide 2 types of rules to the client:<\/p>\n<ul>\n<li>a validation rule to validate the content you wish to cache;<\/li>\n<li>a refresh rules to indicate when you wish the resource to be refresh;<\/li>\n<\/ul>\n<p>The HTTP protocol provides several mechanisms but for this tutorial I&#8217;ll use:<\/p>\n<ul>\n<li>the last modified header as a validator information;<\/li>\n<li>the expires headers as a refresh information;<\/li>\n<\/ul>\n<p>Once again the strategy you choose depends on how and what you wish to save. But whatever the chosen solution is, the logic remains the same.<\/p>\n<ul>\n<li>The client issue a first request;<\/li>\n<li>The server sends to the client a validator and a refresh information header;<\/li>\n<li>The client issue a second request by adding a new header request for the resource;<\/li>\n<li>Depending on the presence and on the value of the new header the server will tell the browser to use its cache or will send a updated version of the resource;<\/li>\n<\/ul>\n<p>Let&#8217;s update once again our code \ud83d\ude42 .<\/p>\n<pre><code class=\"language-php\">&lt;?php\r\ndate_default_timezone_set('UTC');\r\n\r\nfunction isCacheValid($cache, $ttl)\r\n{\r\n    if (! file_exists($cache)) {\r\n        return false;\r\n    }\r\n    $last_modified = new DateTime('@'.filemtime($cache));\r\n    $expire = new DateTime('-'.$ttl, new DateTimezone('UTC')));\r\n    return $last_modified &gt; $expire;\r\n}\r\n\r\n$ttl = '1 DAY';\r\n$params = filterInput($_GET);\r\n$cache = fetchCacheFilePath($params);\r\nif (! isCacheValid($cache, $ttl)) {\r\n    $data = fetchData($params);\r\n    $json = outputEscapedJson($data);\r\n    if (! file_put_contents($cache, $json)) {\r\n        header(\"HTTP\/1.1 500 Internal Server Error\");\r\n        die;\r\n    }\r\n    <code>clearstatcache(true, $cache);<\/code>\r\n}\r\n\r\nif (! is_readable($cache)) {\r\n    header(\"HTTP\/1.1 403 Forbidden\");\r\n    die;\r\n}\r\n\r\n\/\/HTTP Cache Verification\r\n$last_modified = new DateTime('@'.filemtime($cache));\r\n$expires = clone $last_modified;\r\n$expires-&gt;modify('+'.$ttl);\r\nheader(\"Last-Modified: \".$last_modified-&gt;format(DATE_RFC1123));\r\nheader(\"Expires: \".$expires-&gt;format(DATE_RFC1123));\r\nif (\r\n    isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])\r\n    &amp;&amp; $last_modified &lt;= new DateTime($_SERVER['HTTP_IF_MODIFIED_SINCE'])\r\n) {\r\n    header(\"HTTP\/1.1 304 Not Modified\");\r\n    die;\r\n}\r\n\r\nheader(\"Content-Type: application\/json; charset=utf-8\");\r\nheader(\"Content-Length: \".filesize($cache));\r\nif (! @readfile($cache)) {\r\n    header(\"HTTP\/1.1 500 Internal Server Error\");\r\n    header(\"Content-Length: 0\");\r\n    die;\r\n}<\/code><\/pre>\n<p>The crucial parts added are:<\/p>\n<ul>\n<li>the <code>Last-Modified<\/code> and the <code>Expires<\/code> headers with content according the HTTP headers protocols.<\/li>\n<li>the <code>$_SERVER['HTTP_IF_MODIFIED_SINCE']<\/code> used to check whether the resource is changed or not.<\/li>\n<\/ul>\n<p>If the resource has not changed we issue a <strong>304 HTTP Status Code<\/strong> which means that the browser must use its own cache.<\/p>\n<p><strong>Of Note:<\/strong><\/p>\n<ul>\n<li>for simplicity I&#8217;m using the same cache parameter for the server <strong>and<\/strong> the client but nothing prevents you for having 2 different values for both cache it really depends on you business logic;<\/li>\n<li>Alternatively you can use the <code>Etag<\/code> header as a validator rules and the <code>Cache-control<\/code> header as a refresh rules to achieve the same browser cache. The only difference is that when you use the <code>Etag<\/code> header you must validate you resource against the <code>$_SERVER['IF_NONE_MATCH']<\/code> value if it exists;<\/li>\n<\/ul>\n<p>Following this simple tutorial you have learn how to cache a resource, server side and client side in PHP. But above all, now you should understand that knowing and understanding HTTP protocols is a very important asset for anyone who wants to master how the web works.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Methodology on how to properly cache a resource using a server language like PHP and the HTTP Protocol.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[5],"tags":[79,751,259,412,757,540],"class_list":["post-2152","post","type-post","status-publish","format-standard","hentry","category-web","tag-browser","tag-cache","tag-http-headers","tag-php","tag-scripting","tag-tutorial"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/2152","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/comments?post=2152"}],"version-history":[{"count":5,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/2152\/revisions"}],"predecessor-version":[{"id":2234,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/posts\/2152\/revisions\/2234"}],"wp:attachment":[{"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/media?parent=2152"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/categories?post=2152"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nyamsprod.com\/blog\/wp-json\/wp\/v2\/tags?post=2152"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}