Eine einfache Caching-Klasse

Eine kleine Klasse, die das Cachen von Daten per Dateisystem durchführt.

Die nachfolgende Klasse ist eine einfach gehaltene Caching-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}

Deprecated: Directive 'allow_url_include' is deprecated in Unknown on line 0