Die nachfolgende Klasse ist eine einfach gehaltene Caching-Klasse.
Angeboten werden Methoden zum Schreiben, Lesen und Entfernen von Cache-Einträgen.
Hinweis 1: Veraltete Cache-Einträge werden nur dann automatisch gelöscht, wenn sie per get() abgefragt werden.
Hinweis 2: Der Cache-Basis-Ordner wird von der Methode getCacheDir() zurückgegeben. Standardmäßig ist es der Unterordner "cache/" in dem Verzeichnis in dem die Cacher-Klasse liegt. Eine Anpassung an das eigene Dateisystem kann durch Ändern der Rückgabe der Methode erfolgen.
Tests zur Klasse:
Angeboten werden Methoden zum Schreiben, Lesen und Entfernen von Cache-Einträgen.
- set($name, $content, $lifetime): Fügt einen Cache-Eintrag mit der Bezeichnung $name hinzu, welcher $content enthält und $lifetime Sekunden lang gespeichert bleibt. $lifetime ist optional, der Standardwert beträgt eine Stunde. $content darf jeden Datentyp haben, der sich serialisieren lässt. Enthält $name Slashes ("/"), dann werden automatisch entsprechende Unterordner erstellt, falls diese noch nicht vorhanden sind.
- get($name): Gibt den Cache-Eintrag mit Namen $name zurück oder NULL falls dieser noch nicht vorhanden oder bereits veraltet ist.
- remove($name): Entfernt den Cache-Eintrag mit Namen $name.
Hinweis 1: Veraltete Cache-Einträge werden nur dann automatisch gelöscht, wenn sie per get() abgefragt werden.
Hinweis 2: Der Cache-Basis-Ordner wird von der Methode getCacheDir() zurückgegeben. Standardmäßig ist es der Unterordner "cache/" in dem Verzeichnis in dem die Cacher-Klasse liegt. Eine Anpassung an das eigene Dateisystem kann durch Ändern der Rückgabe der Methode erfolgen.
PHP-Code
<?php namespace Caching; use Exception as Exception; /** * Beispiel zum Speichern eines Ergebnisses für eine Stunde: * use Caching/Cacher as Cacher; * ... * Cacher::getInstance()->set('unterordner/cache', 'irgendein ergebnis', 60*60); * * Beispiel zum Lesen des besagten Ergebnisses: * use Caching/Cacher as Cacher; * ... * $cache = Cacher::getInstance()->get('unterordner/cache'); * if ($cache===null) { * //... cache veraltet / noch nicht erstellt ... * } else { * //ergebnis ist in $cache * } */ class Cacher { /** * Die höchstmögliche Lebenszeit für einen Cache-Eintrag. */ const MAX_LIFETIME = 2592000; // 30 Tage /** * Die Instanz des Cachers (Singleton). */ private static $instance = null; /** * Callback-Funktion zur Ermittlung der aktuellen Zeit. * Die tatsächlich aktuell Zeit lässt sich über time() ermitteln, * soll der Cacher aber getestet werden, dann ist die Möglichkeit zum Ändern der * "aktuellen" Zeit hilfreich. * Ein Standard-Callback wird automatisch vom Konstruktor festgelegt. */ private $timeCallback = null; private function __construct() { $this->timeCallback = function() { return time(); }; } /** * Gibt eine Instanz des Cachers zurück. * @return Caching\Cacher */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Gibt den Pfad zum Cache-Unterverzeichnis zurück. * @return string */ private function getCacheDir() { return __DIR__.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR; } /** * Legt einen Cache-Eintrag anhand des Namens des Eintrags und anhand seines * Inhalts fest. Es kann optional eine maximale Haltbarkeit in Sekunden übergeben * werden (falls nicht wird automatisch eine Stunde verwendet). * Der Name des Cache-Eintrags darf Zeichen aus dem Bereich a-z, A-Z und 0-9 sowie Unterstriche * und Slashes ("/") enthalten. Slashes dürfen nicht direkt aufeinander folgen. Slashes am * Anfang und am Ende werden automatisch weggekürzt. Enthält der Name Slashes, dann werden * entsprechende Unterordner automatisch generiert. * Beispiel: * set('page/article/whatever', 'bla'); * erzeugt: Im Cache-Verzeichnis den Unterordner "page" und darin "article" in welchem die * Datei "whatever.txt" liegt. * Existiert bereits ein gleichnamiger Cache-Eintrag, dann wird dieser automatisch überschrieben. * * @throws Exception Bei ungültigem Cache-Name, Cache-Inhalt oder einem maximalem Alter, das kein Integer ist. * @param string $cacheName Name des Cache-Eintrags * @param mixed $content Inhalt des Cache-Eintrags * @param int $lifetime Maximales Alter des Cache-Eintrags in Sekunden (TTL) * @return void */ public function set($cacheName, $content, $lifetime=3600) { $cacheName = $this->prepareCacheName($cacheName); if ($content===null) { throw new Exception('Ungültiger Inhalt des Cache-Eintrags: NULL darf nicht gespeichert werden,' .' da NULL bereits von get() zurückgegeben wird, wenn kein Cache-Eintrag gefunden wurde.'); } if (!is_int($lifetime)) { throw new Exception('Es wurde kein gültiges maximales Alter für den Cache-Eintrag übergeben.'); } if ($lifetime<=0) { return true; } elseif ($lifetime>self::MAX_LIFETIME) { $lifetime = self::MAX_LIFETIME; } $content = serialize($content); // Unterverzeichnis wird ggf angelegt, falls Cache-Name "/" enthaelt $pos = strrpos($this->getCacheDir().$cacheName, DIRECTORY_SEPARATOR); $dir = substr($this->getCacheDir().$cacheName, 0, $pos) . DIRECTORY_SEPARATOR; if (!file_exists($dir)) { $old = umask(0); mkdir($dir, 0755, true); umask($old); } $filepath = $this->getCacheDir() . $cacheName . '.txt'; $cache = array( 'created'=>$this->getTime(), 'lifetime'=>$lifetime, 'content'=>$content ); $cache = gzcompress( serialize($cache), 3 ); file_put_contents($filepath, $cache); chmod($filepath, 0755); } /** * Gibt den Inhalt des Cache-Eintrags mit dem übergebenen Namen zurück, falls dieser zuvor erzeugt wurde * und noch nicht veraltet ist. Sonst wird null zurückgegeben. * @param string $cacheName * @return mixed Der Inhalt des Cache-Eintrags oder null */ public function get($cacheName) { $cacheName = $this->prepareCacheName($cacheName); $filepath = $this->getCacheDir() . $cacheName . '.txt'; if (!file_exists($filepath)) { return null; } else { $cache = unserialize(gzuncompress(file_get_contents($filepath))); if (!is_array($cache) || !isset($cache['created']) || !isset($cache['lifetime']) || !isset($cache['content'])) { throw new Exception('Unbekannter Aufbau der Cache-Datei. Kann Cache daher nicht verarbeiten.'); } $maxAge = $cache['created'] + $cache['lifetime']; if ($this->getTime() > $maxAge) { $this->remove($cacheName); return null; } else { return unserialize($cache['content']); } } } /** * Entfernt einen Cache-Eintrag mit dem übergebenen Namen, falls dieser existiert. * Gibt true zurück, falls der Eintrag gefunden und gelöscht wurde, sonst false. * @return bool */ public function remove($cacheName) { $cacheName = $this->prepareCacheName($cacheName); $filepath = $this->getCacheDir() . $cacheName . '.txt'; if (file_exists($filepath)) { @unlink($filepath); return true; } return false; } /** * Legt eine Callbackfunktion zur Ermittlung der aktuellen Zeit fest. * Es ist i.d.R. nur zu Testzwecken notwendig, die Callback-Funktion zu ändern. * Eine Standard-Callback-Funktion welche den Wert von time() zurückgibt wird * bereits durch den Konstruktor festgelegt. * Wird NULL übergeben, dann wird wieder der Wert von time() verwendet. * Beispiel: * $cb = function() { return time()+10000; }; * Cacher::getInstance()->setTimeCallback($cb); * @param mixed $cb Callback-Funktion oder NULL (=time()) */ public function setTimeCallback($cb=null) { if ($cb===null) { $this->timeCallback = function() { return time(); }; } elseif (is_callable($cb)) { $this->timeCallback = $cb; } else { throw new Exception('NULL oder Callback erwartet, gegeben '.gettype($cb)); } } /** * Gibt einen UNIX-Zeitstempel zurück (entsprechend des time-callbacks). * @return int */ private function getTime() { $cb = $this->timeCallback; return $cb(); } /** * Prüft, ob der übergebene Cache-Name gültig ist und führt ggf. geringere Anpassungen durch, * um etwaige Fehler zu korrigieren. Die korrigierte Version wird zurückgegeben. * @param string $cacheName Name des Cache-Eintrags * @return string */ private function prepareCacheName($cacheName) { if (!is_string($cacheName)) { throw new Exception('Es wurde kein gültiger Name für den Cache-Eintrag übergeben.'); } $cacheName = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $cacheName); // Nur Zeichen im Bereich a-z, A-Z, 0-9 sowie Unterstriche ("_") und Slashes ("/"), aber nicht mehrere Slashes direkt hintereinander if (preg_replace('/([^a-zA-Z0-9_\/\\]|[\/]{2,})/', '', $cacheName)!==$cacheName) { throw new Exception('Der Name des Cache-Eintrags enthält ungültige Zeichen.'); } // Slashes am Anfang und am Ende entfernen $cacheName = trim($cacheName, DIRECTORY_SEPARATOR); return $cacheName; } } ?>
Tests zur Klasse:
PHP-Code
<?php $cacher = Cacher::getInstance(); $cacher->set('test1', 'abc'); var_dump($cacher->get('test1')); $cacher->remove('test1'); var_dump($cacher->get('test1')); $cacher->set('test/test2', 1234); var_dump($cacher->get('test/test2')); $cacher->set('/test/test_xy/test3', array(1, 2, 3)); var_dump($cacher->get('test/test_xy/test3')); $cacher->set('test4', 'abc', 100); // 100 Sekunden maximales Alter var_dump($cacher->get('test4')); $cacher->setTimeCallback(function() { return time()+99; }); // Zeit-Callback 99 Sekunden in die Zukunft legen var_dump($cacher->get('test4')); // soll "abc" zurückgeben $cacher->setTimeCallback(function() { return time()+101; }); // Zeit-Callback 101 Sekunden in die Zukunft legen var_dump($cacher->get('test4')); // soll NULL zurückgeben $cacher->setTimeCallback(null); try { $cacher->set('test/../../attack', 'abc'); } catch (Exception $e) { echo($e); } try { $cacher->set('test////bla', 'abc'); } catch (Exception $e) { echo($e); } ?>
Ausgabe
string(3) "abc" NULL int(1234) array(3) { [0]=> int(1) [1]=> int(2) [2]=> int(3) } string(3) "abc" string(3) "abc" NULL exception 'Exception' with message 'Der Name des Cache-Eintrags enthält ungültige Zeichen.' in ...dateipfad...:215 Stack trace: #0 ...dateipfad...(88): Caching\Cacher->prepareCacheName('test/../../atta...') #1 ...dateipfad...(248): Caching\Cacher->set('test/../../atta...', 'abc') #2 {main}exception 'Exception' with message 'Der Name des Cache-Eintrags enthält ungültige Zeichen.' in ...dateipfad...:215 Stack trace: #0 ...dateipfad...(88): Caching\Cacher->prepareCacheName('test////bla') #1 ...dateipfad...(254): Caching\Cacher->set('test////bla', 'abc') #2 {main}