Dynamic Application Cache Manifest for PHP

Earlier today Paulo Fierro uploaded an example of how to do a dynamic application cache manifest for HTML5 web apps using Ruby. A cache manifest is a text file that browsers look for in order to determine which files to store locally on the device, this…

Earlier today Paulo Fierro blogged an example of how to build a dynamic application cache manifest for HTML5 web apps using Ruby.

A cache manifest is a text file that browsers look for in order to determine which files to store locally on the device, this lets you open the site/app offline, great for web apps (on iOS bookmarking a site to the home screen also let’s you specify an icon and removes browser chrome). Creating a cache manifest dynamically means that you don’t have to worry about modifying a text file every time you add or remove a file from the app.

This prompted me to upload my PHP version that works pretty much in the same way. I built it for an iPad web app that needed to run completely offline collecting peoples details at a car show (it later synced data with a server when it found a connection). It’s pretty basic, it just creates an MD5 of all the files in a directory (apart from excluded files, including itself!), so that when one is changed, either the content or the name, the MD5 value changes and that results in the contents of the manifest changing, causing the browser to re-evaluate it.

Below is the dynamic manifest, let’s call it “cache.manifest.php”:


<?php 
	header("Cache-Control: max-age=0, no-cache, no-store, must-revalidate");
	header("Pragma: no-cache");
	header("Expires: Wed, 11 Jan 1984 05:00:00 GMT");
	header('Content-type: text/cache-manifest'); 
	
	$hashes = "";
	
	function printFiles( $path = '.', $level = 0 ){ 
		global $hashes;
	    $ignore = array('.', '..','.htaccess','cache.manifest.php','index.html','submit.php', "video.mp4");  

	    $dh = @opendir( $path ); 

	    while( false !== ( $file = readdir( $dh ) ) ){ 
	        if( !in_array( $file, $ignore ) ){ 
	            if( is_dir( "$path/$file" ) ){ 
	                printFiles( "$path/$file", ($level+1) ); 
	            } else { 
					$hashes .= md5_file("$path/$file");
	                echo $path."/".$file."n";
	            } 
	        } 
	    } 

	    closedir( $dh ); 
	}
?>
CACHE MANIFEST
<?php 
printFiles('.');
// version hash changes automatically when files are modified
echo "#VersionHash: " . md5($hashes) . "n";
?>

NETWORK:
./submit.php
./video.mp4

FALLBACK:
./video.mp4 ./images/offline.jpg
./images/video.jpg ./images/offline.jpg

Remember that cache space is extremely limited. I think it was about 5mb on the iPad first gen. If you need more space you’ll have to use local SQLLite (perhaps storing blobs) and request more DB space from the user.

So you probably don’t want to be including things like video files, you also don’t want to cache the output of dynamic server-side scripts. This is what the NETWORK and FALLBACK portions of the manifest are used for. You can force the browser to only look to the network for certain files, and also provide fallbacks for files it won’t find offline (in the case above, it shows an offline image in place of the video).

To make the browser look for the manifest, you just add the manifest attribute to the HTML tag:

<html manifest="cache.manifest.php">

Now when it comes to actually forcing your client app to check for updates, the following JavaScript should provide some insights. Sorry it’s a bit rough and ready, but it should illustrate how you might invoke an update:


var cacheStatus = getCacheStatus(window.applicationCache.status);
	console.log("App cache status: " + cacheStatus); 
	
	var appCachedHandler = function () {
		console.log("AppCache cached");
		dialog.dialog("close");
	}
	var updateReadyHandler = function() {
		console.log("AppCache update ready");
		
		$("#versionImg").attr("src", src="images/update_available.png");
		$("#versionImg").css("visibility", "visible");
		
		dialog.dialog("close");
			
		if(	!isSyncing && confirm("Update available, update now?") ) {
			window.applicationCache.update();
			window.applicationCache.swapCache();
			window.location.reload(true);
		}
	}
	var downloadingCacheHandler = function() {
		console.log("AppCache downloading...");
		dialog.dialog({modal: true, title: 'Downloading update...'});
			
		$("#versionImg").attr("src", src="images/loading_small.gif");
		$("#versionImg").css("visibility", "visible");
	}
	var cacheErrorHandler = function(obj) {
		console.log("AppCache error " + obj);
		dialog.dialog({modal: true, title: 'Cache error:'+obj});
	}
	window.applicationCache.addEventListener('cached', appCachedHandler, false);
	window.applicationCache.addEventListener('updateready', updateReadyHandler, false);
	window.applicationCache.addEventListener('downloading', downloadingCacheHandler, false);
	window.applicationCache.addEventListener('onerror', cacheErrorHandler, false);