chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View 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;
}

View 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);
}
}

View 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;
}

View 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();
}
}

View 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);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Framework\Cache;
enum CachePrefix: string
{
case GENERAL = 'cache:';
case QUERY = 'query_cache:';
#case SESSION = 'session:';
}

View 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,
) {}
}

View 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");
}
}

View 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;
}
}

View 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
}
}

View 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;
}

View 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;
}
*/
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Framework\Cache;
interface Serializer
{
public function serialize(mixed $value): string;
public function unserialize(string $value): mixed;
}

View 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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
final readonly class TaggedCache
{
public function __construct(
public Cache $cache
) {}
}