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}