HTML-Ausgabe mit PHP cachen

Spätestens wenn die eigenen Projekten anfangen zu wachsen, wird so mancher merken, dass seine Scripte nicht ganz so performant sind, wie er oder sie es sich erhofft hatte.
Üblicherweise generieren PHP-Files bei einem Seitenaufruf die komplette Ausgabeseite neu und in den meisten Fällen werden wohl auch Unmengen an Schleifen durchlaufen, andere Files eingelesen oder zahlreiche Datenbankabfragen generiert.
Das braucht Zeit, Ressourcen und gerade bei Datenbankabfragen stößt man schnell an das Limit.
Als ich meine ersten Projekte gestartet habe, dachte ich nie daran, das ich irgendwann damit mal tägliche PIs (Page Impressions) im 5, 6 oder gar 7-stelligen Bereich erreichen könnte.

Was aber machen, wenn die Scripte an die Grenzen Ihrer Leistungsfähigkeit geraten?
Das Erste sollte natürlich sein, dass man den Code überprüft und schaut, wo man optimieren kann.
Sind in der Datenbank die Indizes richtig gesetzt?
Verbergen sich im Script irgendwelche unnötigen Schleifen, bremsende Funktionen oder andere Performancefresser?
Wenn alles nicht mehr hilft, tja, spätestens dann wird es Zeit, sich mal mit dem Thema "Caching" auseinander zu setzen.

Caching bezeichnet das Puffern/Speichern von Ausgabe-Inhalten. Hierbei wird ein Inhalt also einmal erstellt und ausgegeben, dann als Kopie abgelegt und bei einem erneuten Zugriff wird nur noch die Kopie, nicht mehr das komplette Original geladen.

Memcache
Seit der PHP-Version 4.3.3 gibt es hierfür Memcache, ein PECL-Modul das durch die Eigenschaft komplette Objekte im Hauptspeicher ablegen zu können, vor allem dabei hilfreich ist, den Datenbank-Load gravierend reduzieren zu können.
Memcache eignet sich deswegen gerade für sehr große Datenbanken oder Seiten mit enorm vielen PIs.
Leider gehört Memcache nicht zum PHP-Standard und muss separat installiert werden, was wiederum nicht jeder Hoster zulässt.

HTML Caching
Die Alternative zu Memcache ist ein einfaches HTML-Caching.
Hierbei wird die Ausgabe einmal generiert und dann statisch in einer Cache-Datei auf der Festplatte gespeichert.
Beim nächsten Aufruf der Seite laden wir dann nicht mehr die Inhalte aus der Datenbank, sondern fügen einfach die vorher gecachte Datei ein.

Hierfür habe ich mir eine simple Klasse geschrieben, die es mir auch ermöglicht, noch nachträglich ganz einfach ein HTML-Caching in laufende Systeme einzufügen.

<?php
/******************
*
*    @class CacheMan
*    @author Heiko Ramaker, www.web-skripte.de
*    @version 1.0
*
*******************/


class CacheMan{

    var 
$CacheDir "cache/"// Cache-Verzeichnis
    
var $TimeInCache "600"// Gueltigkeit (in Sekunden)
    
var $FileName;
    var 
$addDelimiter TRUE// Begrenzer vor und nach dem Content
    
var $host "http://deine-domain.de"// URL des Projekts
    
var $uri;
    var 
$ActPage// Aktuell aufgerufene Seite
    
var $FileExt "txt"// Dateiendung der gecachten Datei
    
var $loadFromCache FALSE// Cache deaktivieren?

    
function CacheMan()
    {
        
$this->_setUri();
        
$this->_setActPage();
        
$this->FileName $this->CacheDir.$this->TimeInCache.'_'.md5($this->ActPage).'.'.$this->FileExt;
    }

    
/*
    Caching starten
    */
    
function _startCaching()
    {
        
$this->_checkGuilty();
        
// Es existiert noch eine gueltige Datei im Cache
        
if($this->loadFromCache === TRUE)
        {
            @
readfile($this->FileName);
            return 
TRUE;
        }
        
// keine gueltige Datei vorhanden, neues Caching starten
        
else
        {
            @
ob_start("ob_gzhandler");
            return 
FALSE;
        }
    }

