Wenn eine PHP-Anwendung bei jedem Seitenaufruf Datenbankabfragen ausführt oder aufwendige Berechnungen durchläuft, kostet das Zeit und Serverressourcen. Ein Datei-Cache speichert das Ergebnis solcher Operationen in einer Textdatei und liefert es bei folgenden Aufrufen direkt aus. Erst wenn die Cache-Datei abgelaufen ist, wird das Ergebnis neu berechnet. Dieses Tutorial zeigt Schritt für Schritt, wie ein solcher Cache in PHP funktioniert.
Wann lohnt sich ein Datei-Cache?
Ein Cache bringt immer dann Vorteile, wenn sich die Daten nicht bei jedem Aufruf ändern. Typische Beispiele sind News-Übersichten, Navigationsmengen aus der Datenbank, API-Antworten externer Dienste oder Berechnungen wie Statistiken. Solange sich die Quelldaten nur selten ändern, kann das Ergebnis problemlos für einige Minuten oder Stunden zwischengespeichert werden.
Keinen Sinn ergibt Caching bei Inhalten, die sich pro Benutzer oder pro Sekunde ändern, zum Beispiel bei einem Live-Chat oder einem Warenkorb.
Einfacher Cache mit file_put_contents() und file_get_contents()
Die einfachste Variante prüft, ob eine Cache-Datei existiert und ob sie noch gültig ist. Falls ja, wird ihr Inhalt gelesen. Falls nicht, wird das Ergebnis neu erzeugt und in die Datei geschrieben.
<?php
$cacheFile = __DIR__ . '/cache/news.txt';
$cacheTime = 300; // 5 Minuten in Sekunden
if (file_exists($cacheFile) && filemtime($cacheFile) > time() - $cacheTime) {
/* Cache ist noch gültig,
Inhalt direkt auslesen */
$ausgabe = file_get_contents($cacheFile);
} else {
/* Cache abgelaufen oder nicht vorhanden,
Daten neu erzeugen */
$ausgabe = 'Erstellt am ' . date('d.m.Y H:i:s');
// Ergebnis in die Cache-Datei schreiben
file_put_contents($cacheFile, $ausgabe, LOCK_EX);
}
echo $ausgabe;
?>
Der Parameter LOCK_EX bei file_put_contents() setzt eine exklusive Dateisperre. Dadurch wird verhindert, dass zwei gleichzeitige Zugriffe die Datei beschädigen. Das ist besonders auf Servern mit vielen Besuchern wichtig.
Cache-Funktion zum Wiederverwenden
Wenn mehrere Stellen im Projekt einen Cache benötigen, lohnt sich eine eigene Funktion. Sie kapselt die gesamte Logik und lässt sich mit unterschiedlichen Dateinamen und Laufzeiten aufrufen.
<?php
/**
* Liest den Cache oder erzeugt ihn neu.
*
* @param string $key Eindeutiger Name für die Cache-Datei
* @param int $ttl Gültigkeitsdauer in Sekunden
* @param callable $callback Funktion, die den Inhalt erzeugt
* @return string Gecachter oder frisch erzeugter Inhalt
*/
function cacheGet(
string $key,
int $ttl,
callable $callback
): string {
$dir = __DIR__ . '/cache';
$file = $dir . '/' . $key . '.cache';
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
if (file_exists($file)
&& filemtime($file) > time() - $ttl
) {
return file_get_contents($file);
}
// Inhalt neu erzeugen
$content = $callback();
file_put_contents($file, $content, LOCK_EX);
return $content;
}
?>
Die Funktion erwartet als dritten Parameter einen Callback. Das ist die Funktion, die den eigentlichen Inhalt erzeugt. Sie wird nur aufgerufen, wenn der Cache abgelaufen ist oder noch nicht existiert.
<?php
$news = cacheGet('news_startseite', 600, function () {
/* Hier stehen die Datenbankabfragen
oder sonstigen Berechnungen */
$pdo = new PDO('mysql:host=localhost;dbname=test',
'user', 'pass');
$stmt = $pdo->query(
'SELECT titel, text FROM news
ORDER BY datum DESC LIMIT 10'
);
$html = '';
while ($row = $stmt->fetch()) {
$html .= '<h3>'
. htmlspecialchars($row['titel'])
. '</h3>';
$html .= '<p>'
. htmlspecialchars($row['text'])
. '</p>';
}
return $html;
});
echo $news;
?>
Beim ersten Aufruf werden die News aus der Datenbank geladen und in cache/news_startseite.cache gespeichert. Die nächsten 10 Minuten liefert cacheGet() den gespeicherten Inhalt, ohne die Datenbank zu berühren.
Komplexe Daten cachen mit serialize()
Manchmal soll nicht nur ein fertiger HTML-String, sondern ein Array oder ein Objekt gecacht werden. In diesem Fall hilft serialize(), um die Daten in einen String umzuwandeln. Beim Lesen macht unserialize() daraus wieder die ursprüngliche Datenstruktur.
<?php
$cacheFile = __DIR__ . '/cache/produkte.cache';
$cacheTime = 3600; // 1 Stunde
if (file_exists($cacheFile)
&& filemtime($cacheFile) > time() - $cacheTime
) {
$produkte = unserialize(
file_get_contents($cacheFile)
);
} else {
/* Produkte aus der Datenbank laden */
$pdo = new PDO('mysql:host=localhost;dbname=shop',
'user', 'pass');
$stmt = $pdo->query(
'SELECT id, name, preis FROM produkte
WHERE aktiv = 1'
);
$produkte = $stmt->fetchAll(PDO::FETCH_ASSOC);
file_put_contents(
$cacheFile,
serialize($produkte),
LOCK_EX
);
}
/* $produkte ist jetzt ein Array,
egal ob aus Cache oder Datenbank */
foreach ($produkte as $p) {
echo $p['name'] . ': '
. number_format($p['preis'], 2, ',', '.')
. " €<br>\n";
}
?>
Da unserialize() beliebige PHP-Objekte wiederherstellen kann, sollte die Cache-Datei niemals von außen manipulierbar sein. Wer auf Nummer sicher gehen will, kann stattdessen json_encode() und json_decode() verwenden. JSON ist sicherer, unterstützt allerdings keine PHP-Objekte.
Cache gezielt löschen
Manchmal soll der Cache nicht erst nach Ablauf der Zeit erneuert werden, sondern sofort. Zum Beispiel wenn ein Administrator einen neuen Beitrag veröffentlicht. Dafür reicht es, die Cache-Datei zu löschen.
<?php
/**
* Löscht eine einzelne Cache-Datei.
*/
function cacheDelete(string $key): bool
{
$file = __DIR__ . '/cache/' . $key . '.cache';
if (file_exists($file)) {
return unlink($file);
}
return true;
}
/**
* Löscht alle Cache-Dateien im Verzeichnis.
*/
function cacheClear(): int
{
$dir = __DIR__ . '/cache';
$count = 0;
foreach (glob($dir . '/*.cache') as $file) {
if (unlink($file)) {
$count++;
}
}
return $count;
}
// Einzelnen Cache löschen
cacheDelete('news_startseite');
// Alle Caches leeren
$geloescht = cacheClear();
echo $geloescht . ' Cache-Dateien entfernt';
?>
Sicherheit und Best Practices
Beim Datei-Caching gibt es ein paar Punkte, die oft übersehen werden.
Cache-Verzeichnis schützen
Cache-Dateien sollten nicht über den Browser abrufbar sein. Am besten liegt das Cache-Verzeichnis außerhalb des Document-Root. Falls das nicht möglich ist, schützt eine .htaccess-Datei den Zugriff.
# .htaccess im Cache-Verzeichnis
Deny from all
Dateiberechtigungen
Cache-Dateien brauchen nur Lese- und Schreibrechte für den Webserver-Benutzer. 0644 reicht in den meisten Fällen aus. Berechtigungen wie 0777 sind unnötig und ein Sicherheitsrisiko.
Fehlerbehandlung
Wenn das Cache-Verzeichnis nicht beschreibbar ist, sollte die Anwendung trotzdem funktionieren. In diesem Fall wird einfach ohne Cache gearbeitet.
<?php
$cacheFile = __DIR__ . '/cache/daten.cache';
$content = null;
/* Versuch den Cache zu lesen */
if (file_exists($cacheFile)
&& filemtime($cacheFile) > time() - 300
) {
$content = file_get_contents($cacheFile);
}
if ($content === null || $content === false) {
/* Daten frisch erzeugen */
$content = berechneDaten();
/* Cache schreiben, Fehler ignorieren */
@file_put_contents($cacheFile, $content, LOCK_EX);
}
echo $content;
?>
Das @ vor file_put_contents() ist hier bewusst gesetzt. Wenn das Schreiben fehlschlägt, funktioniert die Seite trotzdem, nur ohne Cache. An allen anderen Stellen sollte @ vermieden werden, weil es Fehler versteckt.
Alternativen zum Datei-Cache
Ein Datei-Cache eignet sich gut für kleinere Projekte und Shared-Hosting-Umgebungen, wo keine zusätzliche Software installiert werden kann. Für größere Anwendungen gibt es schnellere Lösungen.
APCu speichert Daten direkt im Arbeitsspeicher des PHP-Prozesses. Das ist deutlich schneller als Dateizugriffe, aber die Daten gehen beim Neustart von PHP verloren.
Redis und Memcached sind eigenständige Cache-Server, die über das Netzwerk erreichbar sind. Sie eignen sich für Anwendungen mit mehreren Webservern, weil alle Server auf denselben Cache zugreifen können.
OPcache ist in PHP standardmäßig aktiv und cacht den kompilierten Bytecode von PHP-Dateien. Das beschleunigt die Ausführung, ersetzt aber keinen Daten-Cache.
Für die meisten PHP-Projekte ohne spezialisierte Infrastruktur ist ein Datei-Cache ein solider Einstieg, der spürbare Verbesserungen bringt.