71

I have a PHP 5.1.0 website (actually it's 5.2.9 but it must also run on 5.1.0+).

Pages are generated dynamically but many of them are mostly static. By static I mean the content don't change but the "template" around the content can change over time.

I know they are several cache systems and PHP frameworks already out there, but my host don't have APC or Memcached installed and I'm not using any framework for this particular project.

I want the pages to be cached (I think by default PHP "disallow" cache). So far I'm using:

session_cache_limiter('private'); //Aim at 'public'
session_cache_expire(180);
header("Content-type: $documentMimeType; charset=$documentCharset");
header('Vary: Accept');
header("Content-language: $currentLanguage");

I read many tutorials but I can't find something simple (I know cache is something complex, but I only need some basic stuff).

What are "must" have headers to send to help caching?

James Douglas
  • 3,328
  • 2
  • 22
  • 43
AlexV
  • 22,658
  • 18
  • 85
  • 122
  • 8
    Welcome to StackOverflow. Great first question! – Sampson Dec 29 '09 at 05:11
  • How to specify caching for .js and .css files? Or are they included in the headers generated by the PHP file? – David Spector Jan 06 '20 at 02:12
  • @David Spector You would cache them with your webserver. Under Apache you could do this with an .htaccess look for "Header set Cache-Control". You might want to try to ask a question as comments are not at good place for that :) – AlexV Jan 06 '20 at 03:36

7 Answers7

54

You might want to use private_no_expire instead of private, but set a long expiration for content you know is not going to change and make sure you process if-modified-since and if-none-match requests similar to Emil's post.

$tsstring = gmdate('D, d M Y H:i:s ', $timestamp) . 'GMT';
$etag = $language . $timestamp;

$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false;
$if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? $_SERVER['HTTP_IF_NONE_MATCH'] : false;
if ((($if_none_match && $if_none_match == $etag) || (!$if_none_match)) &&
    ($if_modified_since && $if_modified_since == $tsstring))
{
    header('HTTP/1.1 304 Not Modified');
    exit();
}
else
{
    header("Last-Modified: $tsstring");
    header("ETag: \"{$etag}\"");
}

Where $etag could be a checksum based on the content or the user ID, language, and timestamp, e.g.

$etag = md5($language . $timestamp);
Steve-o
  • 12,678
  • 2
  • 41
  • 60
  • 2
    What you've described would be a Weak E-Tag and should have a "W/" prefix. – Nicholas Shanks Nov 09 '12 at 09:29
  • 1
    I would add "Expires" header as well, as some hosting providers (e.g. servers) send it anyway and they do with date way back in the past, while I think it should be in the future. – Sasho Mar 02 '13 at 05:00
  • 5
    If you're not getting 304s with the above, check there aren't any stray quote marks in HTTP_IF_NONE_MATCH. Replace `$if_none_match == $etag` with `rtrim(ltrim($if_none_match, "'\""), "'\"") == $etag` to be sure. – ReactiveRaven Apr 11 '13 at 11:24
  • 1
    ... and not to forget that an ideal E-Tag is calculated based on the fact that you can read the page content. The HTML that is to be sent to the browser, BEFORE sending it to the browser that is. Generally it also implies use of buffers. Probably already managed by modern php frameworks. If you want to send a "please do not cache me" header, its better use a Cache-Control. Why Cache-Control and not Pragma? [Because Pragma is deprecated](https://www.mnot.net/cache_docs/#PRAGMA). In consequence, its better to send a Cache-Control header. Points down to give misleading. Nice try, though :) – renoirb May 28 '14 at 20:33
  • @renoirb this is more relevant as a new answer than here. – Steve-o May 29 '14 at 15:06
  • 2
    Where is the `$timestamp` coming from? Should that be defined before the rest of the code `$timestamp = time();` instead? Am I missing something? – igasparetto Jul 14 '14 at 12:52
  • @igasparetto it is the effective last modified time of the resource being cached. It is entirely dependent upon what you are attempting to cache. – Steve-o Jan 24 '15 at 17:37
  • Now working for me. I see in network chrome panel that there's no cache :( – Paolo Falomo May 13 '16 at 10:50
  • C/C++ programmers are great in network protocols. thank you for that helpful answer. For people asking about the timestamp. the [filemtime](http://php.net/manual/en/function.filemtime.php) function `$timestamp = filemtime($fileName);` might be helpful to get the last modified time of the file – Accountant م Apr 22 '17 at 09:26
  • @ReactiveRaven I wish I read your comment before spending hours on debugging :( – Accountant م Aug 27 '17 at 09:28
16

You must have an Expires header. Technically, there are other solutions, but the Expires header is really the best one out there, because it tells the browser to not recheck the page before the expiration date and time and just serve the content from the cache. It works really great!

It is also useful to check for a If-Modified-Since header in the request from the browser. This header is sent when the browser is "unsure" if the content in it's cache is still the right version. If your page is not modified since that time, just send back an HTTP 304 code (Not Modified). Here is an example that sends a 304 code for ten minutes:

<?php
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  if(strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) < time() - 600) {
    header('HTTP/1.1 304 Not Modified');
    exit;
  }
}
?>

You can put this check early on in your code to save server resources.

Emil Vikström
  • 90,431
  • 16
  • 141
  • 175
12

Take your pick - or use them all! :-)