    function 
_nowCaching()
    {
        if(
$this->loadFromCache === FALSE)
        {
            
// Datei zum Schreiben &ouml;ffnen
            
$fp = @fopen($this->FileName'w');
            
// Inhalt in die Datei schreiben
            
$cacheThisText "";
            if(
$this->addDelimiter == TRUE){ $cacheThisText .= "<!-- Start ".$this->TimeInCache." web-skripte.de Delimiter -->";}
            
$cacheThisText .= ereg_replace("(\r\n|\n|\r)"""ob_get_contents());
            if(
$this->addDelimiter == TRUE){ $cacheThisText .= "<!-- End ".$this->TimeInCache." web-skripte.de Delimiter -->";}
            @
fwrite($fp$cacheThisText);
            
// Datei schliessen
            
@fclose($fp);
        }
    }

    
/*
    Pruefen ob die Datei im Cache liegt und gueltig ist
    */
    
function _checkCache()
    {
        if(@
file_exists($this->FileName))
        {
            
$this->_clear();
            return 
TRUE;
        }
        else
        {
            return 
FALSE;
        }
    }

    
/*
    Ausgeben wann die Datei zuletzt aktualisiert wurde (UNIX-Timestamp)
    */
    
function _lastChanged()
    {
        
// Datei existiert im Cache
        
if($this->_checkCache() === TRUE)
        {
            
$timestamp = @filemtime($this->FileName);
            
$this->_clear();
            return 
$timestamp;
        }
        
// Datei existiert nicht im Cache
        
else
        {
            return 
FALSE;
        }
    }

    
/*
    Pruefen, ob die gecachte Datei noch gueltig ist
    */
    
function _checkGuilty()
    {
        
// Datei existiert, also pruefen ob sie noch gueltig ist
        
if($this->_checkCache !== FALSE)
        {
            
// Datei ist noch gueltig
            
if(time() - $this->_lastChanged() < $this->TimeInCache)
            {
                
$this->loadFromCache TRUE;
            }
            
// Datei im Cache ist zu alt
            
else
            {
                
$this->loadFromCache FALSE;
            }
        }
        
// Datei existiert nicht mehr, kann also auch nicht mehr gueltig sein
        
else
        {
            return 
FALSE;
        }
    }

    
/*
    Caching beenden
    */
    
function _endCaching()
    {
        if(
$this->loadFromCache === FALSE)
        {
            
$this->_nowCaching();
            @
ob_end_flush();
        }
    }

    
/*
    Dateiname fuer gecachte Version waehlen
    */
    
function _setFileName($cname)
    {
        
$this->FileName $this->CacheDir.$this->TimeInCache.'_'.md5($this->ActPage.$cname).'.'.$this->FileExt;
    }

    
/*
    Gueltigkeitsdauer setzen in Sekunden
    */
    
function _setGuilty($ctime)
    {
        
$this->TimeInCache $ctime;
    }

    
/*
    Aktuelle Seiten-URL
    */
    
function _setActPage()
    {
        
$this->ActPage $this->host $this->uri;
    }

    
/*
    REQUEST_URI setzen
    */
    
function _setUri()
    {
        
$this->uri $_SERVER['REQUEST_URI'];
    }

    
/*
    Status Cache loeschen
    */
    
function _clear()
    {
        @
clearstatcache();
    }

    function 
_setCacheDir($cdir)
    {
        
$this->CacheDir $cdir;
    }
}
// Neue Klasse initialisieren
// Jederzeit direkt aufrufbar
$cache = new CacheMan();
?>


Der Einbau in die eigene Seite funktioniert dank der Klasse ganz einfach:

Zuerst legen wir das in der Klasse eingetragene Verzeichnis (hier: cache) auf unserem Server an und geben dem Verzeichnis die notwendigen rechte zum Lesen und Schreiben.
In diesem Verzeichnis sollen später unsere gecachten Dateien abgelegt werden.
Die o.a. Klasse hat den großen Vorteil, dass nicht nur komplette Seiten, sondern auch einzelne Teilabschnitte (auch mehrere der gleichen Seite) mit unterschiedlich langer Gültigkeitsdauer in den Cache abgelegt werden können.

<?php 
echo 'Eine Webseite mit viel Text...<br />'
echo 
'Der Text interessiert uns hier aber reichlich wenig.<br />'
echo 
'Wir wollen doch blo&szlig; sehen, wie die Klasse funktioniert<br />'
// Wir haben hier eine gewaaaaaaaaaltig grosse Schleife'; 
for($i 0$i <= 500000$i++) { 
   echo 
'Schleifendurchlauf $i<br />'

?>


Das könnte also unser Code sein, BEVOR wir das Caching nutzen.
Die Schleife läuft 500.000 mal durch und generiert bei jedem Aufruf die Ausgabe neu.
Jetzt ergänzen wir das Ganze um unsere neue Cacheing-Klasse:

<?php 
echo 'Eine Webseite mit viel Text...<br />'
echo 
'Der Text interessiert uns hier aber reichlich wenig.<br />'
echo 
'Wir wollen doch blo&szlig; sehen, wie die Klasse funktioniert<br />'
@require_once(
'pfad_zur_datei/cache.class.php'); // Klasse einfuegen 
$cache->_setGuilty(450); // Optional: Gueltigkeit 
$cache->_setFileName('schleife 1'); // Optional: Eindeutiger Name 
if($cache->_startCaching() === FALSE ) { // ...wenn caching deaktiviert ist 
    // Wir haben hier eine gewaaaaaaaaaltig grosse Schleife'; 
    
for($i 0$i <= 500000$i++) { 
       echo 
'Schleifendurchlauf $i<br />'
    } 
$cache->_endCaching(); 
?>


Das war es schon.
Unsere Klasse prüft nun ganz automatisch, ob eine gültige Datei im Cache vorhanden ist.
Wenn ja, dann wird die gespeicherte Datei eingefügt, wenn nein, dann laufen wir die Schleife in Echtzeit durch und legen dann am Ende des Durchlaufs eine neue Cache-Datei an.

Und wer sich nun von mir davon überzeugen lassen hat, wie einfach das Caching mit dieser Klasse ist, der darf sie natürlich gerne nutzen :-)
Über eine eMail, wo sich meine Klasse befindet würde ich mich natürlich freuen.