xscDevBlog – LastSharp & Co.

Der xscheme-DevelopmentBlog

Eigenes Caching-System erstellen (PHP)

without comments

Ein User, der eine Internetseite besucht, erwartet von dieser, dass sie schnell reagiert. Niemand will lange warten, bis ein PHP-Script alle Datensätze aus einer Datenbank geladen hat oder bis ein anderes den Inhalt benutzerfreundlich, womöglich in Tabellenform darstellt. Nur wie kann man Datenbankzugriffe minimieren? Wie kann man Berechnungen/Verarbeitungen einschränken? Die Antwort liegt nahe: ein Cache.

Grundlage

In diesem Artikel möchte ich zeigen, wie man sich mithilfe von PHP einen eigenen Cache für seine Webseite erstellen kann. Wir brauchen hierfür folgende Elemente:

  • Die Cache-Einstellungen, die uns sagen, welche Seiten zwischengespeichert werden und für wie lange.
  • Ein Verzeichnis für unsere zwischengespeicherten Daten.
  • Eine Funktion, die die aktuelle Seite zwischenspeichert.
  • Eine Funktion, die eine zwischengespeicherte Seite wieder ausgibt.

Am einfachsten wird es sein, den Cache als Klasse zu implementieren. So reduziert sich der Wartungsaufwand bei späteren Veränderungen. Unsere Cache-Klasse benötigt folgende Felder:

  • cachePath: das Verzeichnis, das den Cache beinhaltet
  • cacheData: ein assoziatives Array aus [Dateiname => Speicherdauer in Sekunden]-Paaren
  • doCache: aktuelle Seite cachen?

Implementierung

Wir beginnen mit den Feldern:

class Cache {
    // Felder
    var $cachePath;
    var $cacheData = array();
    var $doCache = false;
    // ...
}

Weiter geht es mit den Methoden für die Cache-Einstellungen: setCacheTime(dateiname, speicherdauer) und getCacheTime(dateiname). Diese beiden Methoden schreiben bzw. lesen die jeweilige Speicherdauer einer Datei in das/aus dem Feld $cacheData:

function setCacheTime($dateiname, $speicherdauer) {
    $this->cacheData[$dateiname] = $speicherdauer;
}

function getCacheTime($dateiname) {
    return $this->cacheData[$dateiname];
}

Gibt getCacheTime() den Wert 0 zurück, wird eine Seite nicht gecached.

Nun widmen wir uns aber dem Speichern einer Datei. Zuallererst muss ein eindeutiger Dateiname erstellt werden, was am einfachsten mithilfe des MD5-Algorithmus geht. Was für die Erstellung zu berücksichtigen ist, variiert je nach Website. Für unser Beispiel sei nur der Dateiname der aktuell angezeigten Datei und die übergebenen (GET-)Parameter ($_SERVER["QUERY_STRING"]) einbezogen:

function getCacheFileName($dateiname) {
    return md5($dateiname."?".$_SERVER["QUERY_STRING"]).".tmp";
}

function cacheFileExists($dateiname) { // überprüft die Existenz der Cache-Datei
    return file_exists(dirname($this->cachePath)."/".$this->getCacheFileName($dateiname));
}

Nachdem wir den Dateinamen haben, müssen wir noch festlegen, wie die zu cachenden Daten gespeichert werden. Am sinnvollsten ist es hier, den Inhalt der Datei mithilfe zweier PHP-Zeilen nur dann auszugeben, wenn die Datei noch gültig ist. Ansonsten wird “<!–expired–>” ausgegeben:

<?php $cache_time=<Speicherzeit>; if (time()-$cache_time < <Speicherdauer>) { ?>
<Dateiinhalt>
<?php } else { echo "<!--expired-->"; } ?>

Dies realisieren wir mit folgender Funktion:

function saveCacheFile($dateiname, $daten) {
    // Datei zum Schreiben öffnen
    $fp = fopen(dirname($this->cachePath)."/".$this->getCacheFileName($dateiname), "w");
    // Daten schreiben
    $str =
      "<?"."php \$cache_time = ".time()."; ".
      "if ((time()-\$cache_time)<".$this->getCacheTime($dateiname).") { ?".">\n".
      $daten."\n".
      "<?"."php } else echo '<!--expired-->'; ?".">";
    fputs($fp, $str, strlen($str));
    // Datei schließen
    fclose($fp);
}

Jetzt geht es an das eigentliche Problem bei unserer Aufgabe: Wie komme ich an den Inhalt einer Datei? Wenn ich ihn über fopen() und ähnliches einlese, wird der PHP-Code nicht ausgeführt, und wenn ich die Datei über include() einbinde, habe ich keinerlei Zugriff auf die ausgegebenen Daten. Oder doch?

PHP bietet hier praktischerweise die Funktionen des Output-Buffers: ob_start(), ob_get_contents() und ob_end_flush() und ob_end_clean(). Die erste Funktion aktiviert den Buffer, die zweite liefert seinen Inhalt als String, die dritte gibt den Inhalt aus und die vierte löscht den Buffer ohne Ausgabe. Und genau diese Funktionen benötigen wir.

Wir implementieren hierbei eine einzige Methode: includeCache(originaldatei). Sie überprüft, ob eine Seite gecached werden soll. Wenn ja, wird gechecked, ob sie bereits im Cache liegt und noch gültig ist. Wenn auch das zutrifft wird sie ausgegeben, andernfalls wird der Originalcode ausgeführt. Letztlich wird eine evtl zu cachende Seite gespeichert:

