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}