Eine einfache Caching-Klasse (Thema: PHP Beispiele)

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}
Um unsere Webseite für Sie optimal zu gestalten und fortlaufend verbessern zu können, verwenden wir Cookies. Durch die weitere Nutzung der Webseite stimmen Sie der Verwendung von Cookies zu. Weitere Informationen zu Cookies erhalten Sie in unserer Datenschutzerklärung. OK