chore: complete update
This commit is contained in:
18
src/Framework/Cache/Cache.php
Normal file
18
src/Framework/Cache/Cache.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface Cache
|
||||
{
|
||||
public function get(string $key): CacheItem;
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool;
|
||||
public function has(string $key): bool;
|
||||
public function forget(string $key): bool;
|
||||
public function clear(): bool;
|
||||
|
||||
/**
|
||||
* Führt Callback aus, wenn Wert nicht im Cache ist ("Remember"-Pattern)
|
||||
* und cached das Ergebnis für die gewünschte Zeit
|
||||
*/
|
||||
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem;
|
||||
}
|
||||
32
src/Framework/Cache/CacheDecorator.php
Normal file
32
src/Framework/Cache/CacheDecorator.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
|
||||
final readonly class CacheDecorator
|
||||
{
|
||||
public function __construct(
|
||||
private object $service,
|
||||
private Cache $cache
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function __call(string $name, array $args)
|
||||
{
|
||||
$method = new ReflectionMethod($this->service, $name);
|
||||
$attrs = $method->getAttributes(Cacheable::class);
|
||||
|
||||
if ($attrs) {
|
||||
$attr = $attrs[0]->newInstance();
|
||||
$key = $attr->key ?? $method->getName() . ':' . md5(serialize($args));
|
||||
$ttl = $attr->ttl ?? 3600;
|
||||
return $this->cache->remember($key, fn() => $method->invokeArgs($this->service, $args), $ttl);
|
||||
}
|
||||
|
||||
return $method->invokeArgs($this->service, $args);
|
||||
}
|
||||
}
|
||||
12
src/Framework/Cache/CacheDriver.php
Normal file
12
src/Framework/Cache/CacheDriver.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface CacheDriver
|
||||
{
|
||||
public function get(string $key): CacheItem;
|
||||
public function set(string $key, string $value, ?int $ttl = null): bool;
|
||||
public function has(string $key): bool;
|
||||
public function forget(string $key): bool;
|
||||
public function clear(): bool;
|
||||
}
|
||||
70
src/Framework/Cache/CacheInitializer.php
Normal file
70
src/Framework/Cache/CacheInitializer.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
use App\Framework\Cache\Compression\GzipCompression;
|
||||
use App\Framework\Cache\Compression\NullCompression;
|
||||
use App\Framework\Cache\Driver\ApcuCache;
|
||||
use App\Framework\Cache\Driver\FileCache;
|
||||
use App\Framework\Cache\Driver\InMemoryCache;
|
||||
use App\Framework\Cache\Driver\NullCache;
|
||||
use App\Framework\Cache\Driver\RedisCache;
|
||||
use App\Framework\Cache\Serializer\JsonSerializer;
|
||||
use App\Framework\Cache\Serializer\PhpSerializer;
|
||||
use App\Framework\DI\Initializer;
|
||||
|
||||
final readonly class CacheInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private string $redisHost = 'redis',
|
||||
private int $redisPort = 6379,
|
||||
private int $compressionLevel = -1,
|
||||
private int $minCompressionLength = 1024
|
||||
) {}
|
||||
|
||||
#[Initializer]
|
||||
public function __invoke(): Cache
|
||||
{
|
||||
$this->clear();
|
||||
|
||||
$serializer = new PhpSerializer();
|
||||
$serializer = new JsonSerializer();
|
||||
$compression = new GzipCompression($this->compressionLevel, $this->minCompressionLength);
|
||||
|
||||
|
||||
// L1 Cache:
|
||||
if(function_exists('apcu_clear_cache')) {
|
||||
$apcuCache = new GeneralCache(new APCuCache);
|
||||
}else {
|
||||
$apcuCache = new GeneralCache(new InMemoryCache);
|
||||
}
|
||||
|
||||
$compressedApcuCache = new CompressionCacheDecorator(
|
||||
$apcuCache,
|
||||
$compression,
|
||||
$serializer
|
||||
);
|
||||
|
||||
// L2 Cache:
|
||||
$redisCache = new GeneralCache(new RedisCache(host: $this->redisHost, port: $this->redisPort));
|
||||
$compressedRedisCache = new CompressionCacheDecorator(
|
||||
$redisCache,
|
||||
$compression,
|
||||
$serializer
|
||||
);
|
||||
|
||||
$multiLevelCache = new MultiLevelCache($compressedApcuCache, $compressedRedisCache);
|
||||
|
||||
#return $multiLevelCache;
|
||||
|
||||
return new LoggingCacheDecorator($multiLevelCache);
|
||||
|
||||
|
||||
#return new GeneralCache(new NullCache());
|
||||
}
|
||||
|
||||
private function clear(): void
|
||||
{
|
||||
apcu_clear_cache();
|
||||
}
|
||||
}
|
||||
21
src/Framework/Cache/CacheItem.php
Normal file
21
src/Framework/Cache/CacheItem.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class CacheItem
|
||||
{
|
||||
private function __construct(
|
||||
public string $key,
|
||||
public mixed $value,
|
||||
public bool $isHit,
|
||||
) {}
|
||||
|
||||
public static function miss(string $key): self
|
||||
{
|
||||
return new self($key, null, false);
|
||||
}
|
||||
|
||||
public static function hit(string $key, mixed $value): self
|
||||
{
|
||||
return new self($key, $value, true);
|
||||
}
|
||||
}
|
||||
12
src/Framework/Cache/CachePrefix.php
Normal file
12
src/Framework/Cache/CachePrefix.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
enum CachePrefix: string
|
||||
{
|
||||
case GENERAL = 'cache:';
|
||||
|
||||
case QUERY = 'query_cache:';
|
||||
|
||||
#case SESSION = 'session:';
|
||||
}
|
||||
15
src/Framework/Cache/Cacheable.php
Normal file
15
src/Framework/Cache/Cacheable.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class Cacheable
|
||||
{
|
||||
/**
|
||||
* @param string|null $key Template z.B.: "user:profile:{id}"
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $key = null,
|
||||
public int $ttl = 3600,
|
||||
) {}
|
||||
}
|
||||
23
src/Framework/Cache/Commands/ClearCache.php
Normal file
23
src/Framework/Cache/Commands/ClearCache.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache\Commands;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
|
||||
final readonly class ClearCache
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $cache,
|
||||
) {}
|
||||
|
||||
#[ConsoleCommand("cache:clear", "Clears the cache")]
|
||||
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
|
||||
{
|
||||
$this->cache->clear();
|
||||
$output->writeSuccess("Cache cleared");
|
||||
}
|
||||
}
|
||||
46
src/Framework/Cache/Compression/GzipCompression.php
Normal file
46
src/Framework/Cache/Compression/GzipCompression.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Compression;
|
||||
|
||||
use App\Framework\Cache\CompressionAlgorithm;
|
||||
|
||||
final class GzipCompression implements CompressionAlgorithm
|
||||
{
|
||||
private const string PREFIX = 'gz:';
|
||||
private int $level;
|
||||
private int $threshold;
|
||||
|
||||
public function __construct(int $compressionLevel = -1, int $minLengthToCompress = 1024)
|
||||
{
|
||||
$this->level = $compressionLevel;
|
||||
$this->threshold = $minLengthToCompress;
|
||||
}
|
||||
|
||||
public function compress(string $value, bool $forceCompression = false): string
|
||||
{
|
||||
if (!$forceCompression && strlen($value) < $this->threshold) {
|
||||
return $value;
|
||||
}
|
||||
$compressed = gzcompress($value, $this->level);
|
||||
if ($compressed === false) {
|
||||
// Fallback auf Originalwert bei Fehler
|
||||
return $value;
|
||||
}
|
||||
return self::PREFIX . $compressed;
|
||||
}
|
||||
|
||||
public function decompress(string $value): string
|
||||
{
|
||||
if (!$this->isCompressed($value)) {
|
||||
return $value;
|
||||
}
|
||||
$raw = substr($value, strlen(self::PREFIX));
|
||||
$decompressed = @gzuncompress($raw);
|
||||
return $decompressed !== false ? $decompressed : $value;
|
||||
}
|
||||
|
||||
public function isCompressed(string $value): bool
|
||||
{
|
||||
return strncmp($value, self::PREFIX, strlen(self::PREFIX)) === 0;
|
||||
}
|
||||
}
|
||||
23
src/Framework/Cache/Compression/NullCompression.php
Normal file
23
src/Framework/Cache/Compression/NullCompression.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Compression;
|
||||
|
||||
use App\Framework\Cache\CompressionAlgorithm;
|
||||
|
||||
final class NullCompression implements CompressionAlgorithm
|
||||
{
|
||||
public function compress(string $value, bool $forceCompression = false): string
|
||||
{
|
||||
return $value; // Keine Komprimierung
|
||||
}
|
||||
|
||||
public function decompress(string $value): string
|
||||
{
|
||||
return $value; // Keine Dekomprimierung
|
||||
}
|
||||
|
||||
public function isCompressed(string $value): bool
|
||||
{
|
||||
return false; // Nie komprimiert
|
||||
}
|
||||
}
|
||||
22
src/Framework/Cache/CompressionAlgorithm.php
Normal file
22
src/Framework/Cache/CompressionAlgorithm.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface CompressionAlgorithm
|
||||
{
|
||||
/**
|
||||
* Komprimiert einen Wert. Rückgabe kann String oder Binärdaten sein.
|
||||
* Gibt bei $forceCompression ein Flag an, ob immer komprimiert werden soll.
|
||||
*/
|
||||
public function compress(string $value, bool $forceCompression = false): string;
|
||||
|
||||
/**
|
||||
* Dekodiert einen Wert, falls komprimiert; ansonsten unverändert zurückgeben.
|
||||
*/
|
||||
public function decompress(string $value): string;
|
||||
|
||||
/**
|
||||
* Gibt true zurück, falls $value komprimiert ist (z.B. am Prefix erkennbar)
|
||||
*/
|
||||
public function isCompressed(string $value): bool;
|
||||
}
|
||||
168
src/Framework/Cache/CompressionCacheDecorator.php
Normal file
168
src/Framework/Cache/CompressionCacheDecorator.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class CompressionCacheDecorator implements Cache
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $innerCache,
|
||||
private CompressionAlgorithm $algorithm,
|
||||
private Serializer $serializer
|
||||
) {}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$item = $this->innerCache->get($key);
|
||||
if (!$item->isHit || !is_string($item->value)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
$value = $item->value;
|
||||
|
||||
if ($this->algorithm->isCompressed($value)) {
|
||||
$value = $this->algorithm->decompress($value);
|
||||
}
|
||||
|
||||
try {
|
||||
// Versuche direkt zu deserialisieren.
|
||||
// Schlägt dies fehl, wird der rohe (aber dekomprimierte) Wert zurückgegeben.
|
||||
$unserialized = $this->serializer->unserialize($value);
|
||||
return CacheItem::hit($key, $unserialized);
|
||||
} catch (\Throwable $e) {
|
||||
// Das ist ein erwartetes Verhalten, wenn der Wert ein einfacher String war.
|
||||
// Optional: Loggen des Fehlers für Debugging-Zwecke.
|
||||
// error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}. Assuming raw value.");
|
||||
return CacheItem::hit($key, $value);
|
||||
}
|
||||
|
||||
//LEGACY:
|
||||
|
||||
/*if ($this->algorithm->isCompressed($item->value)) {
|
||||
$decompressed = $this->algorithm->decompress($item->value);
|
||||
|
||||
// Prüfe ob der Inhalt serialisiert wurde
|
||||
if ($this->isSerialized($decompressed)) {
|
||||
try {
|
||||
$unserialized = $this->serializer->unserialize($decompressed);
|
||||
return CacheItem::hit($key, $unserialized);
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback bei Deserialisierung-Fehler
|
||||
error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}, Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn nicht serialisiert oder Deserialisierung fehlgeschlagen, gib dekomprimierten String zurück
|
||||
return CacheItem::hit($key, $decompressed);
|
||||
}
|
||||
|
||||
if($this->isSerialized($item->value)) {
|
||||
try {
|
||||
$unserialized = $this->serializer->unserialize($item->value);
|
||||
return CacheItem::hit($key, $unserialized);
|
||||
} catch (\Throwable $e) {
|
||||
error_log("CompressionCacheDecorator: Deserialization failed for key: {$key}, Error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $item;*/
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
$value = $this->serializer->serialize($value);
|
||||
}
|
||||
$compressed = $this->algorithm->compress($value);
|
||||
return $this->innerCache->set($key, $compressed, $ttl);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return $this->innerCache->has($key);
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
return $this->innerCache->forget($key);
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
return $this->innerCache->clear();
|
||||
}
|
||||
|
||||
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
|
||||
{
|
||||
$item = $this->get($key);
|
||||
if ($item->isHit) {
|
||||
return $item;
|
||||
}
|
||||
$value = $callback();
|
||||
$this->set($key, $value, $ttl);
|
||||
return CacheItem::hit($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob ein String serialisierte PHP-Daten enthält
|
||||
*/
|
||||
/*
|
||||
private function isSerialized(string $data): bool
|
||||
{
|
||||
// Leere Strings oder sehr kurze Strings können nicht serialisiert sein
|
||||
if (strlen($data) < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe auf NULL-Wert (serialisiert als 'N;')
|
||||
if ($data === 'N;') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf boolean false (serialisiert als 'b:0;')
|
||||
if ($data === 'b:0;') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf boolean true (serialisiert als 'b:1;')
|
||||
if ($data === 'b:1;') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf typische serialize() Patterns:
|
||||
// a:N: (array mit N Elementen)
|
||||
// s:N: (string mit N Zeichen)
|
||||
// i:N; (integer mit Wert N)
|
||||
// d:N; (double/float mit Wert N)
|
||||
// O:N: (object mit Klassennamen der Länge N)
|
||||
// b:N; (boolean mit Wert N)
|
||||
// r:N; (reference)
|
||||
// R:N; (reference)
|
||||
if (preg_match('/^[asdObRr]:[0-9]+[:|;]/', $data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf integer Pattern: i:Zahl;
|
||||
if (preg_match('/^i:-?[0-9]+;/', $data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe auf float Pattern: d:Zahl;
|
||||
if (preg_match('/^d:-?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?;/', $data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Zusätzliche Validierung: Versuche tatsächlich zu deserialisieren
|
||||
// aber nur bei verdächtigen Patterns, um Performance zu schonen
|
||||
if (strlen($data) < 1000 && (
|
||||
str_starts_with($data, 'a:') ||
|
||||
str_starts_with($data, 'O:') ||
|
||||
str_starts_with($data, 's:')
|
||||
)) {
|
||||
$test = @unserialize($data);
|
||||
return $test !== false || $data === 'b:0;';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
}
|
||||
57
src/Framework/Cache/Driver/ApcuCache.php
Normal file
57
src/Framework/Cache/Driver/ApcuCache.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Driver;
|
||||
|
||||
use App\Framework\Cache\CacheDriver;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
|
||||
final readonly class ApcuCache implements CacheDriver
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private string $prefix = 'cache:'
|
||||
){}
|
||||
|
||||
private function prefixKey(string $key): string
|
||||
{
|
||||
return $this->prefix . $key;
|
||||
}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
|
||||
$success = false;
|
||||
$value = apcu_fetch($key, $success);
|
||||
if (!$success) {
|
||||
return CacheItem::miss($key);
|
||||
}
|
||||
|
||||
return CacheItem::hit($key, $value);
|
||||
}
|
||||
|
||||
public function set(string $key, string $value, ?int $ttl = null): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
$ttl = $ttl ?? 0;
|
||||
|
||||
return apcu_store($key, $value, $ttl);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
return apcu_exists($key);
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
return apcu_delete($key);
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
return apcu_clear_cache();
|
||||
}
|
||||
}
|
||||
154
src/Framework/Cache/Driver/FileCache.php
Normal file
154
src/Framework/Cache/Driver/FileCache.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Driver;
|
||||
|
||||
use App\Framework\Cache\CacheDriver;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Filesystem\FileStorage;
|
||||
use App\Framework\Filesystem\Storage;
|
||||
|
||||
final readonly class FileCache implements CacheDriver
|
||||
{
|
||||
private const string CACHE_PATH = __DIR__ . '/../storage/cache';
|
||||
|
||||
public function __construct(
|
||||
private Storage $fileSystem = new FileStorage(),
|
||||
) {
|
||||
$this->fileSystem->createDirectory(self::CACHE_PATH);
|
||||
}
|
||||
|
||||
private function getFileName(string $key, ?int $expiresAt): string
|
||||
{
|
||||
// Schütze vor Pfad/komischen Zeichen und Hash den Key
|
||||
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $key);
|
||||
|
||||
$hash = md5($safeKey);
|
||||
|
||||
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash .'_'. ($expiresAt ?? 0) . '.cache.php';
|
||||
}
|
||||
|
||||
private function getFilesForKey(string $key): array
|
||||
{
|
||||
$hash = md5($key);
|
||||
|
||||
$pattern = self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '*.cache.php';
|
||||
|
||||
return glob($pattern) ?: [];
|
||||
}
|
||||
|
||||
private function getLockFileName(string $key): string
|
||||
{
|
||||
$hash = md5($key);
|
||||
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '.lock';
|
||||
}
|
||||
|
||||
private function withKeyLock(string $key, callable $callback)
|
||||
{
|
||||
$lockFile = fopen($this->getLockFileName($key), 'c');
|
||||
if ($lockFile === false) {
|
||||
// Optional: Fehler werfen oder Logging
|
||||
return $callback(null);
|
||||
}
|
||||
|
||||
try {
|
||||
// Exklusiven Lock setzen, Wartezeit begrenzen wenn gewünscht
|
||||
if (flock($lockFile, LOCK_EX)) {
|
||||
return $callback($lockFile);
|
||||
}
|
||||
// Lock konnte nicht gesetzt werden
|
||||
return $callback(null);
|
||||
} finally {
|
||||
flock($lockFile, LOCK_UN);
|
||||
fclose($lockFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$bestFile = null;
|
||||
$bestExpires = null;
|
||||
|
||||
foreach($this->getFilesForKey($key) as $file) {
|
||||
if (!preg_match('/_(\d+)\.cache\.php$/', $file, $m)) {
|
||||
continue;
|
||||
}
|
||||
$expiresAt = (int)$m[1];
|
||||
if ($expiresAt > 0 && $expiresAt < time()) {
|
||||
$this->fileSystem->delete($file);
|
||||
continue;
|
||||
}
|
||||
if ($bestFile === null || $expiresAt > $bestExpires) {
|
||||
$bestFile = $file;
|
||||
$bestExpires = $expiresAt;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestFile === null) {
|
||||
return CacheItem::miss($key);
|
||||
}
|
||||
|
||||
$content = $this->fileSystem->get($bestFile);
|
||||
|
||||
$data = @unserialize($content) ?: [];
|
||||
|
||||
|
||||
if (!isset($data['value'])) {
|
||||
$this->fileSystem->delete($bestFile);
|
||||
return CacheItem::miss($key);
|
||||
}
|
||||
|
||||
return CacheItem::hit($key, $data['value']);
|
||||
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool
|
||||
{
|
||||
return $this->withKeyLock($key, function () use ($key, $value, $ttl) {
|
||||
|
||||
$expiresAt = $ttl ? (time() + $ttl) : null;
|
||||
|
||||
foreach ($this->getFilesForKey($key) as $file) {
|
||||
$this->fileSystem->delete($file);
|
||||
}
|
||||
|
||||
$file = $this->getFileName($key, $expiresAt);
|
||||
|
||||
$data = [
|
||||
'value' => $value,
|
||||
'expires_at' => $expiresAt,
|
||||
];
|
||||
|
||||
$this->fileSystem->put($file, serialize($data));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$item = $this->get($key);
|
||||
return $item->isHit;
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
return $this->withKeyLock($key, function () use ($key) {
|
||||
|
||||
foreach ($this->getFilesForKey($key) as $file) {
|
||||
$this->fileSystem->delete($file);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
$files = glob(self::CACHE_PATH . '*.cache.php') ?: [];
|
||||
foreach ($files as $file) {
|
||||
$this->fileSystem->delete($file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
39
src/Framework/Cache/Driver/InMemoryCache.php
Normal file
39
src/Framework/Cache/Driver/InMemoryCache.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Driver;
|
||||
|
||||
use App\Framework\Cache\CacheDriver;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
|
||||
final class InMemoryCache implements CacheDriver
|
||||
{
|
||||
private array $data = [];
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
return $this->data[$key] ?? CacheItem::miss($key);
|
||||
}
|
||||
|
||||
public function set(string $key, string $value, ?int $ttl = null): bool
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
unset($this->data[$key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
unset($this->data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
35
src/Framework/Cache/Driver/NullCache.php
Normal file
35
src/Framework/Cache/Driver/NullCache.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Driver;
|
||||
|
||||
use App\Framework\Cache\CacheDriver;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
|
||||
class NullCache implements CacheDriver
|
||||
{
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
return CacheItem::miss($key);
|
||||
}
|
||||
|
||||
public function set(string $key, string $value, ?int $ttl = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
134
src/Framework/Cache/Driver/RedisCache.php
Normal file
134
src/Framework/Cache/Driver/RedisCache.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Driver;
|
||||
|
||||
use App\Framework\Cache\CacheDriver;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CachePrefix;
|
||||
use App\Framework\Cache\Serializer;
|
||||
use Predis\Client as Redis;
|
||||
|
||||
final readonly class RedisCache implements CacheDriver
|
||||
{
|
||||
private Redis $redis;
|
||||
|
||||
public function __construct(
|
||||
string $host = '127.0.0.1',
|
||||
int $port = 6379,
|
||||
?string $password = null,
|
||||
int $db = 0,
|
||||
#private Serializer $serializer = new Serializer\PhpSerializer(),
|
||||
private string $prefix = 'cache:'
|
||||
)
|
||||
{
|
||||
$this->redis = new Redis(
|
||||
parameters: [
|
||||
'scheme' => 'tcp',
|
||||
'timeout' => 1.0,
|
||||
'read_write_timeout' => 1.0,
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
]
|
||||
);
|
||||
|
||||
#$this->redis->connect();
|
||||
|
||||
if ($password) {
|
||||
$this->redis->auth($password);
|
||||
}
|
||||
$this->redis->select($db);
|
||||
}
|
||||
|
||||
private function prefixKey(string $key): string
|
||||
{
|
||||
return $this->prefix . $key;
|
||||
}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
|
||||
$data = $this->redis->get($key);
|
||||
if ($data === null) {
|
||||
return CacheItem::miss($key);
|
||||
}
|
||||
#$decoded = $this->serializer->unserialize($data); // oder json_decode($data, true)
|
||||
$decoded = $data;
|
||||
/*if (!is_array($decoded) || !array_key_exists('value', $decoded)) {
|
||||
return CacheItem::miss($key);
|
||||
}*/
|
||||
|
||||
// TODO: REMOVE TTL
|
||||
$ttl = $this->redis->ttl($key);
|
||||
$expiresAt = $ttl > 0 ? (time() + $ttl) : null;
|
||||
return CacheItem::hit(
|
||||
key : $key,
|
||||
value: $decoded,
|
||||
);
|
||||
}
|
||||
|
||||
public function set(string $key, string $value, ?int $ttl = null): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
|
||||
#$payload = $this->serializer->serialize($value); #war: ['value' => $value]
|
||||
|
||||
$payload = $value;
|
||||
|
||||
if ($ttl !== null) {
|
||||
return $this->redis->setex($key, $ttl, $payload)->getPayload() === 'OK';
|
||||
}
|
||||
return $this->redis->set($key, $payload)->getPayload() === 'OK';
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
|
||||
return $this->redis->exists($key) > 0;
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
$key = $this->prefixKey($key);
|
||||
|
||||
return $this->redis->del($key) > 0;
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
try {
|
||||
$patterns = array_map(
|
||||
fn($prefix) => $prefix->value . '*',
|
||||
CachePrefix::cases()
|
||||
);
|
||||
|
||||
foreach($patterns as $pattern) {
|
||||
$this->clearByPattern($pattern);
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function clearByPattern(string $pattern): void
|
||||
{
|
||||
$cursor = 0;
|
||||
$batchSize = 1000; // Batch-Größe für bessere Performance
|
||||
|
||||
do {
|
||||
$result = $this->redis->scan($cursor, [
|
||||
'MATCH' => $pattern,
|
||||
'COUNT' => $batchSize
|
||||
]);
|
||||
|
||||
$cursor = $result[0];
|
||||
$keys = $result[1];
|
||||
|
||||
if (!empty($keys)) {
|
||||
$this->redis->del($keys);
|
||||
}
|
||||
} while ($cursor !== 0);
|
||||
}
|
||||
}
|
||||
38
src/Framework/Cache/FileCacheCleaner.php
Normal file
38
src/Framework/Cache/FileCacheCleaner.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
use App\Framework\Filesystem\FileStorage;
|
||||
use App\Framework\Filesystem\Storage;
|
||||
|
||||
final readonly class FileCacheCleaner
|
||||
{
|
||||
public function __construct(
|
||||
private Storage $fileSystem = new FileStorage(),
|
||||
private string $cacheFolder = __DIR__.'/storage/cache/'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Löscht alle abgelaufenen Cache-Dateien im Cache-Verzeichnis.
|
||||
* Rückgabe: Anzahl gelöschter Dateien
|
||||
*/
|
||||
public function clean(): int
|
||||
{
|
||||
$now = time();
|
||||
$deleted = 0;
|
||||
// Alle passenden Cache-Files suchen (z.B. abcdef12345_1717000000.cache.php)
|
||||
foreach ($this->fileSystem->listDirectory($this->cacheFolder, '*.cache.php') as $file) {
|
||||
// Expires-At aus Dateiname extrahieren
|
||||
if (preg_match('/_(\d+)\.cache\.php$/', $file, $match)) {
|
||||
$expiresAt = (int)$match[1];
|
||||
if ($expiresAt > 0 && $expiresAt < $now) {
|
||||
if ($this->fileSystem->delete($file) === null) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
}
|
||||
48
src/Framework/Cache/GeneralCache.php
Normal file
48
src/Framework/Cache/GeneralCache.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class GeneralCache implements Cache
|
||||
{
|
||||
public function __construct(
|
||||
private CacheDriver $adapter
|
||||
) {}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
return $this->adapter->get($key);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool
|
||||
{
|
||||
return $this->adapter->set($key, $value, $ttl);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return $this->adapter->has($key);
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
return $this->adapter->forget($key);
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
return $this->adapter->clear();
|
||||
}
|
||||
|
||||
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
|
||||
{
|
||||
$item = $this->get($key);
|
||||
|
||||
if ($item->isHit) {
|
||||
return $item;
|
||||
}
|
||||
$value = $callback();
|
||||
$this->set($key, $value, $ttl);
|
||||
return $this->get($key)->isHit ? $this->get($key) : CacheItem::hit($key, $value);
|
||||
|
||||
}
|
||||
}
|
||||
103
src/Framework/Cache/LoggingCacheDecorator.php
Normal file
103
src/Framework/Cache/LoggingCacheDecorator.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class LoggingCacheDecorator implements Cache
|
||||
{
|
||||
public function __construct(
|
||||
private Cache $innerCache,
|
||||
) {}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$cacheItem = $this->innerCache->get($key);
|
||||
$status = $cacheItem->isHit ? 'HIT' : 'MISS';
|
||||
$valueType = $cacheItem->value === null ? 'NULL' : gettype($cacheItem->value);
|
||||
error_log("Cache {$status}: {$key} (value: {$valueType})");
|
||||
return $cacheItem;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool
|
||||
{
|
||||
$result = $this->innerCache->set($key, $value, $ttl);
|
||||
$valueType = $value === null ? 'NULL' : gettype($value);
|
||||
$ttlStr = $ttl === null ? 'default' : (string)$ttl;
|
||||
$success = $result ? 'YES' : 'NO';
|
||||
error_log("Cache SET: {$key} = {$valueType}, TTL: {$ttlStr}, Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$exists = $this->innerCache->has($key);
|
||||
$existsStr = $exists ? 'YES' : 'NO';
|
||||
error_log("Cache HAS: {$key} = {$existsStr}");
|
||||
return $exists;
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
$result = $this->innerCache->forget($key);
|
||||
$success = $result ? 'YES' : 'NO';
|
||||
error_log("Cache FORGET: {$key}, Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
$result = $this->innerCache->clear();
|
||||
$success = $result ? 'YES' : 'NO';
|
||||
error_log("Cache CLEAR: Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getMultiple(array $keys): array
|
||||
{
|
||||
$items = $this->innerCache->getMultiple($keys);
|
||||
$hitCount = 0;
|
||||
$missCount = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item->isHit) {
|
||||
$hitCount++;
|
||||
} else {
|
||||
$missCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$keyList = implode(', ', $keys);
|
||||
error_log("Cache GET_MULTIPLE: [{$keyList}] - Hits: {$hitCount}, Misses: {$missCount}");
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function setMultiple(array $items, ?int $ttl = null): bool
|
||||
{
|
||||
$result = $this->innerCache->setMultiple($items, $ttl);
|
||||
$count = count($items);
|
||||
$ttlStr = $ttl === null ? 'default' : (string)$ttl;
|
||||
$success = $result ? 'YES' : 'NO';
|
||||
error_log("Cache SET_MULTIPLE: {$count} items, TTL: {$ttlStr}, Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function deleteMultiple(array $keys): bool
|
||||
{
|
||||
$result = $this->innerCache->deleteMultiple($keys);
|
||||
$count = count($keys);
|
||||
$success = $result ? 'YES' : 'NO';
|
||||
error_log("Cache DELETE_MULTIPLE: {$count} keys, Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
|
||||
{
|
||||
$result = $this->innerCache->remember($key, $callback, $ttl);
|
||||
|
||||
$success = $result->isHit ? 'YES' : 'NO';
|
||||
error_log("Cache REMEMBER: {$key}, Success: {$success}");
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
124
src/Framework/Cache/MultiLevelCache.php
Normal file
124
src/Framework/Cache/MultiLevelCache.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class MultiLevelCache implements Cache
|
||||
{
|
||||
private const int MAX_FAST_CACHE_SIZE = 1024; //1KB
|
||||
private const int FAST_CACHE_TTL = 300;
|
||||
public function __construct(
|
||||
private Cache $fastCache, // z.B. ArrayCache
|
||||
private Cache $slowCache // z.B. RedisCache, FileCache
|
||||
) {}
|
||||
|
||||
public function get(string $key): CacheItem
|
||||
{
|
||||
$item = $this->fastCache->get($key);
|
||||
if ($item->isHit) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
// Fallback auf "langsame" Ebene
|
||||
$item = $this->slowCache->get($key);
|
||||
if ($item->isHit) {
|
||||
// In schnellen Cache zurücklegen (optional mit TTL aus Slow-Item)
|
||||
if($this->shouldCacheInFast($item->value)) {
|
||||
$this->fastCache->set($key, $item->value, self::FAST_CACHE_TTL);
|
||||
}
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value, ?int $ttl = null): bool
|
||||
{
|
||||
$slowSuccess = $this->slowCache->set($key, $value, $ttl);
|
||||
|
||||
$fastSuccess = true;
|
||||
if($this->shouldCacheInFast($value)) {
|
||||
$fastTtl = min($ttl ?? self::FAST_CACHE_TTL, self::FAST_CACHE_TTL);
|
||||
$fastSuccess = $this->fastCache->set($key, $value, $fastTtl);
|
||||
}
|
||||
|
||||
return $slowSuccess && $fastSuccess;
|
||||
}
|
||||
|
||||
public function forget(string $key): bool
|
||||
{
|
||||
$s1 = $this->fastCache->forget($key);
|
||||
$s2 = $this->slowCache->forget($key);
|
||||
return $s1 && $s2;
|
||||
}
|
||||
|
||||
public function clear(): bool
|
||||
{
|
||||
$s1 = $this->fastCache->clear();
|
||||
$s2 = $this->slowCache->clear();
|
||||
return $s1 && $s2;
|
||||
}
|
||||
|
||||
/*
|
||||
* - **has**: Versucht zuerst den schnelleren Layer. Im Falle eines Hits im "slowCache" wird der Wert aufgewärmt/gecached, damit zukünftige Zugriffe wieder schneller sind.
|
||||
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
// Schneller Check im Fast-Cache
|
||||
if ($this->fastCache->has($key)) {
|
||||
return true;
|
||||
}
|
||||
// Ggf. im Slow-Cache prüfen (und dann in Fast-Cache "aufwärmen")
|
||||
$slowHit = $this->slowCache->get($key);
|
||||
if ($slowHit->isHit) {
|
||||
if($this->shouldCacheInFast($slowHit->value)) {
|
||||
$this->fastCache->set($key, $slowHit->value, self::FAST_CACHE_TTL);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* - **remember**: Holt sich das Item per `get` (inkl. aller Multi-Level-Vorteile, wie bereits vorhanden). Wenn nicht im Cache, wird das Callback ausgeführt, gespeichert und als Treffer zurückgegeben.
|
||||
*/
|
||||
|
||||
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
|
||||
{
|
||||
$item = $this->get($key);
|
||||
if ($item->isHit) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
// Wert generieren, speichern und zurückgeben
|
||||
$value = $callback();
|
||||
$this->set($key, $value, $ttl);
|
||||
// Erstelle neuen CacheItem als Treffer
|
||||
return CacheItem::hit($key, $value);
|
||||
|
||||
}
|
||||
|
||||
private function shouldCacheInFast(mixed $value): bool
|
||||
{
|
||||
// Wenn es bereits ein String ist (serialisiert), nutze dessen Länge
|
||||
if (is_string($value)) {
|
||||
return strlen($value) <= self::MAX_FAST_CACHE_SIZE;
|
||||
}
|
||||
|
||||
// Für Arrays: Schnelle Heuristik ohne Serialisierung
|
||||
if (is_array($value)) {
|
||||
$elementCount = count($value, COUNT_RECURSIVE);
|
||||
// Grobe Schätzung: 50 Bytes pro Element
|
||||
$estimatedSize = $elementCount * 50;
|
||||
return $estimatedSize <= self::MAX_FAST_CACHE_SIZE;
|
||||
}
|
||||
|
||||
// Für Objekte: Konservativ - nicht in fast cache
|
||||
if (is_object($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Primitive Typen: Immer cachen
|
||||
return true;
|
||||
}
|
||||
}
|
||||
9
src/Framework/Cache/Serializer.php
Normal file
9
src/Framework/Cache/Serializer.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface Serializer
|
||||
{
|
||||
public function serialize(mixed $value): string;
|
||||
public function unserialize(string $value): mixed;
|
||||
}
|
||||
25
src/Framework/Cache/Serializer/JsonSerializer.php
Normal file
25
src/Framework/Cache/Serializer/JsonSerializer.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Serializer;
|
||||
|
||||
use App\Framework\Cache\Serializer;
|
||||
use JsonException;
|
||||
|
||||
final readonly class JsonSerializer implements Serializer
|
||||
{
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function serialize(mixed $value): string
|
||||
{
|
||||
return json_encode($value, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function unserialize(string $value): mixed
|
||||
{
|
||||
return json_decode($value, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
19
src/Framework/Cache/Serializer/PhpSerializer.php
Normal file
19
src/Framework/Cache/Serializer/PhpSerializer.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache\Serializer;
|
||||
|
||||
use App\Framework\Cache\Serializer;
|
||||
|
||||
final readonly class PhpSerializer implements Serializer
|
||||
{
|
||||
|
||||
public function serialize(mixed $value): string
|
||||
{
|
||||
return serialize($value);
|
||||
}
|
||||
|
||||
public function unserialize(string $value): mixed
|
||||
{
|
||||
return unserialize($value);
|
||||
}
|
||||
}
|
||||
11
src/Framework/Cache/TaggedCache.php
Normal file
11
src/Framework/Cache/TaggedCache.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
final readonly class TaggedCache
|
||||
{
|
||||
public function __construct(
|
||||
public Cache $cache
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user