function includeCache($originaldatei) {
    // Cachen?
    if ($this->getCacheTime($originaldatei)) {
        ob_start();
        // Datei existiert: auf Gültigkeit prüfen
        if ($this->cacheFileExists($originaldatei)) {
            include(dirname($this->cachePath)."/".$this->getCacheFileName($originaldatei));
            // Überprüfung der Gültigkeit
            $ostr = ob_get_contents();
            if (strpos($ostr, "<!--expired-->")) {
                // ungültig: Buffer löschen, Originaldatei buffern, Cachen
                $this->doCache = true;
                ob_end_clean();
                include($originaldatei);
            } else {
                // gültig: Buffer ausgeben, nicht Cachen
                $this->doCache = false;
                ob_end_flush();
            }
        }
        // Datei existiert nicht: Originaldatei buffern, Cachen aktivieren
        else {
            $this->doCache = true;
            include($originaldatei);
        }
    }
    // Nicht Cachen: normale Ausgabe, nicht cachen
    else {
        $this->doCache = false;
        include($originaldatei);
    }

    // Wenn Ausgabe gecached werden soll: Cache schreiben, gebufferte Daten ausgeben
    if ($this->doCache) {
        $daten = ob_get_contents();
        if ($daten) {
            $this->saveCacheFile($originaldatei, $daten);
            ob_end_flush();
        }
    }
}

Zuguterletzt erstellen wir noch einen Konstruktor, der den Speicherpfad festlegt (das abschließende “/.” ist wichtig, da dirname($this->cachePath) sonst das übergeordnete Verzeichnis liefert.):

function Cache($cp) {
    $this->cachePath=$cp."/.";
}

Um nun eine Datei zu cachen müssen Sie also einfach nur ein Cache-Objekt erstellen und die Datei nicht über das normale include einbinden, sondern über die includeCache-Methode des Objektes, z.B.:

$c = new Cache(dirname(__FILE__)."/tmp/");
$c->setCacheTime("test.php", 15*60);
$c->includeCache("test.php");

“test.php” (z.B. eine datenbankintensive Operation) wird also 15 Minuten lang zwischengespeichert und danach beim nächsten Aufruf der Datei aktualisiert.

Der vollständige Code (mit Formatierung):

class Cache {
		// =====================================================================
		// Felder
		// =====================================================================
		var $cachePath;
		var $cacheData = array();
		var $doCache = false;
		// =====================================================================
		// Methoden
		// =====================================================================
		// Cache-Zeitspanne festlegen & auslesen
		function setCacheTime($dateiname, $speicherdauer) {
			$this->cacheData[$dateiname] = $speicherdauer;
		}

		function getCacheTime($dateiname) {
			return $this->cacheData[$dateiname];
		}
		// =====================================================================
		// Dateiname erstellen und testen, ob ein Cache-File existiert
		function getCacheFileName($dateiname) {
			return md5($dateiname."?".$_SERVER["QUERY_STRING"]).".tmp";
		}

		function cacheFileExists($dateiname) { // überprüft die Existenz der Cache-Datei
			return file_exists(dirname($this->cachePath)."/".$this->getCacheFileName($dateiname));
		}
		// =====================================================================
		// Cache-File speichern
		function saveCacheFile($dateiname, $daten) {
			// Datei zum Schreiben öffnen
			$fp = fopen(dirname($this->cachePath)."/".$this->getCacheFileName($dateiname), "w");
			// Daten schreiben
			$str =
			  "<?"."php \$cache_time = ".time()."; ".
			  "if ((time()-\$cache_time)<".$this->getCacheTime($dateiname).") { ?".">\n".
			  $daten."\n".
			  "<"."?php } else echo ''; ?".">";
			fputs($fp, $str, strlen($str));
			// Datei schließen
			fclose($fp);
		}
		// =====================================================================
		// Datei gecached einbinden
		function includeCache($originaldatei) {
			// Cachen?
			if ($this->getCacheTime($originaldatei)) {
				ob_start();
				// Datei existiert: auf Gültigkeit prüfen
				if ($this->cacheFileExists($originaldatei)) {
					include(dirname($this->cachePath)."/".$this->getCacheFileName($originaldatei));
					// Überprüfung der Gültigkeit
					$ostr = ob_get_contents();
					if (strpos($ostr, "")) {
						// ungültig: Buffer löschen, Originaldatei buffern, Cachen
						$this->doCache = true;
						ob_end_clean();
						include($originaldatei);
					} else {
						// gültig: Buffer ausgeben, nicht Cachen
						$this->doCache = false;
						ob_end_flush();
					}
				}
				// Datei existiert nicht: Originaldatei buffern, Cachen aktivieren
				else {
					$this->doCache = true;
					include($originaldatei);
				}
			}
			// Nicht Cachen: normale Ausgabe, nicht cachen
			else {
				$this->doCache = false;
				include($originaldatei);
			}

			// Wenn Ausgabe gecached werden soll: Cache schreiben, gebufferte Daten ausgeben
			if ($this->doCache) {
				$daten = ob_get_contents();
				if ($daten) {
					$this->saveCacheFile($originaldatei, $daten);
					ob_end_flush();
				}
			}
		}
		// =====================================================================
		// Konstruktor
		// =====================================================================
		function Cache($cp) {
		   $this->cachePath=$cp."/.";
		}
	}

Written by xsc

August 19th, 2008 at 5:31 pm

Leave a Reply