Without caching the page yourself (or at least its Etag) you cannot really make use of the 304. A full fledged caching algorithm is somewhat out of scope, but the general idea:
<?php
function getUrlEtag($url){
//some logic to get an etag, possibly stored in memcached / database / file etc.
}
function setUrlEtag($url,$etag){
//some logic to get an etag, possibly stored in memcached / database / file etc.
}
function getPageCache($url,$etag=''){
//[optional]some logic to get the page from cache instead, possibly not even using etag
}
function setPageCache($url,$content,$etag=''){
//[optional]some logic to save the page to cache, possibly not even using etag
}
ob_start();
$etag = getUrlEtag($_SERVER['REQUEST_URI']);
if(isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag) {
header("HTTP/1.1 304 Not Modified");
exit;
}
if(($content=getPageCache($_SERVER['REQUEST_URI'],$etag))!==false){
echo $content;
exit;
}
?>
//the actual page
<?php
$content = ob_get_clean();
setUrlEtag($_SERVER['REQUEST_URI'],$etag=md5($url.$content));
function setPageCache($_SERVER['REQUEST_URI'],$content,$etag);
header("Etag: $etag");
echo $content;
?>
All common pitfalls apply: you can possibly not display cache pages for logged in users, a caching of partial content could be more desirable, you are yourself responsible for preventing stale content in the cache (possibly using triggers in backend or database on modifications, or just playing around with the getUrlEtag logic), etc. etc.
You could also play around with HTTP_IF_MODIFIED_SINCE if that's easier to control.