header('Expires: Thu, 01-Jan-70 00:00:01 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
Mike Foster
  • 321
  • 2
  • 5
  • session_cache_limiter and session_cache_expire already control Expires, Cache-Control, Last-Modified and Pragma... – AlexV Dec 29 '09 at 14:08
  • I see. Yes, you are right. And I also see that I did not read your question well enough. sorry – Mike Foster Dec 29 '09 at 14:42
9

Here's a small class that does http caching for you. It has a static function called 'Init' that needs 2 parameters, a timestamp of the date that the page (or any other file requested by the browser) was last modified and the maximum age, in seconds, that this page can be held in cache by the browser.

class HttpCache 
{
    public static function Init($lastModifiedTimestamp, $maxAge)
    {
        if (self::IsModifiedSince($lastModifiedTimestamp))
        {
            self::SetLastModifiedHeader($lastModifiedTimestamp, $maxAge);
        }
        else 
        {
            self::SetNotModifiedHeader($maxAge);
        }
    }

    private static function IsModifiedSince($lastModifiedTimestamp)
    {
        $allHeaders = getallheaders();

        if (array_key_exists("If-Modified-Since", $allHeaders))
        {
            $gmtSinceDate = $allHeaders["If-Modified-Since"];
            $sinceTimestamp = strtotime($gmtSinceDate);

            // Can the browser get it from the cache?
            if ($sinceTimestamp != false && $lastModifiedTimestamp <= $sinceTimestamp)
            {
                return false;
            }
        }

        return true;
    }

    private static function SetNotModifiedHeader($maxAge)
    {
        // Set headers
        header("HTTP/1.1 304 Not Modified", true);
        header("Cache-Control: public, max-age=$maxAge", true);
        die();
    }

    private static function SetLastModifiedHeader($lastModifiedTimestamp, $maxAge)
    {
        // Fetching the last modified time of the XML file
        $date = gmdate("D, j M Y H:i:s", $lastModifiedTimestamp)." GMT";

        // Set headers
        header("HTTP/1.1 200 OK", true);
        header("Cache-Control: public, max-age=$maxAge", true);
        header("Last-Modified: $date", true);
    }
}
Jasper
  • 534
  • 3
  • 11
8
<?php
header("Expires: Sat, 26 Jul 2020 05:00:00 GMT"); // Date in the future
?>

Setting an expiration date for the cached page is one useful way to cache it on the client side.

S Pangborn
  • 12,593
  • 6
  • 24
  • 24
  • Good, and using session_cache_limiter and session_cache_expire already taked care of this. – AlexV Dec 29 '09 at 14:11
8

This is the best solution for php cache Just use this in the top of the script

$seconds_to_cache = 3600;
$ts = gmdate("D, d M Y H:i:s", time() + $seconds_to_cache) . " GMT";
header("Expires: $ts");
header("Pragma: cache");
header("Cache-Control: max-age=$seconds_to_cache");
Amit Ghosh Anto
  • 141
  • 1
  • 4
4

I was doing JSON caching at the server coming from Facebook feed nothing was working until I put flush and hid error reporting. I know this is not ideal code, but wanted a quick fix.

error_reporting(0);
    $headers = apache_request_headers();
    //print_r($headers);
    $timestamp = time();
    $tsstring = gmdate('D, d M Y H:i:s ', $timestamp) . 'GMT';
    $etag = md5($timestamp);
    header("Last-Modified: $tsstring");
    header("ETag: \"{$etag}\"");
    header('Expires: Thu, 01-Jan-70 00:00:01 GMT');

    if(isset($headers['If-Modified-Since'])) {
            //echo 'set modified header';
            if(intval(time()) - intval(strtotime($headers['IF-MODIFIED-SINCE'])) < 300) {
              header('HTTP/1.1 304 Not Modified');
              exit();
            }
    }
    flush();
//JSON OP HERE

This worked very well.

halfer
  • 19,824
  • 17
  • 99
  • 186
abksharma
  • 576
  • 7
  • 26
  • 1
    It may not work if you don't set field name $headers['If-Modified-Since'] to uppercase -> $headers['IF-MODIFIED-SINCE] – Mr Br Oct 08 '13 at 15:49