Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -1,18 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\ValueObjects\Duration;
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;
/**
* Get cache items for one or more identifiers (keys, tags, prefixes)
* Returns CacheResult with all matching items (hits and misses)
*/
public function get(CacheIdentifier ...$identifiers): CacheResult;
/**
* Set one or more cache items
* Each CacheItem can have its own TTL
*/
public function set(CacheItem ...$items): bool;
/**
* Check if one or more identifiers exist in cache
* @return array<string, bool> Identifier string => exists
*/
public function has(CacheIdentifier ...$identifiers): array;
/**
* Remove cache items by identifiers (keys, tags, prefixes)
* Supports batch operations and different identifier types
*/
public function forget(CacheIdentifier ...$identifiers): bool;
/**
* Clear all cache items
*/
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;
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem;
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Serializer\Serializer;
/**
* Builder für die einfache Komposition von Cache-Decorators
*
* Ermöglicht eine fluent API für das Erstellen von Cache-Instanzen
* mit verschiedenen Decorators.
*/
final class CacheBuilder
{
private Cache $cache;
private function __construct(Cache $baseCache)
{
$this->cache = $baseCache;
}
/**
* Startet mit einem Base-Cache
*/
public static function create(Cache $baseCache): self
{
return new self($baseCache);
}
/**
* Fügt Logging-Funktionalität hinzu
*/
public function withLogging(): self
{
$this->cache = new LoggingCacheDecorator($this->cache);
return $this;
}
/**
* Fügt Komprimierung hinzu
*
* @deprecated Use GeneralCache with compression parameter instead
*/
public function withCompression(CompressionAlgorithm $algorithm, Serializer $serializer): self
{
trigger_error('CacheBuilder::withCompression() is deprecated. Use GeneralCache with compression parameter instead.', E_USER_DEPRECATED);
// For backward compatibility, wrap with GeneralCache that has compression
// This assumes the cache implements CacheDriver, otherwise this will fail
if ($this->cache instanceof CacheDriver) {
$this->cache = new GeneralCache($this->cache, $serializer, $algorithm);
}
return $this;
}
/**
* Fügt Performance-Metriken hinzu
*/
public function withMetrics(PerformanceCollectorInterface $collector, bool $enabled = true): self
{
// Create default cache metrics instance
$cacheMetrics = new \App\Framework\Cache\Metrics\CacheMetrics();
$this->cache = new \App\Framework\Cache\Metrics\MetricsDecoratedCache(
$this->cache,
$cacheMetrics,
'CacheBuilder',
$collector,
$enabled
);
return $this;
}
/**
* Fügt Validierung hinzu
*/
public function withValidation(array $config = []): self
{
$this->cache = new ValidationCacheDecorator($this->cache, $config);
return $this;
}
/**
* Fügt Event-Dispatching hinzu
*/
public function withEvents(EventDispatcher $eventDispatcher): self
{
$this->cache = new EventCacheDecorator($this->cache, $eventDispatcher);
return $this;
}
/**
* Fügt einen benutzerdefinierten Decorator hinzu
*/
public function withCustomDecorator(callable $decoratorFactory): self
{
$this->cache = $decoratorFactory($this->cache);
return $this;
}
/**
* Erstellt eine Cache-Instanz mit allen konfigurierten Decorators
*/
public function build(): Cache
{
return $this->cache;
}
/**
* Convenience-Methode für eine vollständig ausgestattete Cache-Instanz
*/
public static function createFull(
Cache $baseCache,
PerformanceCollectorInterface $performanceCollector,
EventDispatcher $eventDispatcher,
CompressionAlgorithm $compression,
Serializer $serializer,
array $validationConfig = []
): Cache {
return self::create($baseCache)
->withValidation($validationConfig)
->withCompression($compression, $serializer)
->withMetrics($performanceCollector, true)
->withEvents($eventDispatcher)
->withLogging()
->build();
}
/**
* Convenience-Methode für eine Performance-optimierte Cache-Instanz
*/
public static function createPerformant(
Cache $baseCache,
PerformanceCollectorInterface $performanceCollector,
CompressionAlgorithm $compression,
Serializer $serializer
): Cache {
return self::create($baseCache)
->withCompression($compression, $serializer)
->withMetrics($performanceCollector, true)
->build();
}
/**
* Convenience-Methode für eine Development-Cache-Instanz mit vollständigem Monitoring
*/
public static function createDevelopment(
Cache $baseCache,
PerformanceCollectorInterface $performanceCollector,
EventDispatcher $eventDispatcher,
array $validationConfig = []
): Cache {
return self::create($baseCache)
->withValidation($validationConfig)
->withMetrics($performanceCollector, true)
->withEvents($eventDispatcher)
->withLogging()
->build();
}
}

View File

@@ -1,32 +0,0 @@
<?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

@@ -1,12 +1,35 @@
<?php
declare(strict_types=1);
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;
/**
* Get multiple cache items by keys
*/
public function get(CacheKey ...$keys): CacheResult;
/**
* Set multiple cache items
* Note: CacheDrivers expect values to be serialized strings when needed
*/
public function set(CacheItem ...$items): bool;
/**
* Check if multiple keys exist
* @return array<string, bool> Key string to existence mapping
*/
public function has(CacheKey ...$keys): array;
/**
* Remove multiple keys from cache
*/
public function forget(CacheKey ...$keys): bool;
/**
* Clear all cache data
*/
public function clear(): bool;
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
/**
* Interface for cache identifiers (keys, tags, prefixes, patterns)
* Provides unified way to identify cache items for operations
*/
interface CacheIdentifier
{
/**
* Get string representation of the identifier
*/
public function toString(): string;
/**
* Get the type of cache identifier
*/
public function getType(): CacheIdentifierType;
/**
* Check if this identifier equals another
*/
public function equals(self $other): bool;
/**
* Check if this identifier matches a cache key
* Used for filtering operations
*/
public function matchesKey(CacheKey $key): bool;
/**
* Get a normalized string for internal cache operations
*/
public function getNormalizedString(): string;
}

View File

@@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* Collection of cache identifiers for batch operations
*/
final readonly class CacheIdentifierCollection implements Countable, IteratorAggregate
{
/**
* @param array<CacheIdentifier> $identifiers
*/
private function __construct(
private array $identifiers
) {
}
/**
* Create from CacheIdentifiers using spread operator
*/
public static function fromIdentifiers(CacheIdentifier ...$identifiers): self
{
return new self($identifiers);
}
/**
* Create empty collection
*/
public static function empty(): self
{
return new self([]);
}
/**
* Get all identifiers as array
* @return array<CacheIdentifier>
*/
public function getIdentifiers(): array
{
return $this->identifiers;
}
/**
* Get only cache keys
*/
public function getKeys(): CacheKeyCollection
{
$keys = array_filter(
$this->identifiers,
fn (CacheIdentifier $id) => $id instanceof CacheKey
);
return CacheKeyCollection::fromKeys(...$keys);
}
/**
* Get only cache tags
* @return array<CacheTag>
*/
public function getTags(): array
{
return array_filter(
$this->identifiers,
fn (CacheIdentifier $id) => $id instanceof CacheTag
);
}
/**
* Get only cache prefixes
* @return array<CachePrefix>
*/
public function getPrefixes(): array
{
return array_filter(
$this->identifiers,
fn (CacheIdentifier $id) => $id instanceof CachePrefix
);
}
/**
* Filter by identifier type
*/
public function filterByType(CacheIdentifierType $type): self
{
$filtered = array_filter(
$this->identifiers,
fn (CacheIdentifier $id) => $id->getType() === $type
);
return new self(array_values($filtered));
}
/**
* Check if collection contains specific identifier
*/
public function contains(CacheIdentifier $identifier): bool
{
foreach ($this->identifiers as $existing) {
if ($existing->equals($identifier)) {
return true;
}
}
return false;
}
/**
* Filter identifiers by predicate
*/
public function filter(callable $predicate): self
{
$filtered = array_filter($this->identifiers, $predicate);
return new self(array_values($filtered));
}
/**
* Map over identifiers
*/
public function map(callable $mapper): self
{
$mapped = array_map($mapper, $this->identifiers);
return new self($mapped);
}
/**
* Add another identifier collection
*/
public function merge(self $other): self
{
return new self(array_merge($this->identifiers, $other->identifiers));
}
/**
* Add a single identifier
*/
public function add(CacheIdentifier $identifier): self
{
return new self([...$this->identifiers, $identifier]);
}
/**
* Remove duplicates based on string representation
*/
public function unique(): self
{
$unique = [];
$seen = [];
foreach ($this->identifiers as $identifier) {
$key = $identifier->getType()->value . ':' . $identifier->toString();
if (! isset($seen[$key])) {
$unique[] = $identifier;
$seen[$key] = true;
}
}
return new self($unique);
}
/**
* Check if collection is empty
*/
public function isEmpty(): bool
{
return empty($this->identifiers);
}
/**
* Get identifiers as string array
* @return array<string>
*/
public function toStringArray(): array
{
return array_map(
fn (CacheIdentifier $id) => $id->toString(),
$this->identifiers
);
}
/**
* Group identifiers by type
* @return array<string, array<CacheIdentifier>>
*/
public function groupByType(): array
{
$groups = [];
foreach ($this->identifiers as $identifier) {
$type = $identifier->getType()->value;
$groups[$type] ??= [];
$groups[$type][] = $identifier;
}
return $groups;
}
/**
* Countable implementation
*/
public function count(): int
{
return count($this->identifiers);
}
/**
* IteratorAggregate implementation
* @return Traversable<int, CacheIdentifier>
*/
public function getIterator(): Traversable
{
foreach ($this->identifiers as $identifier) {
yield $identifier;
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
/**
* Enum for different types of cache identifiers
*/
enum CacheIdentifierType: string
{
case KEY = 'key';
case TAG = 'tag';
case PREFIX = 'prefix';
case PATTERN = 'pattern';
/**
* Check if this type supports batch operations
*/
public function supportsBatchOperations(): bool
{
return match ($this) {
self::KEY => true,
self::TAG, self::PREFIX, self::PATTERN => true,
};
}
/**
* Check if this type supports exact matching
*/
public function isExactMatch(): bool
{
return match ($this) {
self::KEY => true,
self::TAG, self::PREFIX, self::PATTERN => false,
};
}
/**
* Get description for debugging
*/
public function getDescription(): string
{
return match ($this) {
self::KEY => 'Exact cache key match',
self::TAG => 'All items with specific tag',
self::PREFIX => 'All items with key prefix',
self::PATTERN => 'All items matching pattern',
};
}
}

View File

@@ -1,63 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Async\AsyncService;
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\Cache\Metrics\CacheMetrics;
use App\Framework\Cache\Metrics\CacheMetricsInterface;
use App\Framework\Cache\Metrics\MetricsDecoratedCache;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Redis\RedisConfig;
use App\Framework\Redis\RedisConnection;
use App\Framework\Serializer\Json\JsonSerializer;
use App\Framework\Serializer\Php\PhpSerializer;
final readonly class CacheInitializer
{
public function __construct(
private PerformanceCollectorInterface $performanceCollector,
private Container $container,
private ?AsyncService $asyncService = null,
#private CacheMetricsInterface $cacheMetrics,
private string $redisHost = 'redis',
private int $redisPort = 6379,
private int $compressionLevel = -1,
private int $minCompressionLength = 1024
) {}
private int $minCompressionLength = 1024,
private bool $enableAsync = true
) {
}
#[Initializer]
public function __invoke(): Cache
{
$this->clear();
#$this->clear();
$serializer = new PhpSerializer();
$serializer = new JsonSerializer();
#$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);
// L1 Cache: Fast cache with compression for larger values
if (function_exists('apcu_clear_cache')) {
$apcuCache = new GeneralCache(new APCuCache(), $serializer, $compression);
} else {
$apcuCache = new GeneralCache(new InMemoryCache(), $serializer, $compression);
}
$compressedApcuCache = new CompressionCacheDecorator(
$apcuCache,
$compression,
$serializer
);
// L2 Cache: Persistent cache with compression
try {
$redisConfig = new RedisConfig(
host: $this->redisHost,
port: $this->redisPort,
database: 1 // Use DB 1 for cache
);
$redisConnection = new RedisConnection($redisConfig, 'cache');
$redisCache = new GeneralCache(new RedisCache($redisConnection), $serializer, $compression);
} catch (\Throwable $e) {
// Fallback to file cache if Redis is not available
error_log("Redis not available, falling back to file cache: " . $e->getMessage());
$redisCache = new GeneralCache(new FileCache(), $serializer, $compression);
}
// L2 Cache:
$redisCache = new GeneralCache(new RedisCache(host: $this->redisHost, port: $this->redisPort));
$compressedRedisCache = new CompressionCacheDecorator(
$redisCache,
$compression,
$serializer
);
#$redisCache->clear();
$multiLevelCache = new MultiLevelCache($compressedApcuCache, $compressedRedisCache);
$multiLevelCache = new MultiLevelCache($apcuCache, $redisCache);
#return $multiLevelCache;
return new LoggingCacheDecorator($multiLevelCache);
#return new LoggingCacheDecorator($multiLevelCache);
#return new GeneralCache(new NullCache(), $serializer, $compression);
// Create cache metrics instance directly to avoid DI circular dependency
$cacheMetrics = new CacheMetrics();
// Bind it to container for other services that might need it
if (! $this->container->has(CacheMetricsInterface::class)) {
$this->container->bind(CacheMetricsInterface::class, $cacheMetrics);
}
// Add comprehensive cache metrics with integrated performance tracking
$metricsCache = new MetricsDecoratedCache(
$multiLevelCache,
$cacheMetrics,
'MultiLevel',
$this->performanceCollector,
true // Performance tracking enabled
);
// Wrap with SmartCache for intelligent async processing and pattern support
return new SmartCache(
$metricsCache,
$this->asyncService,
$this->enableAsync
);
#return new GeneralCache(new NullCache());

View File

@@ -1,21 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\ValueObjects\Duration;
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 CacheKey $key,
public mixed $value,
public bool $isHit = false,
public ?Duration $ttl = null,
) {
}
public static function hit(string $key, mixed $value): self
public static function miss(CacheKey $key): self
{
return new self($key, $value, true);
return new self(key: $key, value: null, isHit: false, ttl: null);
}
public static function hit(CacheKey $key, mixed $value, ?Duration $ttl = null): self
{
return new self(key: $key, value: $value, isHit: true, ttl: $ttl);
}
/**
* Create a cache item for setting (not from cache retrieval)
*/
public static function forSet(CacheKey $key, mixed $value, ?Duration $ttl = null): self
{
return new self(
key: $key,
value: $value,
ttl: $ttl
);
}
public static function fromValues(string $key, mixed $value, int|Duration|null $ttl = null): self
{
return new self(
key : CacheKey::fromString($key),
value: $value,
ttl : $ttl instanceof Duration ? $ttl : Duration::fromSeconds($ttl),
);
}
/**
* Check if this item has a TTL set
*/
public function hasTtl(): bool
{
return $this->ttl !== null;
}
/**
* Create a new cache item with different TTL
*/
public function withTtl(?Duration $ttl): self
{
return new self($this->key, $this->value, $this->isHit, $ttl);
}
/**
* Create a new cache item with different value
*/
public function withValue(mixed $value): self
{
return new self($this->key, $value, $this->isHit, $this->ttl);
}
}

View File

@@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use InvalidArgumentException;
/**
* Value Object für Cache-Schlüssel
* Stellt sicher, dass Cache-Schlüssel gültig und konsistent sind
*/
final readonly class CacheKey implements CacheIdentifier
{
private const int MAX_KEY_LENGTH = 250;
private const string NAMESPACE_SEPARATOR = ':';
private function __construct(
private string $key
) {
$this->validate($key);
}
/**
* Erstellt einen CacheKey aus einem String
*/
public static function fromString(string $key): self
{
return new self($key);
}
/**
* Create cache key with automatic truncation for very long keys
*/
public static function fromStringSafe(string $key): self
{
if (strlen($key) > self::MAX_KEY_LENGTH) {
// Truncate and hash to ensure uniqueness while staying within limits
$key = substr($key, 0, 200) . '_' . md5($key);
}
return new self($key);
}
/**
* Erstellt einen CacheKey mit Namespace
*/
public static function fromNamespace(string $namespace, string $key): self
{
return new self($namespace . self::NAMESPACE_SEPARATOR . $key);
}
/**
* Erstellt einen CacheKey für ein bestimmtes Objekt
*/
public static function forObject(object $object, string $suffix = ''): self
{
$className = self::getShortClassName($object::class);
$objectId = method_exists($object, 'getId') ? $object->getId() : spl_object_id($object);
$key = $className . self::NAMESPACE_SEPARATOR . $objectId;
if (! empty($suffix)) {
$key .= self::NAMESPACE_SEPARATOR . $suffix;
}
return new self($key);
}
/**
* Erstellt einen CacheKey für eine Klasse
*/
public static function forClass(string $class, string $suffix = ''): self
{
$className = self::getShortClassName($class);
$key = $className;
if (! empty($suffix)) {
$key .= self::NAMESPACE_SEPARATOR . $suffix;
}
return new self($key);
}
/**
* Erstellt einen CacheKey für eine Abfrage
*/
public static function forQuery(string $query, array $parameters = []): self
{
$normalizedQuery = self::normalizeString($query);
$queryHash = md5($normalizedQuery);
$key = 'query' . self::NAMESPACE_SEPARATOR . $queryHash;
if (! empty($parameters)) {
$paramHash = md5(serialize($parameters));
$key .= self::NAMESPACE_SEPARATOR . $paramHash;
}
return new self($key);
}
/**
* Erstellt einen CacheKey für eine Sammlung von Daten
*/
public static function forCollection(string $type, array $criteria = []): self
{
$shortType = self::getShortClassName($type);
$key = 'collection' . self::NAMESPACE_SEPARATOR . $shortType;
if (! empty($criteria)) {
$criteriaHash = md5(serialize($criteria));
$key .= self::NAMESPACE_SEPARATOR . $criteriaHash;
}
return new self($key);
}
/**
* Gibt den Schlüssel als String zurück
*/
public function toString(): string
{
return $this->key;
}
/**
* Gibt den Schlüssel als String zurück
*/
public function __toString(): string
{
return $this->key;
}
/**
* Fügt einen Suffix zum Schlüssel hinzu
*/
public function withSuffix(string $suffix): self
{
return new self($this->key . self::NAMESPACE_SEPARATOR . $suffix);
}
/**
* Fügt einen Namespace zum Schlüssel hinzu
*/
public function withNamespace(string $namespace): self
{
return new self($namespace . self::NAMESPACE_SEPARATOR . $this->key);
}
/**
* Prüft, ob der Schlüssel einem Muster entspricht
*/
public function matches(string $pattern): bool
{
return fnmatch($pattern, $this->key);
}
/**
* Prüft, ob zwei CacheKeys gleich sind
*/
public function equals(CacheIdentifier $other): bool
{
return $other instanceof self && $this->key === $other->key;
}
/**
* Get the type of cache identifier
*/
public function getType(): CacheIdentifierType
{
return CacheIdentifierType::KEY;
}
/**
* Check if this identifier matches a cache key (CacheIdentifier interface)
*/
public function matchesKey(CacheKey $key): bool
{
return $this->equals($key);
}
/**
* Get a normalized string for internal cache operations
*/
public function getNormalizedString(): string
{
return $this->key;
}
/**
* Validiert den Schlüssel
*/
private function validate(string $key): void
{
if (empty($key)) {
throw new InvalidArgumentException('Cache key cannot be empty');
}
if (strlen($key) > self::MAX_KEY_LENGTH) {
throw new InvalidArgumentException(sprintf(
'Cache key length exceeds maximum of %d characters (got %d)',
self::MAX_KEY_LENGTH,
strlen($key)
));
}
// Prüfe auf ungültige Zeichen (z.B. Leerzeichen, Steuerzeichen)
if (preg_match('/[\s\n\r\t\0\x0B]/', $key)) {
throw new InvalidArgumentException('Cache key contains invalid characters');
}
}
/**
* Normalisiert einen String für konsistente Hashes
*/
private static function normalizeString(string $input): string
{
// Entferne überflüssige Leerzeichen
return preg_replace('/\s+/', ' ', trim($input));
}
/**
* Extrahiert den Klassennamen ohne Namespace
*/
private static function getShortClassName(string $class): string
{
$parts = explode('\\', $class);
return end($parts);
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* Collection of cache keys for batch operations
*/
final readonly class CacheKeyCollection implements Countable, IteratorAggregate
{
/**
* @param array<CacheKey> $keys
*/
private function __construct(
private array $keys
) {
}
/**
* Create from CacheKeys using spread operator
*/
public static function fromKeys(CacheKey ...$keys): self
{
return new self($keys);
}
/**
* Create empty collection
*/
public static function empty(): self
{
return new self([]);
}
/**
* Get all keys as array
* @return array<CacheKey>
*/
public function getKeys(): array
{
return $this->keys;
}
/**
* Get first key or null if empty
*/
public function first(): ?CacheKey
{
return $this->keys[0] ?? null;
}
/**
* Get last key or null if empty
*/
public function last(): ?CacheKey
{
return end($this->keys) ?: null;
}
/**
* Check if collection contains specific key
*/
public function contains(CacheKey $key): bool
{
foreach ($this->keys as $existingKey) {
if ($existingKey->equals($key)) {
return true;
}
}
return false;
}
/**
* Filter keys by predicate
*/
public function filter(callable $predicate): self
{
$filtered = array_filter($this->keys, $predicate);
return new self(array_values($filtered));
}
/**
* Map over keys
*/
public function map(callable $mapper): self
{
$mapped = array_map($mapper, $this->keys);
return new self($mapped);
}
/**
* Add another key collection
*/
public function merge(self $other): self
{
return new self(array_merge($this->keys, $other->keys));
}
/**
* Add a single key
*/
public function add(CacheKey $key): self
{
return new self([...$this->keys, $key]);
}
/**
* Remove duplicates based on string representation
*/
public function unique(): self
{
$unique = [];
$seen = [];
foreach ($this->keys as $key) {
$keyString = $key->toString();
if (! isset($seen[$keyString])) {
$unique[] = $key;
$seen[$keyString] = true;
}
}
return new self($unique);
}
/**
* Check if collection is empty
*/
public function isEmpty(): bool
{
return empty($this->keys);
}
/**
* Get keys as string array
* @return array<string>
*/
public function toStringArray(): array
{
return array_map(fn (CacheKey $key) => $key->toString(), $this->keys);
}
/**
* Countable implementation
*/
public function count(): int
{
return count($this->keys);
}
/**
* IteratorAggregate implementation
* @return Traversable<int, CacheKey>
*/
public function getIterator(): Traversable
{
foreach ($this->keys as $key) {
yield $key;
}
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use InvalidArgumentException;
/**
* Cache pattern identifier for wildcard-based operations
* Supports patterns like "user:*", "cache.*.data", etc.
*/
final readonly class CachePattern implements CacheIdentifier
{
private const int MAX_PATTERN_LENGTH = 150;
private const string PATTERN_MARKER = 'pattern:';
private function __construct(
private string $pattern,
private string $compiledRegex
) {
$this->validate($pattern);
}
/**
* Create cache pattern from wildcard string
*
* Supports:
* - user:* (matches user:123, user:456, etc.)
* - cache.*.data (matches cache.sessions.data, cache.users.data)
* - temp:** (matches temp:anything:nested:deeply)
*/
public static function fromWildcard(string $pattern): self
{
$regex = self::compilePattern($pattern);
return new self($pattern, $regex);
}
/**
* Create pattern for all keys with prefix
*/
public static function withPrefix(string $prefix): self
{
return self::fromWildcard($prefix . '*');
}
/**
* Create pattern for all user-related keys
*/
public static function forUser(string|int $userId): self
{
return self::fromWildcard("user:{$userId}:*");
}
/**
* Create pattern for all session keys
*/
public static function forSessions(): self
{
return self::fromWildcard('session:*');
}
/**
* Create pattern for temporary keys
*/
public static function forTemporary(): self
{
return self::fromWildcard('temp:**');
}
/**
* Create pattern for namespace
*/
public static function forNamespace(string $namespace): self
{
return self::fromWildcard("{$namespace}:**");
}
public function toString(): string
{
return $this->pattern;
}
public function getType(): CacheIdentifierType
{
return CacheIdentifierType::PATTERN;
}
public function equals(CacheIdentifier $other): bool
{
return $other instanceof self && $this->pattern === $other->pattern;
}
public function matchesKey(CacheKey $key): bool
{
return preg_match($this->compiledRegex, $key->toString()) === 1;
}
public function getNormalizedString(): string
{
return self::PATTERN_MARKER . $this->pattern;
}
/**
* Get the original wildcard pattern
*/
public function getPattern(): string
{
return $this->pattern;
}
/**
* Get compiled regex pattern
*/
public function getCompiledRegex(): string
{
return $this->compiledRegex;
}
/**
* Check if pattern is simple prefix (ends with single *)
*/
public function isSimplePrefix(): bool
{
return str_ends_with($this->pattern, '*') &&
substr_count($this->pattern, '*') === 1 &&
! str_contains($this->pattern, '**');
}
/**
* Get prefix part for simple prefix patterns
*/
public function getPrefix(): ?string
{
if (! $this->isSimplePrefix()) {
return null;
}
return substr($this->pattern, 0, -1);
}
/**
* Check if pattern matches deep nesting (**)
*/
public function isDeepPattern(): bool
{
return str_contains($this->pattern, '**');
}
/**
* Estimate selectivity (0.0 = matches everything, 1.0 = very specific)
*/
public function getSelectivity(): float
{
$wildcardCount = substr_count($this->pattern, '*');
$deepCount = substr_count($this->pattern, '**');
$length = strlen($this->pattern);
// More specific patterns have higher selectivity
$specificity = $length / max(1, $wildcardCount * 5 + $deepCount * 10);
return min(1.0, $specificity / 20); // Normalize to 0-1 range
}
/**
* Compile wildcard pattern to regex
*/
private static function compilePattern(string $pattern): string
{
// Escape special regex characters except * and **
$escaped = preg_quote($pattern, '/');
// Replace escaped wildcards back
$escaped = str_replace('\\*\\*', '__DEEP_WILDCARD__', $escaped);
$escaped = str_replace('\\*', '__WILDCARD__', $escaped);
// Convert to regex
$regex = str_replace('__DEEP_WILDCARD__', '.*', $escaped);
$regex = str_replace('__WILDCARD__', '[^:]*', $regex);
return '/^' . $regex . '$/';
}
/**
* Validate the pattern
*/
private function validate(string $pattern): void
{
if (empty($pattern)) {
throw new InvalidArgumentException('Cache pattern cannot be empty');
}
if (strlen($pattern) > self::MAX_PATTERN_LENGTH) {
throw new InvalidArgumentException(sprintf(
'Cache pattern length exceeds maximum of %d characters (got %d)',
self::MAX_PATTERN_LENGTH,
strlen($pattern)
));
}
// Check for invalid characters
if (preg_match('/[\s\n\r\t\0\x0B]/', $pattern)) {
throw new InvalidArgumentException('Cache pattern contains invalid characters');
}
// Validate wildcard usage
if (str_contains($pattern, '***')) {
throw new InvalidArgumentException('Cache pattern cannot contain more than two consecutive wildcards');
}
}
}

View File

@@ -1,12 +1,182 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
enum CachePrefix: string
use InvalidArgumentException;
/**
* Cache prefix identifier for prefix-based operations
* Allows operations on all cache items with specific prefix
*/
final readonly class CachePrefix implements CacheIdentifier
{
case GENERAL = 'cache:';
private const int MAX_PREFIX_LENGTH = 100;
private const string PREFIX_MARKER = 'prefix:';
case QUERY = 'query_cache:';
private function __construct(
private string $prefix
) {
$this->validate($prefix);
}
#case SESSION = 'session:';
/**
* Create cache prefix from string
*/
public static function fromString(string $prefix): self
{
return new self($prefix);
}
/**
* Create prefix for general cache items
*/
public static function general(): self
{
return new self('cache:');
}
/**
* Create prefix for query cache items
*/
public static function query(): self
{
return new self('query_cache:');
}
/**
* Create prefix for session items
*/
public static function session(): self
{
return new self('session:');
}
/**
* Create prefix for specific namespace
*/
public static function forNamespace(string $namespace): self
{
return new self($namespace . ':');
}
/**
* Create prefix for user-related items
*/
public static function forUser(string|int $userId): self
{
return new self("user:{$userId}:");
}
/**
* Create prefix for temporary items
*/
public static function forTemporary(): self
{
return new self('temp:');
}
/**
* Get string representation of the prefix
*/
public function toString(): string
{
return $this->prefix;
}
/**
* Get the type of cache identifier
*/
public function getType(): CacheIdentifierType
{
return CacheIdentifierType::PREFIX;
}
/**
* Check if this prefix equals another identifier
*/
public function equals(CacheIdentifier $other): bool
{
return $other instanceof self && $this->prefix === $other->prefix;
}
/**
* Check if this prefix matches a cache key
*/
public function matchesKey(CacheKey $key): bool
{
return str_starts_with($key->toString(), $this->prefix);
}
/**
* Get a normalized string for internal cache operations
*/
public function getNormalizedString(): string
{
return self::PREFIX_MARKER . $this->prefix;
}
/**
* Create a cache key with this prefix
*/
public function createKey(string $suffix): CacheKey
{
return CacheKey::fromString($this->prefix . $suffix);
}
/**
* Remove this prefix from a cache key string
*/
public function removeFromKey(string $key): string
{
if (str_starts_with($key, $this->prefix)) {
return substr($key, strlen($this->prefix));
}
return $key;
}
/**
* Check if prefix ends with separator
*/
public function hasTrailingSeparator(): bool
{
return str_ends_with($this->prefix, ':');
}
/**
* Ensure prefix has trailing separator
*/
public function withTrailingSeparator(): self
{
if ($this->hasTrailingSeparator()) {
return $this;
}
return new self($this->prefix . ':');
}
/**
* Validate the prefix
*/
private function validate(string $prefix): void
{
if (empty($prefix)) {
throw new InvalidArgumentException('Cache prefix cannot be empty');
}
if (strlen($prefix) > self::MAX_PREFIX_LENGTH) {
throw new InvalidArgumentException(sprintf(
'Cache prefix length exceeds maximum of %d characters (got %d)',
self::MAX_PREFIX_LENGTH,
strlen($prefix)
));
}
// Check for invalid characters
if (preg_match('/[\s\n\r\t\0\x0B*?]/', $prefix)) {
throw new InvalidArgumentException('Cache prefix contains invalid characters');
}
}
}

View File

@@ -0,0 +1,358 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* Collection of cache items returned from multi-key cache operations
* Provides convenient access to hits, misses, and individual items
*
* Backward compatibility: For single-key operations, provides direct access
* to isHit and value properties of the first item
*/
final readonly class CacheResult implements Countable, IteratorAggregate
{
/**
* Backward compatibility: TRUE if at least one item is a hit
*/
public readonly bool $isHit;
/**
* Backward compatibility: Value of the first item (for single-key operations)
*/
public readonly mixed $value;
/**
* @param array<string, CacheItem> $items Keyed by cache key string
*/
private function __construct(
private array $items
) {
// Initialize backward compatibility properties
$firstKey = array_key_first($this->items);
$firstItem = $firstKey !== null ? $this->items[$firstKey] : CacheItem::miss(CacheKey::fromString('_empty_'));
$this->isHit = $firstItem->isHit;
$this->value = $firstItem->value;
}
/**
* Create from CacheItems using spread operator
*/
public static function fromItems(CacheItem ...$items): self
{
// EMERGENCY: Ultra-strict limits to prevent memory exhaustion
$itemCount = count($items);
// Check memory before processing
$memoryUsage = memory_get_usage(true);
if ($memoryUsage > 400 * 1024 * 1024) { // >400MB
error_log("EMERGENCY: CacheResult refused - memory usage: " . round($memoryUsage / 1024 / 1024, 2) . "MB");
throw new \RuntimeException("EMERGENCY: Memory usage too high for CacheResult creation");
}
// Ultra-strict item limit
if ($itemCount > 100) { // Reduced from 1000 to 100
error_log("EMERGENCY: Too many cache items ($itemCount) - max 100 allowed");
throw new \RuntimeException("EMERGENCY: Too many cache items ($itemCount) - max 100 allowed");
}
$indexed = [];
$count = 0;
foreach ($items as $item) {
// EMERGENCY: Memory check during iteration
if ($count % 10 === 0) {
$currentMemory = memory_get_usage(true);
if ($currentMemory > 450 * 1024 * 1024) { // >450MB
error_log("EMERGENCY: CacheResult iteration stopped at item $count - memory critical");
break;
}
}
// EMERGENCY: Ultra-strict iteration limit
if (++$count > 100) {
error_log("EMERGENCY: CacheResult truncated at 100 items (was $itemCount)");
break;
}
$indexed[$item->key->toString()] = $item;
}
return new self($indexed);
}
/**
* Create from key-value pairs
* @param array<CacheKey, mixed> $data
*/
public static function fromData(array $data): self
{
$items = [];
foreach ($data as $key => $value) {
$keyString = $key->toString();
$items[$keyString] = $value !== null
? CacheItem::hit($key, $value)
: CacheItem::miss($key);
}
return new self($items);
}
/**
* Create empty result
*/
public static function empty(): self
{
return new self([]);
}
/**
* Get cache item for specific key
*/
public function getItem(CacheKey $key): CacheItem
{
$keyString = $key->toString();
return $this->items[$keyString] ?? CacheItem::miss($key);
}
/**
* Get all cache items
* @return array<string, CacheItem> Keyed by cache key string
*/
public function getItems(): array
{
return $this->items;
}
/**
* Get only cache hits as new CacheResult
*/
public function getHits(): self
{
$hits = array_filter($this->items, fn (CacheItem $item) => $item->isHit);
return new self($hits);
}
/**
* Get only cache misses as new CacheResult
*/
public function getMisses(): self
{
$misses = array_filter($this->items, fn (CacheItem $item) => ! $item->isHit);
return new self($misses);
}
/**
* Check if specific key resulted in a cache hit
*/
public function hasHit(CacheKey $key): bool
{
return $this->getItem($key)->isHit;
}
/**
* Check if specific key resulted in a cache miss
*/
public function hasMiss(CacheKey $key): bool
{
return ! $this->hasHit($key);
}
/**
* Get hit ratio as percentage (0.0 to 1.0)
*/
public function getHitRatio(): float
{
if (empty($this->items)) {
return 0.0;
}
$hitCount = count($this->getHits()->items);
return $hitCount / count($this->items);
}
/**
* Get values for all cache hits
* @return array<string, mixed> Keyed by cache key string
*/
public function getHitValues(): array
{
$values = [];
foreach ($this->getHits()->items as $keyString => $item) {
$values[$keyString] = $item->value;
}
return $values;
}
/**
* Get cache keys that resulted in hits
*/
public function getHitKeys(): CacheKeyCollection
{
$keys = [];
foreach ($this->getHits()->items as $item) {
$keys[] = $item->key;
}
return CacheKeyCollection::fromKeys(...$keys);
}
/**
* Get cache keys that resulted in misses
*/
public function getMissKeys(): CacheKeyCollection
{
$keys = [];
foreach ($this->getMisses()->items as $item) {
$keys[] = $item->key;
}
return CacheKeyCollection::fromKeys(...$keys);
}
/**
* Get all cache keys
*/
public function getKeys(): CacheKeyCollection
{
$keys = [];
foreach ($this->items as $item) {
$keys[] = $item->key;
}
return CacheKeyCollection::fromKeys(...$keys);
}
/**
* Check if result contains any items
*/
public function isEmpty(): bool
{
return empty($this->items);
}
/**
* Check if all requested keys were cache hits
*/
public function isCompleteHit(): bool
{
if (empty($this->items)) {
return false;
}
foreach ($this->items as $item) {
if (! $item->isHit) {
return false;
}
}
return true;
}
/**
* Check if all requested keys were cache misses
*/
public function isCompleteMiss(): bool
{
if (empty($this->items)) {
return true;
}
foreach ($this->items as $item) {
if ($item->isHit) {
return false;
}
}
return true;
}
/**
* Get value for specific key, or null if miss
*/
public function getValue(CacheKey $key): mixed
{
$item = $this->getItem($key);
return $item->isHit ? $item->value : null;
}
/**
* Filter results by predicate, returns new CacheResult
*/
public function filter(callable $predicate): self
{
$filtered = array_filter($this->items, $predicate);
return new self($filtered);
}
/**
* Map over cache items, returns new CacheResult
*/
public function map(callable $mapper): self
{
$mapped = array_map($mapper, $this->items);
return self::fromItems(...$mapped);
}
/**
* Merge with another CacheResult
*/
public function merge(self $other): self
{
return new self(array_merge($this->items, $other->items));
}
/**
* Countable implementation
*/
public function count(): int
{
return count($this->items);
}
/**
* IteratorAggregate implementation
* @return Traversable<string, CacheItem>
*/
public function getIterator(): Traversable
{
foreach ($this->items as $keyString => $item) {
yield $keyString => $item;
}
}
/**
* Convert to array for debugging
* @return array<string, array{key: string, value: mixed, hit: bool}>
*/
public function toArray(): array
{
$result = [];
foreach ($this->items as $keyString => $item) {
$result[$keyString] = [
'key' => $item->key->toString(),
'value' => $item->value,
'hit' => $item->isHit,
];
}
return $result;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use InvalidArgumentException;
/**
* Cache tag identifier for grouping and batch operations
* Allows invalidating multiple cache items by tag
*/
final readonly class CacheTag implements CacheIdentifier
{
private const int MAX_TAG_LENGTH = 100;
private const string TAG_PREFIX = 'tag:';
private function __construct(
private string $tag
) {
$this->validate($tag);
}
/**
* Create cache tag from string
*/
public static function fromString(string $tag): self
{
return new self($tag);
}
/**
* Create cache tag for specific domain/type
*/
public static function forType(string $type): self
{
return new self($type);
}
/**
* Create cache tag for user-related items
*/
public static function forUser(string|int $userId): self
{
return new self("user:{$userId}");
}
/**
* Create cache tag for entity type
*/
public static function forEntity(string $entityType): self
{
return new self("entity:{$entityType}");
}
/**
* Get string representation of the tag
*/
public function toString(): string
{
return $this->tag;
}
/**
* Get the type of cache identifier
*/
public function getType(): CacheIdentifierType
{
return CacheIdentifierType::TAG;
}
/**
* Check if this tag equals another identifier
*/
public function equals(CacheIdentifier $other): bool
{
return $other instanceof self && $this->tag === $other->tag;
}
/**
* Check if this tag matches a cache key
* Tags match keys that contain the tag in their metadata
*/
public function matchesKey(CacheKey $key): bool
{
// This would need to be implemented by checking key metadata/tags
// For now, simple string containment check
return str_contains($key->toString(), $this->tag);
}
/**
* Get a normalized string for internal cache operations
*/
public function getNormalizedString(): string
{
return self::TAG_PREFIX . $this->tag;
}
/**
* Create a cache key that includes this tag
*/
public function createKeyWithTag(string $baseKey): CacheKey
{
return CacheKey::fromString($baseKey . ':' . $this->tag);
}
/**
* Check if tag matches a pattern
*/
public function matchesPattern(string $pattern): bool
{
return fnmatch($pattern, $this->tag);
}
/**
* Validate the tag
*/
private function validate(string $tag): void
{
if (empty($tag)) {
throw new InvalidArgumentException('Cache tag cannot be empty');
}
if (strlen($tag) > self::MAX_TAG_LENGTH) {
throw new InvalidArgumentException(sprintf(
'Cache tag length exceeds maximum of %d characters (got %d)',
self::MAX_TAG_LENGTH,
strlen($tag)
));
}
// Check for invalid characters (allow colons for namespacing)
if (preg_match('/[\s\n\r\t\0\x0B*?]/', $tag)) {
throw new InvalidArgumentException('Cache tag contains invalid characters');
}
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
#[\Attribute(\Attribute::TARGET_METHOD)]
@@ -11,5 +13,6 @@ final class Cacheable
public function __construct(
public ?string $key = null,
public int $ttl = 3600,
) {}
) {
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Commands;
@@ -7,17 +8,176 @@ use App\Framework\Cache\Cache;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Core\PathProvider;
use App\Framework\DI\Container;
use App\Framework\Redis\RedisConnectionPool;
final readonly class ClearCache
{
public function __construct(
private Cache $cache,
) {}
private PathProvider $pathProvider,
private Container $container
) {
}
#[ConsoleCommand("cache:clear", "Clears the cache")]
#[ConsoleCommand("cache:clear", "Clears all caches (application, discovery, routes, opcache, redis)")]
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
{
$this->cache->clear();
$output->writeSuccess("Cache cleared");
$cleared = [];
// Clear OPcache
if (function_exists('opcache_reset')) {
opcache_reset();
$cleared[] = 'OPcache';
}
// Clear Redis cache completely - this is now ALWAYS done
// because Discovery cache and other critical framework caches are stored in Redis
if ($this->clearRedisCompletely()) {
$cleared[] = 'Redis (FLUSHALL)';
} else {
// Fallback: Clear application cache through Cache interface
$this->cache->clear();
$cleared[] = 'Application cache (fallback)';
}
// Clear discovery cache files (redundant after Redis FLUSHALL, but safe)
$this->clearDiscoveryFiles();
$cleared[] = 'Discovery files';
// Clear routes cache
$routesCacheFile = $this->pathProvider->resolvePath('/cache/routes.cache.php');
if (file_exists($routesCacheFile)) {
unlink($routesCacheFile);
$cleared[] = 'Routes cache';
}
// Clear all cache files
$this->clearAllCacheFiles();
$cleared[] = 'All cache files';
$output->writeSuccess('Cleared: ' . implode(', ', $cleared));
}
#[ConsoleCommand("redis:flush", "Advanced Redis cache clearing with options")]
public function flushRedis(ConsoleInput $input, ConsoleOutput $output): void
{
$cleared = [];
if (! $this->container->has(RedisConnectionPool::class)) {
$output->writeError('Redis connection pool not available');
return;
}
try {
$redisPool = $this->container->get(RedisConnectionPool::class);
$connection = $redisPool->getConnection('cache');
$redis = $connection->getClient();
// Option: --db to flush specific database
if ($input->hasOption('db')) {
$database = (int) $input->getOption('db');
$redis->select($database);
$result = $redis->flushDB();
if ($result === true) {
$cleared[] = "Redis database $database";
} else {
$output->writeError("Failed to flush Redis database $database");
return;
}
} else {
// Default: FLUSHALL
$result = $redis->flushAll();
if ($result === true) {
$cleared[] = 'Redis (all databases)';
} else {
$output->writeError('Failed to flush Redis');
return;
}
}
$output->writeSuccess('Cleared: ' . implode(', ', $cleared));
} catch (\Throwable $e) {
$output->writeError('Redis flush failed: ' . $e->getMessage());
}
}
private function clearDiscoveryFiles(): void
{
$cacheDir = $this->pathProvider->resolvePath('/cache');
if (! is_dir($cacheDir)) {
return;
}
$files = glob($cacheDir . '/discovery_*.cache.php');
foreach ($files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
}
private function clearAllCacheFiles(): void
{
$cacheDir = $this->pathProvider->resolvePath('/cache');
if (! is_dir($cacheDir)) {
return;
}
$files = glob($cacheDir . '/*.cache.php');
foreach ($files as $file) {
if (file_exists($file)) {
unlink($file);
}
}
}
private function clearRedisCompletely(): bool
{
try {
// Strategy 1: Direct Redis connection via RedisConnectionPool (lazy-loaded)
try {
if ($this->container->has(RedisConnectionPool::class)) {
$redisPool = $this->container->get(RedisConnectionPool::class);
$connection = $redisPool->getConnection('cache');
$redis = $connection->getClient();
// Use FLUSHALL to clear all Redis databases
$result = $redis->flushAll();
if ($result === true) {
return true;
}
// Fallback: FLUSHDB for cache database (database 1)
$redis->select(1);
$result = $redis->flushDB();
if ($result === true) {
return true;
}
}
} catch (\Throwable $e) {
error_log("Direct Redis connection failed: " . $e->getMessage());
}
// Strategy 2: Through cache interface clear method (limited effectiveness)
try {
$this->cache->clear();
// Note: This only clears application cache patterns, not Discovery cache
return false; // Return false to indicate partial clearing
} catch (\Throwable $e) {
error_log("Cache interface clear failed: " . $e->getMessage());
}
return false;
} catch (\Exception $e) {
error_log("Redis cache clear failed: " . $e->getMessage());
return false;
}
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Compression;
use App\Framework\Cache\CompressionAlgorithm;
@@ -7,7 +9,9 @@ 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)
@@ -18,7 +22,7 @@ final class GzipCompression implements CompressionAlgorithm
public function compress(string $value, bool $forceCompression = false): string
{
if (!$forceCompression && strlen($value) < $this->threshold) {
if (! $forceCompression && strlen($value) < $this->threshold) {
return $value;
}
$compressed = gzcompress($value, $this->level);
@@ -26,16 +30,18 @@ final class GzipCompression implements CompressionAlgorithm
// Fallback auf Originalwert bei Fehler
return $value;
}
return self::PREFIX . $compressed;
}
public function decompress(string $value): string
{
if (!$this->isCompressed($value)) {
if (! $this->isCompressed($value)) {
return $value;
}
$raw = substr($value, strlen(self::PREFIX));
$decompressed = @gzuncompress($raw);
return $decompressed !== false ? $decompressed : $value;
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Compression;
use App\Framework\Cache\CompressionAlgorithm;
/**
* No-operation compression algorithm that performs no compression
*
* This implements the Null Object Pattern for compression,
* allowing the cache system to work without null checks.
*/
final readonly class NoCompression implements CompressionAlgorithm
{
public function compress(string $value, bool $forceCompression = false): string
{
// No compression - return value unchanged
return $value;
}
public function decompress(string $value): string
{
// No decompression needed - return value unchanged
return $value;
}
public function isCompressed(string $value): bool
{
// Values are never compressed with this algorithm
return false;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Compression;
use App\Framework\Cache\CompressionAlgorithm;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
interface CompressionAlgorithm

View File

@@ -1,168 +0,0 @@
<?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,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Contracts;
use App\Framework\Cache\CacheDriver;
/**
* Interface for cache implementations that can provide access to their underlying driver
*
* This allows proper access to driver-specific features like scanning
* without resorting to reflection APIs.
*/
interface DriverAccessible
{
/**
* Get the underlying cache driver for direct access
*
* @return CacheDriver|null The driver, or null if not available
*/
public function getDriver(): ?CacheDriver;
/**
* Check if the underlying driver supports a specific interface
*
* @param class-string $interface The interface to check for
* @return bool Whether the driver implements the interface
*/
public function driverSupports(string $interface): bool;
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Contracts;
/**
* Interface for cache drivers that support key scanning operations
*
* Not all drivers can efficiently scan keys, so this is optional.
* Drivers that implement this interface can provide pattern-based operations.
*/
interface Scannable
{
/**
* Scan for keys matching a pattern
*
* @param string $pattern Wildcard pattern (e.g., "user:*", "cache.*.data")
* @param int $limit Maximum number of keys to return (0 = no limit)
* @return array<string> Array of matching key strings
*/
public function scan(string $pattern, int $limit = 1000): array;
/**
* Scan for keys with a specific prefix
*
* @param string $prefix Key prefix to match
* @param int $limit Maximum number of keys to return (0 = no limit)
* @return array<string> Array of matching key strings
*/
public function scanPrefix(string $prefix, int $limit = 1000): array;
/**
* Get all available keys (use with caution on large datasets)
*
* @param int $limit Maximum number of keys to return (0 = no limit)
* @return array<string> Array of all key strings
*/
public function getAllKeys(int $limit = 1000): array;
/**
* Get performance characteristics of scanning for this driver
*
* @return array{
* efficient: bool,
* max_recommended_keys: int,
* estimated_time_per_1000_keys: float
* }
*/
public function getScanPerformance(): array;
}

View File

@@ -1,53 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
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
private function prefixKey(CacheKey $key): string
{
$key = $this->prefixKey($key);
return $this->prefix . (string)$key;
}
$success = false;
$value = apcu_fetch($key, $success);
if (!$success) {
return CacheItem::miss($key);
public function get(CacheKey ...$keys): CacheResult
{
if (empty($keys)) {
return CacheResult::empty();
}
return CacheItem::hit($key, $value);
$items = [];
foreach ($keys as $key) {
$prefixedKey = $this->prefixKey($key);
$success = false;
$value = apcu_fetch($prefixedKey, $success);
if ($success) {
// EMERGENCY: Check memory before creating CacheItem
$currentMemory = memory_get_usage(true);
if ($currentMemory > 400 * 1024 * 1024) { // >400MB
error_log("🚨 APCU CACHE EMERGENCY: Memory {$currentMemory} bytes - converting hit to miss for key: {$key->toString()}");
$items[] = CacheItem::miss($key);
continue;
}
// EMERGENCY: Check value size before creating CacheItem
try {
$serializedSize = strlen(serialize($value));
if ($serializedSize > 5 * 1024 * 1024) { // >5MB
error_log("🚨 APCU CACHE BLOCK: Value too large ({$serializedSize} bytes) for key: {$key->toString()}");
$items[] = CacheItem::miss($key);
continue;
}
} catch (\Throwable $e) {
error_log("🚨 APCU CACHE ERROR: Cannot serialize value for key {$key->toString()}: {$e->getMessage()}");
$items[] = CacheItem::miss($key);
continue;
}
$items[] = CacheItem::hit($key, $value);
} else {
$items[] = CacheItem::miss($key);
}
}
return CacheResult::fromItems(...$items);
}
public function set(string $key, string $value, ?int $ttl = null): bool
public function set(CacheItem ...$items): bool
{
$key = $this->prefixKey($key);
$ttl = $ttl ?? 0;
if (empty($items)) {
return true;
}
return apcu_store($key, $value, $ttl);
$success = true;
foreach ($items as $item) {
// EMERGENCY: Check value size before storing
try {
$serializedSize = strlen(serialize($item->value));
if ($serializedSize > 5 * 1024 * 1024) { // >5MB
error_log("🚨 APCU CACHE SET BLOCK: Value too large ({$serializedSize} bytes) for key: {$item->key->toString()}");
$success = false;
continue;
}
} catch (\Throwable $e) {
error_log("🚨 APCU CACHE SET ERROR: Cannot serialize value for key {$item->key->toString()}: {$e->getMessage()}");
$success = false;
continue;
}
$prefixedKey = $this->prefixKey($item->key);
$ttlSeconds = $item->ttl !== null ? $item->ttl->toCacheSeconds() : 0;
$result = apcu_store($prefixedKey, $item->value, $ttlSeconds);
$success = $success && $result;
}
return $success;
}
public function has(string $key): bool
public function has(CacheKey ...$keys): array
{
$key = $this->prefixKey($key);
return apcu_exists($key);
if (empty($keys)) {
return [];
}
$results = [];
foreach ($keys as $key) {
$prefixedKey = $this->prefixKey($key);
$keyString = (string)$key;
$results[$keyString] = apcu_exists($prefixedKey);
}
return $results;
}
public function forget(string $key): bool
public function forget(CacheKey ...$keys): bool
{
$key = $this->prefixKey($key);
return apcu_delete($key);
if (empty($keys)) {
return true;
}
$success = true;
foreach ($keys as $key) {
$prefixedKey = $this->prefixKey($key);
$result = apcu_delete($prefixedKey);
$success = $success && $result;
}
return $success;
}
public function clear(): bool

View File

@@ -1,13 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Contracts\Scannable;
use App\Framework\Filesystem\FileStorage;
use App\Framework\Filesystem\Storage;
final readonly class FileCache implements CacheDriver
final readonly class FileCache implements CacheDriver, Scannable
{
private const string CACHE_PATH = __DIR__ . '/../storage/cache';
@@ -17,32 +22,36 @@ final readonly class FileCache implements CacheDriver
$this->fileSystem->createDirectory(self::CACHE_PATH);
}
private function getFileName(string $key, ?int $expiresAt): string
private function getFileName(CacheKey $key, ?int $expiresAt): string
{
// Schütze vor Pfad/komischen Zeichen und Hash den Key
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $key);
$keyString = (string)$key;
$safeKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $keyString);
$hash = md5($safeKey);
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash .'_'. ($expiresAt ?? 0) . '.cache.php';
}
private function getFilesForKey(string $key): array
private function getFilesForKey(CacheKey $key): array
{
$hash = md5($key);
$keyString = (string)$key;
$hash = md5($keyString);
$pattern = self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '*.cache.php';
return glob($pattern) ?: [];
}
private function getLockFileName(string $key): string
private function getLockFileName(CacheKey $key): string
{
$hash = md5($key);
$keyString = (string)$key;
$hash = md5($keyString);
return self::CACHE_PATH . DIRECTORY_SEPARATOR . $hash . '.lock';
}
private function withKeyLock(string $key, callable $callback)
private function withKeyLock(CacheKey $key, callable $callback): mixed
{
$lockFile = fopen($this->getLockFileName($key), 'c');
if ($lockFile === false) {
@@ -55,6 +64,7 @@ final readonly class FileCache implements CacheDriver
if (flock($lockFile, LOCK_EX)) {
return $callback($lockFile);
}
// Lock konnte nicht gesetzt werden
return $callback(null);
} finally {
@@ -63,19 +73,19 @@ final readonly class FileCache implements CacheDriver
}
}
public function get(string $key): CacheItem
private function getSingleKey(CacheKey $key): CacheItem
{
$bestFile = null;
$bestExpires = null;
foreach($this->getFilesForKey($key) as $file) {
if (!preg_match('/_(\d+)\.cache\.php$/', $file, $m)) {
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) {
@@ -90,56 +100,95 @@ final readonly class FileCache implements CacheDriver
$content = $this->fileSystem->get($bestFile);
$data = @unserialize($content) ?: [];
if (!isset($data['value'])) {
if ($content === null || $content === '') {
$this->fileSystem->delete($bestFile);
return CacheItem::miss($key);
}
return CacheItem::hit($key, $data['value']);
return CacheItem::hit($key, $content);
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
public function get(CacheKey ...$keys): CacheResult
{
return $this->withKeyLock($key, function () use ($key, $value, $ttl) {
if (empty($keys)) {
return CacheResult::empty();
}
$expiresAt = $ttl ? (time() + $ttl) : null;
$items = [];
foreach ($keys as $key) {
$items[] = $this->getSingleKey($key);
}
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 CacheResult::fromItems(...$items);
}
public function set(CacheItem ...$items): bool
{
if (empty($items)) {
return true;
});
}
$success = true;
foreach ($items as $item) {
$result = $this->withKeyLock($item->key, function () use ($item) {
$ttlSeconds = $item->ttl !== null ? $item->ttl->toCacheSeconds() : null;
$expiresAt = $ttlSeconds ? (time() + $ttlSeconds) : null;
foreach ($this->getFilesForKey($item->key) as $file) {
$this->fileSystem->delete($file);
}
$file = $this->getFileName($item->key, $expiresAt);
// Store value directly as string (no serialization)
$this->fileSystem->put($file, $item->value);
return true;
});
$success = $success && $result;
}
return $success;
}
public function has(string $key): bool
public function has(CacheKey ...$keys): array
{
$item = $this->get($key);
return $item->isHit;
if (empty($keys)) {
return [];
}
$results = [];
foreach ($keys as $key) {
$keyString = (string)$key;
$item = $this->getSingleKey($key);
$results[$keyString] = $item->isHit;
}
return $results;
}
public function forget(string $key): bool
public function forget(CacheKey ...$keys): bool
{
return $this->withKeyLock($key, function () use ($key) {
foreach ($this->getFilesForKey($key) as $file) {
$this->fileSystem->delete($file);
}
if (empty($keys)) {
return true;
});
}
$success = true;
foreach ($keys as $key) {
$result = $this->withKeyLock($key, function () use ($key) {
foreach ($this->getFilesForKey($key) as $file) {
$this->fileSystem->delete($file);
}
return true;
});
$success = $success && $result;
}
return $success;
}
public function clear(): bool
@@ -151,4 +200,115 @@ final readonly class FileCache implements CacheDriver
return true;
}
// === Scannable Interface Implementation ===
public function scan(string $pattern, int $limit = 1000): array
{
$regex = $this->patternToRegex($pattern);
$matches = [];
$count = 0;
$files = glob(self::CACHE_PATH . '*.cache.php') ?: [];
foreach ($files as $file) {
if ($limit > 0 && $count >= $limit) {
break;
}
$key = $this->fileToKey($file);
if (preg_match($regex, $key)) {
$matches[] = $key;
$count++;
}
}
return $matches;
}
public function scanPrefix(string $prefix, int $limit = 1000): array
{
$matches = [];
$count = 0;
$files = glob(self::CACHE_PATH . '*.cache.php') ?: [];
foreach ($files as $file) {
if ($limit > 0 && $count >= $limit) {
break;
}
$key = $this->fileToKey($file);
if (str_starts_with($key, $prefix)) {
$matches[] = $key;
$count++;
}
}
return $matches;
}
public function getAllKeys(int $limit = 1000): array
{
$keys = [];
$count = 0;
$files = glob(self::CACHE_PATH . '*.cache.php') ?: [];
foreach ($files as $file) {
if ($limit > 0 && $count >= $limit) {
break;
}
$keys[] = $this->fileToKey($file);
$count++;
}
return $keys;
}
public function getScanPerformance(): array
{
return [
'efficient' => false,
'max_recommended_keys' => 1000,
'estimated_time_per_1000_keys' => 0.1, // 100ms per 1000 keys
];
}
/**
* Convert filename back to cache key
*/
private function fileToKey(string $filepath): string
{
$filename = basename($filepath, '.cache.php');
// Remove hash prefix if present
if (strpos($filename, '_') !== false) {
$parts = explode('_', $filename, 2);
if (count($parts) === 2) {
return $parts[1];
}
}
return $filename;
}
/**
* Convert wildcard pattern to regex
*/
private function patternToRegex(string $pattern): string
{
// Escape special regex characters except * and **
$escaped = preg_quote($pattern, '/');
// Replace escaped wildcards back
$escaped = str_replace('\\*\\*', '__DEEP_WILDCARD__', $escaped);
$escaped = str_replace('\\*', '__WILDCARD__', $escaped);
// Convert to regex
$regex = str_replace('__DEEP_WILDCARD__', '.*', $escaped);
$regex = str_replace('__WILDCARD__', '[^:]*', $regex);
return '/^' . $regex . '$/';
}
}

View File

@@ -1,39 +1,165 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Contracts\Scannable;
final class InMemoryCache implements CacheDriver
final class InMemoryCache implements CacheDriver, Scannable
{
private array $data = [];
public function get(string $key): CacheItem
public function get(CacheKey ...$keys): CacheResult
{
return $this->data[$key] ?? CacheItem::miss($key);
if (empty($keys)) {
return CacheResult::empty();
}
$items = [];
foreach ($keys as $key) {
$keyString = (string)$key;
if (isset($this->data[$keyString])) {
$items[] = CacheItem::hit($key, $this->data[$keyString]);
} else {
$items[] = CacheItem::miss($key);
}
}
return CacheResult::fromItems(...$items);
}
public function set(string $key, string $value, ?int $ttl = null): bool
public function set(CacheItem ...$items): bool
{
$this->data[$key] = $value;
if (empty($items)) {
return true;
}
foreach ($items as $item) {
$keyString = (string)$item->key;
$this->data[$keyString] = $item->value;
}
return true;
}
public function has(string $key): bool
public function has(CacheKey ...$keys): array
{
return isset($this->data[$key]);
if (empty($keys)) {
return [];
}
$results = [];
foreach ($keys as $key) {
$keyString = (string)$key;
$results[$keyString] = isset($this->data[$keyString]);
}
return $results;
}
public function forget(string $key): bool
public function forget(CacheKey ...$keys): bool
{
unset($this->data[$key]);
if (empty($keys)) {
return true;
}
foreach ($keys as $key) {
$keyString = (string)$key;
unset($this->data[$keyString]);
}
return true;
}
public function clear(): bool
{
unset($this->data);
$this->data = [];
return true;
}
// === Scannable Interface Implementation ===
public function scan(string $pattern, int $limit = 1000): array
{
$regex = $this->patternToRegex($pattern);
$matches = [];
$count = 0;
foreach (array_keys($this->data) as $key) {
if ($limit > 0 && $count >= $limit) {
break;
}
if (preg_match($regex, $key)) {
$matches[] = $key;
$count++;
}
}
return $matches;
}
public function scanPrefix(string $prefix, int $limit = 1000): array
{
$matches = [];
$count = 0;
foreach (array_keys($this->data) as $key) {
if ($limit > 0 && $count >= $limit) {
break;
}
if (str_starts_with($key, $prefix)) {
$matches[] = $key;
$count++;
}
}
return $matches;
}
public function getAllKeys(int $limit = 1000): array
{
$keys = array_keys($this->data);
if ($limit > 0 && count($keys) > $limit) {
return array_slice($keys, 0, $limit);
}
return $keys;
}
public function getScanPerformance(): array
{
return [
'efficient' => true,
'max_recommended_keys' => 10000,
'estimated_time_per_1000_keys' => 0.001, // 1ms per 1000 keys
];
}
/**
* Convert wildcard pattern to regex
*/
private function patternToRegex(string $pattern): string
{
// Escape special regex characters except * and **
$escaped = preg_quote($pattern, '/');
// Replace escaped wildcards back
$escaped = str_replace('\\*\\*', '__DEEP_WILDCARD__', $escaped);
$escaped = str_replace('\\*', '__WILDCARD__', $escaped);
// Convert to regex
$regex = str_replace('__DEEP_WILDCARD__', '.*', $escaped);
$regex = str_replace('__WILDCARD__', '[^:]*', $regex);
return '/^' . $regex . '$/';
}
}

View File

@@ -1,29 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Contracts\Scannable;
class NullCache implements CacheDriver
final class NullCache implements CacheDriver, Scannable
{
public function get(string $key): CacheItem
public function get(CacheKey ...$keys): CacheResult
{
return CacheItem::miss($key);
if (empty($keys)) {
return CacheResult::empty();
}
$items = array_map(fn ($key) => CacheItem::miss($key), $keys);
return CacheResult::fromItems(...$items);
}
public function set(string $key, string $value, ?int $ttl = null): bool
public function set(CacheItem ...$items): bool
{
return true;
}
public function has(string $key): bool
public function has(CacheKey ...$keys): array
{
return false;
if (empty($keys)) {
return [];
}
$results = [];
foreach ($keys as $key) {
$keyString = (string)$key;
$results[$keyString] = false;
}
return $results;
}
public function forget(string $key): bool
public function forget(CacheKey ...$keys): bool
{
return true;
}
@@ -32,4 +52,33 @@ class NullCache implements CacheDriver
{
return true;
}
// === Scannable Interface Implementation ===
public function scan(string $pattern, int $limit = 1000): array
{
// NullCache has no data, so scan returns empty array
return [];
}
public function scanPrefix(string $prefix, int $limit = 1000): array
{
// NullCache has no data, so prefix scan returns empty array
return [];
}
public function getAllKeys(int $limit = 1000): array
{
// NullCache has no keys
return [];
}
public function getScanPerformance(): array
{
return [
'efficient' => true,
'max_recommended_keys' => 0,
'estimated_time_per_1000_keys' => 0.0, // No time needed for empty results
];
}
}

View File

@@ -1,113 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Driver;
use App\Framework\Cache\CacheDriver;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CachePrefix;
use App\Framework\Cache\Serializer;
use Predis\Client as Redis;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Contracts\Scannable;
use App\Framework\Redis\RedisConnectionInterface;
use Redis;
final readonly class RedisCache implements CacheDriver
final readonly class RedisCache implements CacheDriver, Scannable
{
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 RedisConnectionInterface $connection,
private string $prefix = 'cache:'
)
) {
$this->redis = $this->connection->getClient();
}
private function prefixKey(CacheKey $key): string
{
$this->redis = new Redis(
parameters: [
'scheme' => 'tcp',
'timeout' => 1.0,
'read_write_timeout' => 1.0,
'host' => $host,
'port' => $port,
]
);
return $this->prefix . (string)$key;
}
#$this->redis->connect();
if ($password) {
$this->redis->auth($password);
public function get(CacheKey ...$keys): CacheResult
{
if (empty($keys)) {
return CacheResult::empty();
}
$this->redis->select($db);
}
private function prefixKey(string $key): string
{
return $this->prefix . $key;
}
// Use Redis MGET for batch operations
$prefixedKeys = array_map(fn ($key) => $this->prefixKey($key), $keys);
$values = $this->redis->mget($prefixedKeys);
public function get(string $key): CacheItem
{
$key = $this->prefixKey($key);
$data = $this->redis->get($key);
if ($data === null) {
return CacheItem::miss($key);
$items = [];
foreach ($keys as $index => $key) {
$value = $values[$index];
if ($value !== false) {
$items[] = CacheItem::hit($key, $value);
} else {
$items[] = 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,
);
return CacheResult::fromItems(...$items);
}
public function set(string $key, string $value, ?int $ttl = null): bool
public function set(CacheItem ...$items): 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';
if (empty($items)) {
return true;
}
return $this->redis->set($key, $payload)->getPayload() === 'OK';
// Use Redis pipeline for batch operations
$pipe = $this->redis->multi(Redis::PIPELINE);
foreach ($items as $item) {
$prefixedKey = $this->prefixKey($item->key);
if ($item->ttl !== null) {
$ttlSeconds = $item->ttl->toCacheSeconds();
$pipe->setex($prefixedKey, $ttlSeconds, $item->value);
} else {
$pipe->set($prefixedKey, $item->value);
}
}
$results = $pipe->exec();
// Check if all operations were successful
foreach ($results as $result) {
if ($result !== true) {
return false;
}
}
return true;
}
public function has(string $key): bool
public function has(CacheKey ...$keys): array
{
$key = $this->prefixKey($key);
if (empty($keys)) {
return [];
}
return $this->redis->exists($key) > 0;
// Use Redis pipeline for batch existence checks
$pipe = $this->redis->multi(Redis::PIPELINE);
foreach ($keys as $key) {
$prefixedKey = $this->prefixKey($key);
$pipe->exists($prefixedKey);
}
$results = $pipe->exec();
$hasResults = [];
foreach ($keys as $index => $key) {
$keyString = (string)$key;
$hasResults[$keyString] = ($results[$index] ?? 0) > 0;
}
return $hasResults;
}
public function forget(string $key): bool
public function forget(CacheKey ...$keys): bool
{
$key = $this->prefixKey($key);
if (empty($keys)) {
return true;
}
return $this->redis->del($key) > 0;
$prefixedKeys = array_map(fn ($key) => $this->prefixKey($key), $keys);
$deletedCount = $this->redis->del($prefixedKeys);
return $deletedCount > 0;
}
public function clear(): bool
{
try {
$patterns = array_map(
fn($prefix) => $prefix->value . '*',
fn ($prefix) => $prefix->value . '*',
CachePrefix::cases()
);
foreach($patterns as $pattern) {
foreach ($patterns as $pattern) {
$this->clearByPattern($pattern);
}
return true;
} catch (\Throwable $e) {
} catch (\Throwable $e) {
return false;
}
}
@@ -118,17 +145,97 @@ final readonly class RedisCache implements CacheDriver
$batchSize = 1000; // Batch-Größe für bessere Performance
do {
$result = $this->redis->scan($cursor, [
'MATCH' => $pattern,
'COUNT' => $batchSize
]);
$keys = $this->redis->scan($cursor, $pattern, $batchSize);
$cursor = $result[0];
$keys = $result[1];
if (!empty($keys)) {
if (! empty($keys)) {
$this->redis->del($keys);
}
} while ($cursor !== 0);
}
// === Scannable Interface Implementation ===
public function scan(string $pattern, int $limit = 1000): array
{
// Convert wildcard pattern to Redis pattern
$redisPattern = $this->wildcardToRedisPattern($pattern);
$cursor = 0;
$matches = [];
$batchSize = min(100, $limit ?: 100);
do {
$keys = $this->redis->scan($cursor, $redisPattern, $batchSize);
if (! empty($keys)) {
$matches = array_merge($matches, $keys);
// Stop if we've reached the limit
if ($limit > 0 && count($matches) >= $limit) {
$matches = array_slice($matches, 0, $limit);
break;
}
}
} while ($cursor !== 0);
return $matches;
}
public function scanPrefix(string $prefix, int $limit = 1000): array
{
// Redis SCAN with prefix pattern
$pattern = $prefix . '*';
$cursor = 0;
$matches = [];
$batchSize = min(100, $limit ?: 100);
do {
$keys = $this->redis->scan($cursor, $pattern, $batchSize);
if (! empty($keys)) {
$matches = array_merge($matches, $keys);
// Stop if we've reached the limit
if ($limit > 0 && count($matches) >= $limit) {
$matches = array_slice($matches, 0, $limit);
break;
}
}
} while ($cursor !== 0);
return $matches;
}
public function getAllKeys(int $limit = 1000): array
{
// Scan all keys with * pattern
return $this->scan('*', $limit);
}
public function getScanPerformance(): array
{
return [
'efficient' => true,
'max_recommended_keys' => 100000,
'estimated_time_per_1000_keys' => 0.01, // 10ms per 1000 keys
];
}
/**
* Convert wildcard pattern to Redis SCAN pattern
*/
private function wildcardToRedisPattern(string $pattern): string
{
// Redis supports * for any characters and ? for single character
// Our pattern uses * for single level and ** for multi-level
// Convert ** to * (Redis doesn't distinguish levels)
$redisPattern = str_replace('**', '*', $pattern);
// Redis pattern is ready to use
return $redisPattern;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Cache\Events\CacheClear;
use App\Framework\Cache\Events\CacheDelete;
use App\Framework\Cache\Events\CacheHit;
use App\Framework\Cache\Events\CacheMiss;
use App\Framework\Cache\Events\CacheSet;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Duration;
/**
* Cache-Decorator für Event-Dispatching
*
* Dieser Decorator feuert Events für alle Cache-Operationen,
* was eine lose gekoppelte Überwachung und Reaktion ermöglicht.
*/
final readonly class EventCacheDecorator implements Cache
{
public function __construct(
private Cache $innerCache,
private EventDispatcher $eventDispatcher
) {
}
public function get(CacheIdentifier ...$identifiers): CacheResult
{
$result = $this->innerCache->get(...$identifiers);
foreach ($result->getItems() as $item) {
if ($item->isHit) {
$valueSize = $this->calculateValueSize($item->value);
$this->eventDispatcher->dispatch(CacheHit::create($item->key, $item->value, $valueSize));
} else {
$this->eventDispatcher->dispatch(CacheMiss::create($item->key));
}
}
return $result;
}
public function set(CacheItem ...$items): bool
{
$result = $this->innerCache->set(...$items);
foreach ($items as $item) {
$valueSize = $this->calculateValueSize($item->value);
$this->eventDispatcher->dispatch(CacheSet::create($item->key, $item->value, $item->ttl, $result, $valueSize));
}
return $result;
}
public function has(CacheIdentifier ...$identifiers): array
{
return $this->innerCache->has(...$identifiers);
}
public function forget(CacheIdentifier ...$identifiers): bool
{
$result = $this->innerCache->forget(...$identifiers);
foreach ($identifiers as $identifier) {
if ($identifier instanceof CacheKey) {
$this->eventDispatcher->dispatch(CacheDelete::create($identifier, $result));
}
}
return $result;
}
public function clear(): bool
{
$result = $this->innerCache->clear();
$this->eventDispatcher->dispatch(CacheClear::create($result));
return $result;
}
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
// Check if already cached
$existingResult = $this->innerCache->get($key);
$existing = $existingResult->getItem($key);
if ($existing->isHit) {
$valueSize = $this->calculateValueSize($existing->value);
$this->eventDispatcher->dispatch(CacheHit::create($key, $existing->value, $valueSize));
return $existing;
}
// Cache miss - execute callback
$this->eventDispatcher->dispatch(CacheMiss::create($key));
$result = $this->innerCache->remember($key, $callback, $ttl);
if (! $result->isHit) {
$valueSize = $this->calculateValueSize($result->value);
$this->eventDispatcher->dispatch(CacheSet::create($key, $result->value, $ttl, true, $valueSize));
}
return $result;
}
/**
* Calculates the approximate size of a value
*/
private function calculateValueSize(mixed $value): int
{
if (is_string($value)) {
return strlen($value);
}
if (is_int($value) || is_float($value)) {
return 8;
}
if (is_bool($value)) {
return 1;
}
if ($value === null) {
return 0;
}
try {
return strlen(serialize($value));
} catch (\Throwable) {
return 0;
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Events;
/**
* Event fired when the entire cache is cleared
*/
final readonly class CacheClear
{
public function __construct(
public bool $success,
public float $timestamp = 0.0
) {
}
/**
* Create a new CacheClear event with the current timestamp
*/
public static function create(bool $success): self
{
return new self($success, microtime(true));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Events;
use App\Framework\Cache\CacheKey;
/**
* Event fired when a key is deleted from cache
*/
final readonly class CacheDelete
{
public function __construct(
public CacheKey $key,
public bool $success,
public float $timestamp = 0.0
) {
}
/**
* Create a new CacheDelete event with the current timestamp
*/
public static function create(CacheKey $key, bool $success): self
{
return new self($key, $success, microtime(true));
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Events;
use App\Framework\Cache\CacheKey;
/**
* Event fired when a cache hit occurs
*/
final readonly class CacheHit
{
public function __construct(
public CacheKey $key,
public mixed $value,
public int $valueSize = 0,
public float $timestamp = 0.0
) {
}
/**
* Create a new CacheHit event with the current timestamp
*/
public static function create(CacheKey $key, mixed $value, int $valueSize = 0): self
{
return new self($key, $value, $valueSize, microtime(true));
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Events;
use App\Framework\Cache\CacheKey;
/**
* Event fired when a cache miss occurs
*/
final readonly class CacheMiss
{
public function __construct(
public CacheKey $key,
public float $timestamp = 0.0
) {
}
/**
* Create a new CacheMiss event with the current timestamp
*/
public static function create(CacheKey $key): self
{
return new self($key, microtime(true));
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Events;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
/**
* Event fired when a value is set in cache
*/
final readonly class CacheSet
{
public function __construct(
public CacheKey $key,
public mixed $value,
public ?Duration $ttl,
public bool $success,
public int $valueSize = 0,
public float $timestamp = 0.0
) {
}
/**
* Create a new CacheSet event with the current timestamp
*/
public static function create(
CacheKey $key,
mixed $value,
?Duration $ttl,
bool $success,
int $valueSize = 0
): self {
return new self($key, $value, $ttl, $success, $valueSize, microtime(true));
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Filesystem\FileStorage;
@@ -10,7 +12,8 @@ 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.
@@ -32,7 +35,7 @@ final readonly class FileCacheCleaner
}
}
}
return $deleted;
}
}

View File

@@ -1,31 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
final readonly class GeneralCache implements Cache
use App\Framework\Cache\Compression\NoCompression;
use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Serializer\Serializer;
final readonly class GeneralCache implements Cache, DriverAccessible
{
private const int COMPRESSION_THRESHOLD = 1024; // 1KB threshold for compression
public function __construct(
private CacheDriver $adapter
) {}
public function get(string $key): CacheItem
{
return $this->adapter->get($key);
private CacheDriver $adapter,
private Serializer $serializer,
private CompressionAlgorithm $compressionAlgorithm = new NoCompression(),
private bool $autoCompress = true
) {
}
public function set(string $key, mixed $value, ?int $ttl = null): bool
public function get(CacheIdentifier ...$identifiers): CacheResult
{
return $this->adapter->set($key, $value, $ttl);
if (empty($identifiers)) {
return CacheResult::empty();
}
// GeneralCache only supports CacheKey identifiers
// For pattern/prefix/tag support, use SmartCache instead
$keys = [];
foreach ($identifiers as $identifier) {
if ($identifier instanceof CacheKey) {
$keys[] = $identifier;
}
// Ignore non-key identifiers - they're not supported in GeneralCache
}
if (empty($keys)) {
return CacheResult::empty();
}
// Get items from adapter
$result = $this->adapter->get(...$keys);
$items = $result->getItems();
// Apply decompression (NoCompression will be no-op)
$items = $this->decompressItems($items);
return CacheResult::fromItems(...$items);
}
public function has(string $key): bool
public function set(CacheItem ...$items): bool
{
return $this->adapter->has($key);
if (empty($items)) {
return true;
}
// EMERGENCY: Monitor cache sizes
foreach ($items as $item) {
$this->monitorCacheSize($item);
}
// Apply compression (NoCompression will be no-op)
$items = $this->compressItems($items);
// CacheDriver now accepts CacheItem objects directly
return $this->adapter->set(...$items);
}
public function forget(string $key): bool
public function has(CacheIdentifier ...$identifiers): array
{
return $this->adapter->forget($key);
if (empty($identifiers)) {
return [];
}
// GeneralCache only supports CacheKey identifiers
// For pattern/prefix/tag support, use SmartCache instead
$keys = [];
foreach ($identifiers as $identifier) {
if ($identifier instanceof CacheKey) {
$keys[] = $identifier;
}
// Ignore non-key identifiers - they're not supported in GeneralCache
}
if (empty($keys)) {
return [];
}
// Direct adapter call for key existence check
return $this->adapter->has(...$keys);
}
public function forget(CacheIdentifier ...$identifiers): bool
{
if (empty($identifiers)) {
return true;
}
// GeneralCache only supports CacheKey identifiers
// For pattern/prefix/tag support, use SmartCache instead
$keys = [];
foreach ($identifiers as $identifier) {
if ($identifier instanceof CacheKey) {
$keys[] = $identifier;
}
// Ignore non-key identifiers - they're not supported in GeneralCache
}
if (empty($keys)) {
return true;
}
// Direct adapter call for key deletion
return $this->adapter->forget(...$keys);
}
public function clear(): bool
@@ -33,16 +122,224 @@ final readonly class GeneralCache implements Cache
return $this->adapter->clear();
}
public function remember(string $key, callable $callback, int $ttl = 3600): CacheItem
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$item = $this->get($key);
$result = $this->get($key);
$item = $result->getItem($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);
$value = $callback();
$this->set(CacheItem::forSet($key, $value, $ttl));
return CacheItem::hit($key, $value, $ttl);
}
/**
* Compress cache items before storage
*
* @param array<CacheItem> $items
* @return array<CacheItem>
*/
private function compressItems(array $items): array
{
$processedItems = [];
foreach ($items as $item) {
$value = $item->value;
// Always serialize non-string values
if (! is_string($value)) {
$value = $this->serializer->serialize($value);
}
// Apply compression if beneficial (NoCompression will return unchanged)
if ($this->shouldCompress($value)) {
$value = $this->compressionAlgorithm->compress($value);
}
$processedItems[] = CacheItem::forSet($item->key, $value, $item->ttl);
}
return $processedItems;
}
/**
* Decompress cache items after retrieval
*
* @param array<CacheItem> $items
* @return array<CacheItem>
*/
private function decompressItems(array $items): array
{
$processedItems = [];
foreach ($items as $item) {
if (! $item->isHit || ! is_string($item->value)) {
$processedItems[] = $item;
continue;
}
$value = $item->value;
// Always attempt decompression (NoCompression will return unchanged)
if ($this->compressionAlgorithm->isCompressed($value)) {
$value = $this->compressionAlgorithm->decompress($value);
}
try {
// Always attempt deserialization
$unserialized = $this->serializer->deserialize($value);
$processedItems[] = CacheItem::hit($item->key, $unserialized, $item->ttl);
} catch (\Throwable $e) {
// Keep as string if deserialization fails (expected for plain strings)
$processedItems[] = CacheItem::hit($item->key, $value, $item->ttl);
}
}
return $processedItems;
}
/**
* Determine if a value should be compressed
*/
private function shouldCompress(string $value): bool
{
if (! $this->autoCompress) {
return false;
}
// Only compress if value is above threshold
return strlen($value) >= self::COMPRESSION_THRESHOLD;
}
/**
* Get the underlying cache driver (implements DriverAccessible)
*/
public function getDriver(): ?CacheDriver
{
return $this->adapter;
}
/**
* Check if the underlying driver supports a specific interface
*/
public function driverSupports(string $interface): bool
{
return $this->adapter instanceof $interface;
}
/**
* Check if compression is enabled
*/
public function hasCompressionEnabled(): bool
{
return ! ($this->compressionAlgorithm instanceof NoCompression);
}
/**
* Get compression statistics
*/
public function getCompressionStats(): array
{
return [
'compression_enabled' => $this->hasCompressionEnabled(),
'auto_compress' => $this->autoCompress,
'compression_threshold' => self::COMPRESSION_THRESHOLD,
'algorithm' => get_class($this->compressionAlgorithm),
'serializer' => get_class($this->serializer),
];
}
/**
* EMERGENCY: Monitor cache item sizes to identify memory explosion sources
*/
private function monitorCacheSize(CacheItem $item): void
{
try {
// Get serialized size to understand cache impact
$serializedValue = serialize($item->value);
$sizeBytes = strlen($serializedValue);
$sizeMB = round($sizeBytes / 1024 / 1024, 2);
// Log large cache items
if ($sizeBytes > 50 * 1024) { // >50KB for GeneralCache
$keyString = $item->key->toString();
$valueType = get_debug_type($item->value);
error_log("⚠️ GENERAL CACHE SIZE WARNING: {$sizeMB}MB");
error_log(" 📋 Cache Key: '{$keyString}'");
error_log(" 🏷️ Value Type: {$valueType}");
// If it's an object, get class name
if (is_object($item->value)) {
$className = get_class($item->value);
error_log(" 🔍 Object Class: {$className}");
}
// Show cache key pattern analysis
$keyAnalysis = $this->analyzeCacheKeyPattern($keyString);
error_log(" 🔑 Key Pattern: {$keyAnalysis}");
}
// EMERGENCY: Block extremely large cache items
if ($sizeBytes > 5 * 1024 * 1024) { // >5MB for GeneralCache (more strict)
error_log("🛑 GENERAL CACHE BLOCK: Refusing to cache {$sizeMB}MB item");
error_log(" 📋 Blocked Key: '{$keyString}'");
error_log(" 🚫 Reason: Exceeds 5MB limit");
throw new \RuntimeException("GeneralCache item too large: {$sizeMB}MB (max 5MB)");
}
} catch (\Throwable $e) {
// Don't break caching, just log the monitoring error
error_log("GeneralCache size monitoring failed: " . $e->getMessage());
}
}
/**
* Analyze cache key patterns to identify system components
*/
private function analyzeCacheKeyPattern(string $key): string
{
$patterns = [];
// Identify common patterns
if (str_contains($key, 'discovery')) {
$patterns[] = 'DISCOVERY_SYSTEM';
}
if (str_contains($key, 'reflection')) {
$patterns[] = 'REFLECTION_CACHE';
}
if (str_contains($key, 'container') || str_contains($key, 'di_')) {
$patterns[] = 'DEPENDENCY_INJECTION';
}
if (str_contains($key, 'route')) {
$patterns[] = 'ROUTING_SYSTEM';
}
if (str_contains($key, 'compiled') || str_contains($key, 'compilation')) {
$patterns[] = 'COMPILED_DATA';
}
if (str_contains($key, 'template')) {
$patterns[] = 'TEMPLATE_SYSTEM';
}
if (str_contains($key, 'meta')) {
$patterns[] = 'METADATA_SYSTEM';
}
if (empty($patterns)) {
$patterns[] = 'UNKNOWN_PATTERN';
}
return implode(', ', $patterns);
}
}

View File

@@ -4,44 +4,68 @@ declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\ValueObjects\Duration;
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
public function get(CacheIdentifier ...$identifiers): CacheResult
{
$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}");
$result = $this->innerCache->get(...$identifiers);
$hitCount = $result->getHits()->count();
$missCount = $result->getMisses()->count();
$identifierList = implode(', ', array_map(fn ($id) => $id->toString(), $identifiers));
error_log("Cache GET: [{$identifierList}] - Hits: {$hitCount}, Misses: {$missCount}");
return $result;
}
public function has(string $key): bool
public function set(CacheItem ...$items): bool
{
$exists = $this->innerCache->has($key);
$existsStr = $exists ? 'YES' : 'NO';
error_log("Cache HAS: {$key} = {$existsStr}");
return $exists;
$result = $this->innerCache->set(...$items);
$count = count($items);
$success = $result ? 'YES' : 'NO';
foreach ($items as $item) {
$valueType = $item->value === null ? 'NULL' : gettype($item->value);
$ttlStr = $item->ttl === null ? 'default' : $item->ttl->toSeconds() . 's';
error_log("Cache SET: {$item->key} = {$valueType}, TTL: {$ttlStr}");
}
error_log("Cache SET_BATCH: {$count} items, Success: {$success}");
return $result;
}
public function forget(string $key): bool
public function has(CacheIdentifier ...$identifiers): array
{
$result = $this->innerCache->forget($key);
$result = $this->innerCache->has(...$identifiers);
foreach ($result as $identifierString => $exists) {
$existsStr = $exists ? 'YES' : 'NO';
error_log("Cache HAS: {$identifierString} = {$existsStr}");
}
return $result;
}
public function forget(CacheIdentifier ...$identifiers): bool
{
$result = $this->innerCache->forget(...$identifiers);
$count = count($identifiers);
$success = $result ? 'YES' : 'NO';
error_log("Cache FORGET: {$key}, Success: {$success}");
$identifierList = implode(', ', array_map(fn ($id) => $id->toString(), $identifiers));
error_log("Cache FORGET: [{$identifierList}], Success: {$success}");
return $result;
}
@@ -50,54 +74,18 @@ final readonly class LoggingCacheDecorator implements Cache
$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
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$result = $this->innerCache->remember($key, $callback, $ttl);
$success = $result->isHit ? 'YES' : 'NO';
error_log("Cache REMEMBER: {$key}, Success: {$success}");
$status = $result->isHit ? 'HIT' : 'COMPUTED';
$ttlStr = $ttl === null ? 'default' : $ttl->toSeconds() . 's';
error_log("Cache REMEMBER: {$key} = {$status}, TTL: {$ttlStr}");
return $result;
}
}

View File

@@ -0,0 +1,353 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Metrics;
final class CacheMetrics implements CacheMetricsInterface
{
private array $stats = [
'hits' => 0,
'misses' => 0,
'sets' => 0,
'deletes' => 0,
'clears' => 0,
'total_size' => 0,
'total_latency' => 0.0,
'start_time' => 0,
'drivers' => [],
];
private array $keyStats = [];
private int $maxTrackedKeys = 1000; // Prevent memory issues
private string $persistenceFile;
private float $samplingRate = 0.1; // Only record 10% of operations to avoid huge numbers
private bool $debugMode = false;
private int $lastSaveTime = 0;
private int $saveInterval = 30; // Save every 30 seconds
public function __construct(string $persistenceFile = '/tmp/cache_metrics.json')
{
$this->persistenceFile = $persistenceFile;
$this->loadFromFile();
if ($this->stats['start_time'] === 0) {
$this->stats['start_time'] = time();
}
$this->lastSaveTime = time();
}
public function __destruct()
{
// Save metrics on shutdown
$this->saveToFile();
}
public function enableDebugMode(): void
{
$this->debugMode = true;
$this->samplingRate = 1.0; // Record everything in debug mode
}
private function shouldRecord(): bool
{
return $this->debugMode || (mt_rand() / mt_getrandmax()) < $this->samplingRate;
}
public function recordHit(string $driver, string $key, int $size = 0, float $latency = 0.0): void
{
if (! $this->shouldRecord()) {
return;
}
$this->stats['hits']++;
$this->stats['total_latency'] += $latency;
$this->initDriver($driver);
$this->stats['drivers'][$driver]['hits']++;
$this->stats['drivers'][$driver]['total_latency'] += $latency;
$this->trackKey($key, 'hit', $size);
$this->maybeSave();
}
public function recordMiss(string $driver, string $key, float $latency = 0.0): void
{
if (! $this->shouldRecord()) {
return;
}
$this->stats['misses']++;
$this->stats['total_latency'] += $latency;
$this->initDriver($driver);
$this->stats['drivers'][$driver]['misses']++;
$this->stats['drivers'][$driver]['total_latency'] += $latency;
$this->trackKey($key, 'miss', 0);
$this->maybeSave();
}
public function recordSet(string $driver, string $key, int $size, int $ttl, float $latency = 0.0): void
{
if (! $this->shouldRecord()) {
return;
}
$this->stats['sets']++;
$this->stats['total_size'] += $size;
$this->stats['total_latency'] += $latency;
$this->initDriver($driver);
$this->stats['drivers'][$driver]['sets']++;
$this->stats['drivers'][$driver]['total_size'] += $size;
$this->stats['drivers'][$driver]['total_latency'] += $latency;
$this->trackKey($key, 'set', $size, $ttl);
$this->maybeSave();
}
public function recordDelete(string $driver, string $key, float $latency = 0.0): void
{
if (! $this->shouldRecord()) {
return;
}
$this->stats['deletes']++;
$this->stats['total_latency'] += $latency;
$this->initDriver($driver);
$this->stats['drivers'][$driver]['deletes']++;
$this->stats['drivers'][$driver]['total_latency'] += $latency;
$this->trackKey($key, 'delete', 0);
$this->maybeSave();
}
public function recordClear(string $driver, float $latency = 0.0): void
{
// Always record clear operations (they are rare and important)
$this->stats['clears']++;
$this->stats['total_latency'] += $latency;
$this->initDriver($driver);
$this->stats['drivers'][$driver]['clears']++;
$this->stats['drivers'][$driver]['total_latency'] += $latency;
// Clear key tracking for this driver (simplified - clear all since we don't track driver per key)
$this->keyStats = [];
// Save immediately for clear operations as they are rare but important
$this->saveToFile();
}
public function getStats(): CacheStatsSnapshot
{
$hitRate = $this->getHitRate();
$avgLatency = $this->getAverageLatency();
$driverStats = $this->getFormattedDriverStats();
$topKeys = $this->getTopKeys();
$heaviestKeys = $this->getHeaviestKeys();
return new CacheStatsSnapshot(
hitRate: $hitRate,
totalHits: $this->stats['hits'],
totalMisses: $this->stats['misses'],
totalSets: $this->stats['sets'],
totalDeletes: $this->stats['deletes'],
totalClears: $this->stats['clears'],
driverStats: $driverStats,
topKeys: $topKeys,
heaviestKeys: $heaviestKeys,
avgLatency: $avgLatency,
totalSize: $this->stats['total_size'],
timestamp: $this->stats['start_time']
);
}
public function getStatsForDriver(string $driver): array
{
if (! isset($this->stats['drivers'][$driver])) {
return [];
}
$driverData = $this->stats['drivers'][$driver];
$totalOps = $driverData['hits'] + $driverData['misses'];
$hitRate = $totalOps > 0 ? $driverData['hits'] / $totalOps : 0.0;
$avgLatency = $totalOps > 0 ? $driverData['total_latency'] / $totalOps : 0.0;
return [
'driver' => $driver,
'hit_rate' => $hitRate,
'hits' => $driverData['hits'],
'misses' => $driverData['misses'],
'sets' => $driverData['sets'],
'deletes' => $driverData['deletes'],
'clears' => $driverData['clears'],
'total_size' => $driverData['total_size'],
'avg_latency' => $avgLatency,
];
}
public function reset(): void
{
$this->stats = [
'hits' => 0,
'misses' => 0,
'sets' => 0,
'deletes' => 0,
'clears' => 0,
'total_size' => 0,
'total_latency' => 0.0,
'start_time' => time(),
'drivers' => [],
];
$this->keyStats = [];
$this->saveToFile();
}
private function loadFromFile(): void
{
if (! file_exists($this->persistenceFile)) {
return;
}
try {
$data = json_decode(file_get_contents($this->persistenceFile), true);
if (is_array($data)) {
$this->stats = array_merge($this->stats, $data['stats'] ?? []);
$this->keyStats = $data['keyStats'] ?? [];
}
} catch (\Throwable $e) {
// If loading fails, start fresh
error_log("Failed to load cache metrics: " . $e->getMessage());
}
}
private function maybeSave(): void
{
$now = time();
if ($now - $this->lastSaveTime >= $this->saveInterval) {
$this->saveToFile();
$this->lastSaveTime = $now;
}
}
private function saveToFile(): void
{
try {
$data = [
'stats' => $this->stats,
'keyStats' => $this->keyStats,
'saved_at' => time(),
];
file_put_contents($this->persistenceFile, json_encode($data, JSON_PRETTY_PRINT));
} catch (\Throwable $e) {
// If saving fails, continue silently
error_log("Failed to save cache metrics: " . $e->getMessage());
}
}
public function getHitRate(): float
{
$total = $this->stats['hits'] + $this->stats['misses'];
return $total > 0 ? $this->stats['hits'] / $total : 0.0;
}
public function getTopKeys(int $limit = 10): array
{
// Sort by access frequency
$sorted = $this->keyStats;
uasort($sorted, fn ($a, $b) => $b['access_count'] <=> $a['access_count']);
return array_slice($sorted, 0, $limit, true);
}
public function getHeaviestKeys(int $limit = 10): array
{
// Sort by size
$sorted = $this->keyStats;
uasort($sorted, fn ($a, $b) => $b['size'] <=> $a['size']);
return array_slice($sorted, 0, $limit, true);
}
private function initDriver(string $driver): void
{
if (! isset($this->stats['drivers'][$driver])) {
$this->stats['drivers'][$driver] = [
'hits' => 0,
'misses' => 0,
'sets' => 0,
'deletes' => 0,
'clears' => 0,
'total_size' => 0,
'total_latency' => 0.0,
];
}
}
private function trackKey(string $key, string $operation, int $size, int $ttl = 0): void
{
// Prevent memory issues by limiting tracked keys
if (count($this->keyStats) >= $this->maxTrackedKeys && ! isset($this->keyStats[$key])) {
return;
}
if (! isset($this->keyStats[$key])) {
$this->keyStats[$key] = [
'access_count' => 0,
'hit_count' => 0,
'miss_count' => 0,
'size' => 0,
'last_access' => time(),
'ttl' => 0,
];
}
$this->keyStats[$key]['access_count']++;
$this->keyStats[$key]['last_access'] = time();
switch ($operation) {
case 'hit':
$this->keyStats[$key]['hit_count']++;
break;
case 'miss':
$this->keyStats[$key]['miss_count']++;
break;
case 'set':
$this->keyStats[$key]['size'] = $size;
$this->keyStats[$key]['ttl'] = $ttl;
break;
case 'delete':
unset($this->keyStats[$key]);
break;
}
}
private function getAverageLatency(): float
{
$totalOps = $this->stats['hits'] + $this->stats['misses'] + $this->stats['sets'] + $this->stats['deletes'];
return $totalOps > 0 ? $this->stats['total_latency'] / $totalOps : 0.0;
}
private function getFormattedDriverStats(): array
{
$formatted = [];
foreach ($this->stats['drivers'] as $driver => $stats) {
$formatted[$driver] = $this->getStatsForDriver($driver);
}
return $formatted;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Metrics;
interface CacheMetricsInterface
{
/**
* Record a cache hit
*/
public function recordHit(string $driver, string $key, int $size = 0, float $latency = 0.0): void;
/**
* Record a cache miss
*/
public function recordMiss(string $driver, string $key, float $latency = 0.0): void;
/**
* Record a cache set operation
*/
public function recordSet(string $driver, string $key, int $size, int $ttl, float $latency = 0.0): void;
/**
* Record a cache delete operation
*/
public function recordDelete(string $driver, string $key, float $latency = 0.0): void;
/**
* Record a cache clear operation
*/
public function recordClear(string $driver, float $latency = 0.0): void;
/**
* Get comprehensive cache statistics
*/
public function getStats(): CacheStatsSnapshot;
/**
* Get statistics for a specific driver
*/
public function getStatsForDriver(string $driver): array;
/**
* Reset all statistics
*/
public function reset(): void;
/**
* Get real-time hit rate
*/
public function getHitRate(): float;
/**
* Get most frequently accessed keys
*/
public function getTopKeys(int $limit = 10): array;
/**
* Get largest cached items
*/
public function getHeaviestKeys(int $limit = 10): array;
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Metrics;
final readonly class CacheStatsSnapshot
{
public function __construct(
public float $hitRate,
public int $totalHits,
public int $totalMisses,
public int $totalSets,
public int $totalDeletes,
public int $totalClears,
public array $driverStats,
public array $topKeys,
public array $heaviestKeys,
public float $avgLatency,
public int $totalSize,
public int $timestamp
) {
}
public function getTotalOperations(): int
{
return $this->totalHits + $this->totalMisses + $this->totalSets + $this->totalDeletes;
}
public function getEfficiencyRating(): string
{
return match (true) {
$this->hitRate >= 0.9 => 'Excellent',
$this->hitRate >= 0.8 => 'Good',
$this->hitRate >= 0.7 => 'Fair',
$this->hitRate >= 0.5 => 'Poor',
default => 'Critical'
};
}
public function calculateOpsPerSecond(): float
{
$uptime = time() - $this->timestamp;
return $uptime > 0 ? $this->getTotalOperations() / $uptime : 0.0;
}
public function toArray(): array
{
return [
'summary' => [
'hit_rate' => round($this->hitRate * 100, 2),
'hit_rate_formatted' => round($this->hitRate * 100, 2) . '%',
'efficiency' => $this->getEfficiencyRating(),
'total_operations' => $this->getTotalOperations(),
'ops_per_second' => round($this->calculateOpsPerSecond(), 2),
],
'operations' => [
'hits' => $this->totalHits,
'misses' => $this->totalMisses,
'sets' => $this->totalSets,
'deletes' => $this->totalDeletes,
'clears' => $this->totalClears,
],
'performance' => [
'avg_latency_ms' => round($this->avgLatency * 1000, 2),
'total_size_mb' => round($this->totalSize / 1024 / 1024, 2),
],
'drivers' => $this->driverStats,
'analytics' => [
'top_keys' => $this->topKeys,
'heaviest_keys' => $this->heaviestKeys,
],
'metadata' => [
'timestamp' => $this->timestamp,
'generated_at' => date('Y-m-d H:i:s', $this->timestamp),
],
];
}
public function getRecommendations(): array
{
$recommendations = [];
if ($this->hitRate < 0.7) {
$recommendations[] = [
'type' => 'warning',
'message' => 'Low cache hit rate detected. Consider increasing TTL values or reviewing cache strategy.',
'priority' => 'high',
];
}
if ($this->avgLatency > 0.01) {
$recommendations[] = [
'type' => 'warning',
'message' => 'High cache latency detected. Check cache driver performance.',
'priority' => 'medium',
];
}
if ($this->totalSize > 100 * 1024 * 1024) { // 100MB
$recommendations[] = [
'type' => 'info',
'message' => 'Large cache size detected. Consider implementing cache eviction policies.',
'priority' => 'low',
];
}
if (empty($recommendations)) {
$recommendations[] = [
'type' => 'success',
'message' => 'Cache performance is optimal.',
'priority' => 'info',
];
}
return $recommendations;
}
}

View File

@@ -0,0 +1,327 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Metrics;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheIdentifier;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
use App\Framework\Performance\PerformanceCategory;
final readonly class MetricsDecoratedCache implements Cache
{
public function __construct(
private Cache $inner,
private CacheMetricsInterface $metrics,
private string $driverName,
private ?PerformanceCollectorInterface $performanceCollector = null,
private bool $performanceEnabled = true
) {
}
public function get(CacheIdentifier ...$identifiers): CacheResult
{
if (empty($identifiers)) {
return CacheResult::empty();
}
// Performance tracking setup
$metricKey = 'cache_get_batch';
$identifierCount = count($identifiers);
$context = [
'operation' => 'get_batch',
'driver' => $this->driverName,
'identifier_count' => $identifierCount,
'identifiers' => array_map(fn ($id) => $this->sanitizeKey($id->toString()), $identifiers),
];
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->startTiming($metricKey, PerformanceCategory::CACHE, $context);
}
try {
$startTime = microtime(true);
$result = $this->inner->get(...$identifiers);
$latency = microtime(true) - $startTime;
// Record metrics for each item
foreach ($result->getItems() as $item) {
$keyString = $item->key->toString();
$size = 0;
if ($item->isHit) {
$size = $this->calculateSize($item->value);
$this->metrics->recordHit($this->driverName, $keyString, $size, $latency);
} else {
$this->metrics->recordMiss($this->driverName, $keyString, $latency);
}
}
// Record batch operation metrics
$hitCount = $result->getHits()->count();
$missCount = $result->getMisses()->count();
$hitRatio = $result->getHitRatio();
// Record batch operation metrics (if available)
if (method_exists($this->metrics, 'recordBatchOperation')) {
$this->metrics->recordBatchOperation(
$this->driverName,
'get',
$identifierCount,
$hitCount,
$missCount,
$hitRatio,
$latency
);
}
// Performance tracking for batch operation
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment('cache_hits', PerformanceCategory::CACHE, $hitCount, $context);
$this->performanceCollector->increment('cache_misses', PerformanceCategory::CACHE, $missCount, $context);
$this->performanceCollector->recordMetric('cache_hit_rate', PerformanceCategory::CACHE, $hitRatio, $context);
$this->performanceCollector->increment('cache_operations_total', PerformanceCategory::CACHE, 1, $context);
}
return $result;
} catch (\Throwable $e) {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment(
'cache_errors',
PerformanceCategory::CACHE,
1,
array_merge($context, ['error' => $e::class])
);
}
throw $e;
} finally {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->endTiming($metricKey);
}
}
}
public function set(CacheItem ...$items): bool
{
if (empty($items)) {
return true;
}
$metricKey = 'cache_set_batch';
$itemCount = count($items);
$totalSize = array_sum(array_map(fn ($item) => $this->calculateSize($item->value), $items));
$context = [
'operation' => 'set_batch',
'driver' => $this->driverName,
'item_count' => $itemCount,
'total_size' => $totalSize,
];
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->startTiming($metricKey, PerformanceCategory::CACHE, $context);
}
try {
$startTime = microtime(true);
$result = $this->inner->set(...$items);
$latency = microtime(true) - $startTime;
// Record metrics for each item
foreach ($items as $item) {
$keyString = (string)$item->key;
$valueSize = $this->calculateSize($item->value);
$ttlSeconds = $item->ttl !== null ? $item->ttl->toCacheSeconds() : 0;
if ($result) {
$this->metrics->recordSet($this->driverName, $keyString, $valueSize, $ttlSeconds, $latency);
}
}
if ($result) {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment('cache_sets_success', PerformanceCategory::CACHE, $itemCount, $context);
$this->performanceCollector->recordMetric('cache_stored_bytes', PerformanceCategory::CACHE, $totalSize, $context);
}
} else {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment('cache_sets_failed', PerformanceCategory::CACHE, $itemCount, $context);
}
}
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment('cache_operations_total', PerformanceCategory::CACHE, 1, $context);
}
return $result;
} catch (\Throwable $e) {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->increment(
'cache_errors',
PerformanceCategory::CACHE,
1,
array_merge($context, ['error' => $e::class])
);
}
throw $e;
} finally {
if ($this->performanceCollector && $this->performanceEnabled) {
$this->performanceCollector->endTiming($metricKey);
}
}
}
public function has(CacheIdentifier ...$identifiers): array
{
// Note: 'has' doesn't trigger hit/miss metrics as it's not a data access
return $this->inner->has(...$identifiers);
}
public function forget(CacheIdentifier ...$identifiers): bool
{
if (empty($identifiers)) {
return true;
}
$startTime = microtime(true);
$result = $this->inner->forget(...$identifiers);
$latency = microtime(true) - $startTime;
if ($result) {
foreach ($identifiers as $identifier) {
$keyString = $identifier->toString();
$this->metrics->recordDelete($this->driverName, $keyString, $latency);
}
}
return $result;
}
public function delete(CacheKey $key): bool
{
// Alias for forget() - some cache implementations use delete()
return $this->forget($key);
}
public function clear(): bool
{
$startTime = microtime(true);
$result = $this->inner->clear();
$latency = microtime(true) - $startTime;
if ($result) {
$this->metrics->recordClear($this->driverName, $latency);
}
return $result;
}
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
// For remember, we track both the get attempt and potential set
$startTime = microtime(true);
$item = $this->inner->remember($key, $callback, $ttl);
$totalLatency = microtime(true) - $startTime;
$keyString = (string)$key;
$ttlSeconds = $ttl !== null ? $ttl->toCacheSeconds() : 3600; // Default to 1 hour if not specified
if ($item->isHit) {
// Cache hit - only record the get operation
$size = $this->calculateSize($item->value);
$this->metrics->recordHit($this->driverName, $keyString, $size, $totalLatency);
} else {
// Cache miss + set - record both operations
$size = $this->calculateSize($item->value);
// Split latency between miss and set (rough estimate)
$missLatency = $totalLatency * 0.1; // Assume miss is 10% of total
$setLatency = $totalLatency * 0.9; // Assume set is 90% of total
$this->metrics->recordMiss($this->driverName, $keyString, $missLatency);
$this->metrics->recordSet($this->driverName, $keyString, $size, $ttlSeconds, $setLatency);
}
return $item;
}
/**
* Get the underlying cache instance (for advanced usage)
*/
public function getInner(): Cache
{
return $this->inner;
}
/**
* Get the metrics collector
*/
public function getMetrics(): CacheMetricsInterface
{
return $this->metrics;
}
/**
* Get the driver name
*/
public function getDriverName(): string
{
return $this->driverName;
}
/**
* Calculate approximate size of a value in bytes
*/
private function calculateSize(mixed $value): int
{
if ($value === null) {
return 0;
}
try {
// Use serialization to get approximate size
return strlen(serialize($value));
} catch (\Throwable $e) {
// Fallback for unserializable values
if (is_string($value)) {
return strlen($value);
}
if (is_array($value)) {
return strlen(json_encode($value));
}
if (is_object($value)) {
return strlen(json_encode($value));
}
// Rough estimate for primitives
return match (gettype($value)) {
'boolean' => 1,
'integer' => 8,
'double' => 8,
default => 16, // Fallback
};
}
}
/**
* Sanitizes cache keys for safe logging (removes potential sensitive data)
*/
private function sanitizeKey(string $key): string
{
if (strlen($key) > 100) {
return substr($key, 0, 100) . '...';
}
// Remove potential sensitive patterns
$key = preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '[EMAIL]', $key);
$key = preg_replace('/\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/', '[CARD]', $key);
return $key;
}
}

View File

@@ -1,51 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
final readonly class MultiLevelCache implements Cache
use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Core\ValueObjects\Duration;
final readonly class MultiLevelCache implements Cache, DriverAccessible
{
private const int MAX_FAST_CACHE_SIZE = 1024; //1KB
private const int FAST_CACHE_TTL = 300;
private const int FAST_CACHE_TTL_SECONDS = 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
/**
* Returns the default TTL for fast cache as a Duration object
*/
private static function getDefaultFastCacheTTL(): Duration
{
$slowSuccess = $this->slowCache->set($key, $value, $ttl);
return Duration::fromSeconds(self::FAST_CACHE_TTL_SECONDS);
}
public function get(CacheIdentifier ...$identifiers): CacheResult
{
if (empty($identifiers)) {
return CacheResult::empty();
}
// Try fast cache first
$fastResult = $this->fastCache->get(...$identifiers);
$allItems = [];
$missedIdentifiers = [];
foreach ($identifiers as $identifier) {
$item = $fastResult->getItem($identifier instanceof CacheKey ? $identifier : CacheKey::fromString($identifier->toString()));
if ($item->isHit) {
$allItems[] = $item;
} else {
$missedIdentifiers[] = $identifier;
}
}
// Fallback to slow cache for missed items
if (! empty($missedIdentifiers)) {
$slowResult = $this->slowCache->get(...$missedIdentifiers);
foreach ($slowResult->getItems() as $item) {
if ($item->isHit && $item->key instanceof CacheKey) {
// Cache in fast cache if appropriate
if ($this->shouldCacheInFast($item->value)) {
$this->fastCache->set(CacheItem::forSet($item->key, $item->value, self::getDefaultFastCacheTTL()));
}
}
$allItems[] = $item;
}
}
return CacheResult::fromItems(...$allItems);
}
public function set(CacheItem ...$items): bool
{
if (empty($items)) {
return true;
}
$slowSuccess = $this->slowCache->set(...$items);
// Also set in fast cache if appropriate
$fastItems = [];
foreach ($items as $item) {
if ($this->shouldCacheInFast($item->value)) {
$fastTtl = $item->ttl !== null ?
Duration::fromSeconds(min($item->ttl->toSeconds(), self::FAST_CACHE_TTL_SECONDS)) :
self::getDefaultFastCacheTTL();
$fastItems[] = CacheItem::forSet($item->key, $item->value, $fastTtl);
}
}
$fastSuccess = true;
if($this->shouldCacheInFast($value)) {
$fastTtl = min($ttl ?? self::FAST_CACHE_TTL, self::FAST_CACHE_TTL);
$fastSuccess = $this->fastCache->set($key, $value, $fastTtl);
if (! empty($fastItems)) {
$fastSuccess = $this->fastCache->set(...$fastItems);
}
return $slowSuccess && $fastSuccess;
}
public function forget(string $key): bool
public function forget(CacheIdentifier ...$identifiers): bool
{
$s1 = $this->fastCache->forget($key);
$s2 = $this->slowCache->forget($key);
if (empty($identifiers)) {
return true;
}
$s1 = $this->fastCache->forget(...$identifiers);
$s2 = $this->slowCache->forget(...$identifiers);
return $s1 && $s2;
}
@@ -53,37 +108,54 @@ final readonly class MultiLevelCache implements Cache
{
$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
public function has(CacheIdentifier ...$identifiers): array
{
// Schneller Check im Fast-Cache
if ($this->fastCache->has($key)) {
return true;
if (empty($identifiers)) {
return [];
}
// 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);
$fastResults = $this->fastCache->has(...$identifiers);
$results = [];
$toCheckInSlow = [];
foreach ($identifiers as $identifier) {
$keyString = $identifier->toString();
if ($fastResults[$keyString] ?? false) {
$results[$keyString] = true;
} else {
$toCheckInSlow[] = $identifier;
$results[$keyString] = false; // Default to false, will be updated if found in slow
}
return true;
}
return false;
// Check missed items in slow cache and warm up fast cache
if (! empty($toCheckInSlow)) {
$slowResults = $this->slowCache->has(...$toCheckInSlow);
foreach ($toCheckInSlow as $identifier) {
$keyString = $identifier->toString();
if ($slowResults[$keyString] ?? false) {
$results[$keyString] = true;
// Warm up fast cache if it's a key identifier
if ($identifier instanceof CacheKey) {
$slowResult = $this->slowCache->get($identifier);
$item = $slowResult->getItem($identifier);
if ($item->isHit && $this->shouldCacheInFast($item->value)) {
$this->fastCache->set(CacheItem::forSet($identifier, $item->value, self::getDefaultFastCacheTTL()));
}
}
}
}
}
return $results;
}
/*
* - **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
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$item = $this->get($key);
if ($item->isHit) {
@@ -93,9 +165,9 @@ final readonly class MultiLevelCache implements Cache
// 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
@@ -110,6 +182,7 @@ final readonly class MultiLevelCache implements Cache
$elementCount = count($value, COUNT_RECURSIVE);
// Grobe Schätzung: 50 Bytes pro Element
$estimatedSize = $elementCount * 50;
return $estimatedSize <= self::MAX_FAST_CACHE_SIZE;
}
@@ -121,4 +194,57 @@ final readonly class MultiLevelCache implements Cache
// Primitive Typen: Immer cachen
return true;
}
/**
* Get the underlying cache driver (uses slow cache as primary)
*/
public function getDriver(): ?CacheDriver
{
// Try slow cache first as it's typically the primary storage
if ($this->slowCache instanceof DriverAccessible) {
return $this->slowCache->getDriver();
}
// If slow cache doesn't have driver access, try fast cache
if ($this->fastCache instanceof DriverAccessible) {
return $this->fastCache->getDriver();
}
// Check if the cache layers are directly drivers
if ($this->slowCache instanceof CacheDriver) {
return $this->slowCache;
}
if ($this->fastCache instanceof CacheDriver) {
return $this->fastCache;
}
return null;
}
/**
* Check if the underlying driver supports a specific interface
*/
public function driverSupports(string $interface): bool
{
$driver = $this->getDriver();
return $driver !== null && $driver instanceof $interface;
}
/**
* Get the slow cache (primary storage layer)
*/
public function getSlowCache(): Cache
{
return $this->slowCache;
}
/**
* Get the fast cache (quick access layer)
*/
public function getFastCache(): Cache
{
return $this->fastCache;
}
}

View File

@@ -0,0 +1,339 @@
# Cache-System mit Decorator-Pattern
Dieses erweiterte Cache-System nutzt das **Decorator-Pattern** für maximale Flexibilität und Erweiterbarkeit. Verschiedene Cache-Features können modular kombiniert werden, ohne die Kernfunktionalität zu beeinträchtigen.
## 🏗️ Architektur
### Kernkomponenten
- **`Cache` Interface**: Basis-Interface für alle Cache-Implementierungen
- **Cache-Driver**: Konkrete Implementierungen (FileCache, RedisCache, etc.)
- **Cache-Decorators**: Erweitern die Funktionalität ohne Änderung der Basis-Implementierung
- **CacheBuilder**: Fluent API für einfache Decorator-Komposition
### Decorator-Pattern Vorteile
**Modular**: Features können einzeln hinzugefügt/entfernt werden
**Performant**: Kein Overhead durch nicht genutzte Features
**Erweiterbar**: Neue Decorators ohne Änderung bestehender Klassen
**Testbar**: Jeder Decorator kann isoliert getestet werden
## 🔧 Verfügbare Decorators
### 1. MetricsDecoratedCache
**Performance-Monitoring und Metriken**
```php
$cacheMetrics = new \App\Framework\Cache\Metrics\CacheMetrics();
$cache = new \App\Framework\Cache\Metrics\MetricsDecoratedCache(
$baseCache,
$cacheMetrics,
'MyCache',
$performanceCollector,
true
);
```
**Features:**
- Hit/Miss-Ratios
- Operation-Timing
- Speicherverbrauch-Tracking
- Fehler-Zählung
- Integration in das Framework Performance-System
**Metriken:**
- `cache_hits` / `cache_misses`
- `cache_operations_total`
- `cache_retrieved_bytes` / `cache_stored_bytes`
- `cache_remember_callback_duration`
### 2. ValidationCacheDecorator
**Sicherheit und Datenintegrität**
```php
$config = [
'max_key_length' => 250,
'max_value_size' => 1024 * 1024, // 1MB
'strict_key_validation' => true,
'forbidden_types' => ['resource'],
'reserved_prefixes' => ['__', 'system:'],
];
$cache = new ValidationCacheDecorator($baseCache, $config);
```
**Validierungsregeln:**
- Key-Länge und -Format
- Value-Größe und -Typ
- TTL-Bereiche
- Verbotene Patterns
- Reservierte Prefixes
### 3. EventCacheDecorator
**Event-basierte Architektur**
```php
$cache = new EventCacheDecorator($baseCache, $eventDispatcher);
```
**Events:**
- `CacheHit` - Bei Cache-Treffern
- `CacheMiss` - Bei Cache-Fehlern
- `CacheSet` - Beim Setzen von Werten
- `CacheDelete` - Beim Löschen von Keys
- `CacheClear` - Beim Cache-Leeren
### 4. LoggingCacheDecorator
**Detailliertes Logging**
```php
$cache = new LoggingCacheDecorator($baseCache);
```
**Log-Outputs:**
```
Cache HIT: user:123 (value: array)
Cache SET: session:abc = string, TTL: 3600, Success: YES
Cache MISS: product:456
```
### 5. CompressionCacheDecorator
**Automatische Komprimierung**
```php
$cache = new CompressionCacheDecorator(
$baseCache,
new GzipCompression(),
new PhpSerializer()
);
```
## 🚀 Verwendung
### Einfache Decorator-Komposition
```php
// Manuell
$cache = new FileCache('/path/to/cache');
$cache = new ValidationCacheDecorator($cache, $validationConfig);
$cacheMetrics = new \App\Framework\Cache\Metrics\CacheMetrics();
$cache = new \App\Framework\Cache\Metrics\MetricsDecoratedCache($cache, $cacheMetrics, 'MyCache', $performanceCollector);
$cache = new LoggingCacheDecorator($cache);
// Mit CacheBuilder (empfohlen)
$cache = CacheBuilder::create(new FileCache('/path/to/cache'))
->withValidation($validationConfig)
->withMetrics($performanceCollector)
->withEvents($eventDispatcher)
->withLogging()
->build();
```
### Vorgefertigte Konfigurationen
```php
// Für Production: Performance-optimiert
$cache = CacheBuilder::createPerformant(
new RedisCache($redisConnection),
$performanceCollector,
new GzipCompression(),
new PhpSerializer()
);
// Für Development: Vollständiges Monitoring
$cache = CacheBuilder::createDevelopment(
new FileCache('/tmp/cache'),
$performanceCollector,
$eventDispatcher,
['strict_key_validation' => true]
);
// Vollausstattung
$cache = CacheBuilder::createFull(
new RedisCache($redisConnection),
$performanceCollector,
$eventDispatcher,
new GzipCompression(),
new PhpSerializer(),
['max_value_size' => 512 * 1024] // 512KB
);
```
### Event-Handler Beispiele
```php
// Cache-Statistiken sammeln
$eventDispatcher->addListener(CacheHit::class, function(CacheHit $event) {
$this->statsCollector->recordHit($event->key, $event->valueSize);
});
// Warnung bei großen Cache-Werten
$eventDispatcher->addListener(CacheSet::class, function(CacheSet $event) {
if ($event->valueSize > 100 * 1024) { // 100KB
$this->logger->warning("Large cache value set", [
'key' => $event->key,
'size' => $event->valueSize
]);
}
});
// Cache-Miss Debugging
$eventDispatcher->addListener(CacheMiss::class, function(CacheMiss $event) {
$this->debugLogger->debug("Cache miss for key: {$event->key}");
});
```
## 🔒 Sicherheitsfeatures
### Validation-Konfiguration
```php
$secureConfig = [
// Basis-Validierung
'max_key_length' => 200,
'max_value_size' => 512 * 1024, // 512KB
'min_ttl' => 60, // Min. 1 Minute
'max_ttl' => 7 * 24 * 3600, // Max. 1 Woche
// Strikte Validierung
'strict_key_validation' => true,
'strict_object_validation' => true,
// Verbotene Elemente
'forbidden_types' => ['resource'],
'forbidden_key_patterns' => [
'/\.\./', // Directory traversal
'/\/\//', // Double slashes
'/\x00/', // Null bytes
'/[<>]/', // HTML-like chars
],
// Reservierte Prefixes
'reserved_prefixes' => [
'__',
'system:',
'internal:',
'framework:',
],
// Custom Validator
'value_validator' => function($value) {
// Keine serialisierten Objects mit kritischen Methoden
if (is_object($value)) {
$dangerousMethods = ['__destruct', '__wakeup', '__toString'];
$reflection = new ReflectionClass($value);
foreach ($dangerousMethods as $method) {
if ($reflection->hasMethod($method)) {
return false;
}
}
}
return true;
}
];
```
## 📊 Performance-Integration
Die `MetricsDecoratedCache` integriert sich nahtlos in das Framework Performance-System:
```php
// Cache-Metriken im Performance Report
Cache Category: 45.2 ms (156 calls)
- cache_hits: 142
- cache_misses: 14
- cache_operations_total: 156
- cache_retrieved_bytes: 45.2 KB
- cache_stored_bytes: 12.8 KB
```
## 🧪 Testing
### Unit Tests für Decorators
```php
class MetricsDecoratedCacheTest extends TestCase
{
public function testRecordsHitMetrics(): void
{
$mockCache = $this->createMock(Cache::class);
$collector = $this->createMock(PerformanceCollector::class);
$mockCache->method('get')->willReturn(CacheItem::hit('key', 'value'));
$collector->expects($this->once())
->method('increment')
->with('cache_hits', PerformanceCategory::CACHE, 1);
$cacheMetrics = new \App\Framework\Cache\Metrics\CacheMetrics();
$decorator = new \App\Framework\Cache\Metrics\MetricsDecoratedCache($mockCache, $cacheMetrics, 'Test', $collector);
$decorator->get('test-key');
}
}
```
## 🔄 Migration von alten Systemen
```php
// Alt: Direkte Cache-Nutzung
$cache = new FileCache('/path');
$value = $cache->get('key');
// Neu: Mit Decorators (Backward-Compatible)
$cache = CacheBuilder::create(new FileCache('/path'))
->withMetrics($performanceCollector)
->build();
$value = $cache->get('key'); // Gleiche API!
```
## 🎯 Best Practices
### 1. Decorator-Reihenfolge beachten
```php
// ✅ Richtig: Validation → Compression → Metrics → Logging
CacheBuilder::create($baseCache)
->withValidation($config) // Zuerst validieren
->withCompression($algo, $ser) // Dann komprimieren
->withMetrics($collector) // Performance messen
->withLogging() // Zuletzt loggen
->build();
// ❌ Falsch: Logging vor Validation
// Würde auch invalide Operationen loggen
```
### 2. Production vs Development
```php
// Production: Minimal, performant
if ($isProduction) {
$cache = CacheBuilder::createPerformant($baseCache, $collector, $compression, $serializer);
} else {
// Development: Vollständiges Monitoring
$cache = CacheBuilder::createDevelopment($baseCache, $collector, $eventDispatcher, $validationConfig);
}
```
### 3. Conditional Decorators
```php
$builder = CacheBuilder::create($baseCache);
if ($enableMetrics) {
$builder->withMetrics($performanceCollector);
}
if ($enableValidation) {
$builder->withValidation($validationConfig);
}
if ($debugMode) {
$builder->withLogging();
}
$cache = $builder->build();
```
---
**Das Decorator-Pattern macht das Cache-System extrem flexibel und erweiterbar, ohne Komplexität oder Performance-Einbußen für nicht genutzte Features.**

View File

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

View File

@@ -1,25 +0,0 @@
<?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

@@ -1,19 +0,0 @@
<?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,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Cache\Contracts\DriverAccessible;
use ReflectionException;
use ReflectionMethod;
final readonly class ServiceCacheDecorator implements DriverAccessible
{
public function __construct(
private object $service,
private Cache $cache
) {
}
/**
* @throws ReflectionException
*/
public function __call(string $name, array $args): mixed
{
$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);
}
/**
* Get the underlying cache driver
*/
public function getDriver(): ?CacheDriver
{
if ($this->cache instanceof DriverAccessible) {
return $this->cache->getDriver();
}
if ($this->cache instanceof CacheDriver) {
return $this->cache;
}
return null;
}
/**
* Check if the underlying driver supports a specific interface
*/
public function driverSupports(string $interface): bool
{
$driver = $this->getDriver();
return $driver !== null && $driver instanceof $interface;
}
/**
* Get the wrapped cache instance
*/
public function getCache(): Cache
{
return $this->cache;
}
}

View File

@@ -0,0 +1,966 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Async\AsyncService;
use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Cache\Contracts\Scannable;
use App\Framework\Core\ValueObjects\Duration;
/**
* Smart cache implementation that replaces AsyncAwareCache
*
* Features:
* - Integrated async functionality with intelligent batching
* - Pattern and prefix support for bulk operations
* - Automatic async/sync decision making
* - Backward compatibility with existing Cache interface
*/
final class SmartCache implements Cache, DriverAccessible
{
private const int ASYNC_THRESHOLD = 5; // Use async for 5+ operations
private const int LARGE_BATCH_THRESHOLD = 20; // Extra optimization for large batches
private readonly ?TagIndex $tagIndex;
public function __construct(
private readonly Cache $innerCache,
private readonly ?AsyncService $asyncService = null,
private readonly bool $asyncEnabled = true
) {
// Initialize tag index if we have access to a scannable driver
$driver = $this->getDriver();
$this->tagIndex = ($driver instanceof Scannable) ? new TagIndex($driver) : null;
}
public function get(CacheIdentifier ...$identifiers): CacheResult
{
if (empty($identifiers)) {
return CacheResult::empty();
}
// Separate different identifier types for optimized handling
$exactKeys = [];
$patterns = [];
$prefixes = [];
$tags = [];
foreach ($identifiers as $identifier) {
match ($identifier->getType()) {
CacheIdentifierType::KEY => $exactKeys[] = $identifier,
CacheIdentifierType::PATTERN => $patterns[] = $identifier,
CacheIdentifierType::PREFIX => $prefixes[] = $identifier,
CacheIdentifierType::TAG => $tags[] = $identifier,
};
}
$results = [];
// Handle exact keys with potential async optimization
if (! empty($exactKeys)) {
if ($this->shouldUseAsync(count($exactKeys))) {
$results[] = $this->getAsync($exactKeys);
} else {
$results[] = $this->innerCache->get(...$exactKeys);
}
}
// Handle patterns - these require special processing
foreach ($patterns as $pattern) {
$results[] = $this->getByPattern($pattern);
}
// Handle prefixes - these require special processing
foreach ($prefixes as $prefix) {
$results[] = $this->getByPrefix($prefix);
}
// Handle tags - these require special processing
foreach ($tags as $tag) {
$results[] = $this->getByTag($tag);
}
// Merge all results
return $this->mergeResults(...$results);
}
public function set(CacheItem ...$items): bool
{
if (empty($items)) {
return true;
}
// EMERGENCY: Monitor cache item sizes to identify memory issues
foreach ($items as $item) {
$this->monitorCacheItemSize($item);
}
// Use async for large batches
if ($this->shouldUseAsync(count($items))) {
return $this->setAsync($items);
}
return $this->innerCache->set(...$items);
}
public function has(CacheIdentifier ...$identifiers): array
{
if (empty($identifiers)) {
return [];
}
// Separate identifier types
$exactKeys = [];
$patterns = [];
$prefixes = [];
$tags = [];
foreach ($identifiers as $identifier) {
match ($identifier->getType()) {
CacheIdentifierType::KEY => $exactKeys[] = $identifier,
CacheIdentifierType::PATTERN => $patterns[] = $identifier,
CacheIdentifierType::PREFIX => $prefixes[] = $identifier,
CacheIdentifierType::TAG => $tags[] = $identifier,
};
}
$results = [];
// Handle exact keys
if (! empty($exactKeys)) {
if ($this->shouldUseAsync(count($exactKeys))) {
$keyResults = $this->hasAsync($exactKeys);
} else {
$keyResults = $this->innerCache->has(...$exactKeys);
}
$results = array_merge($results, $keyResults);
}
// Handle patterns
foreach ($patterns as $pattern) {
$patternResults = $this->hasByPattern($pattern);
$results = array_merge($results, $patternResults);
}
// Handle prefixes
foreach ($prefixes as $prefix) {
$prefixResults = $this->hasByPrefix($prefix);
$results = array_merge($results, $prefixResults);
}
// Handle tags
foreach ($tags as $tag) {
$tagResults = $this->hasByTag($tag);
$results = array_merge($results, $tagResults);
}
return $results;
}
public function forget(CacheIdentifier ...$identifiers): bool
{
if (empty($identifiers)) {
return true;
}
// Separate identifier types
$exactKeys = [];
$patterns = [];
$prefixes = [];
$tags = [];
foreach ($identifiers as $identifier) {
match ($identifier->getType()) {
CacheIdentifierType::KEY => $exactKeys[] = $identifier,
CacheIdentifierType::PATTERN => $patterns[] = $identifier,
CacheIdentifierType::PREFIX => $prefixes[] = $identifier,
CacheIdentifierType::TAG => $tags[] = $identifier,
};
}
$success = true;
// Handle exact keys
if (! empty($exactKeys)) {
if ($this->shouldUseAsync(count($exactKeys))) {
$success &= $this->forgetAsync($exactKeys);
} else {
$success &= $this->innerCache->forget(...$exactKeys);
}
}
// Handle patterns
foreach ($patterns as $pattern) {
$deletedCount = $this->forgetByPattern($pattern);
$success = $success && ($deletedCount >= 0); // Success if no error occurred
}
// Handle prefixes
foreach ($prefixes as $prefix) {
$deletedCount = $this->forgetByPrefix($prefix);
$success = $success && ($deletedCount >= 0); // Success if no error occurred
}
// Handle tags
foreach ($tags as $tag) {
$deletedCount = $this->forgetByTag($tag);
$success = $success && ($deletedCount >= 0); // Success if no error occurred
}
return (bool) $success;
}
public function clear(): bool
{
return $this->innerCache->clear();
}
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
return $this->innerCache->remember($key, $callback, $ttl);
}
/**
* Bulk remember operation with intelligent async processing
*
* @param array<CacheKey, callable> $keyCallbackPairs
* @return array<string, CacheItem>
*/
public function rememberMultiple(array $keyCallbackPairs, ?Duration $ttl = null): array
{
if (empty($keyCallbackPairs)) {
return [];
}
// Check existing cache entries first
$keys = array_keys($keyCallbackPairs);
$existingResults = $this->innerCache->get(...$keys);
$results = [];
$toCompute = [];
// Separate cache hits from misses
foreach ($keyCallbackPairs as $key => $callback) {
$item = $existingResults->getItem($key);
if ($item->isHit) {
$results[$key->toString()] = $item;
} else {
$toCompute[$key->toString()] = ['key' => $key, 'callback' => $callback];
}
}
// Compute missing values with async if beneficial
if (! empty($toCompute)) {
if ($this->shouldUseAsync(count($toCompute))) {
$computed = $this->computeAsync($toCompute, $ttl);
} else {
$computed = $this->computeSync($toCompute, $ttl);
}
$results = array_merge($results, $computed);
}
return $results;
}
/**
* Determine if async processing should be used
*/
private function shouldUseAsync(int $operationCount): bool
{
return $this->asyncEnabled &&
$this->asyncService !== null &&
$operationCount >= self::ASYNC_THRESHOLD;
}
/**
* Async get operation with intelligent batching
*/
private function getAsync(array $keys): CacheResult
{
if (! $this->asyncService) {
return $this->innerCache->get(...$keys);
}
// For very large batches, split into parallel chunks
if (count($keys) >= self::LARGE_BATCH_THRESHOLD) {
$chunkSize = max(5, intval(count($keys) / 4)); // 4 parallel operations
$chunks = array_chunk($keys, $chunkSize);
$operations = [];
foreach ($chunks as $index => $chunk) {
$operations["chunk_{$index}"] = fn () => $this->innerCache->get(...$chunk);
}
$results = $this->asyncService->parallel($operations)->await();
return $this->mergeResults(...array_values($results));
}
// For medium batches, just delegate to inner cache
return $this->innerCache->get(...$keys);
}
/**
* Async set operation with intelligent batching
*/
private function setAsync(array $items): bool
{
if (! $this->asyncService) {
return $this->innerCache->set(...$items);
}
// For very large batches, split into parallel chunks
if (count($items) >= self::LARGE_BATCH_THRESHOLD) {
$chunkSize = max(5, intval(count($items) / 4)); // 4 parallel operations
$chunks = array_chunk($items, $chunkSize);
$operations = [];
foreach ($chunks as $index => $chunk) {
$operations["chunk_{$index}"] = fn () => $this->innerCache->set(...$chunk);
}
$results = $this->asyncService->parallel($operations)->await();
return ! in_array(false, $results, true);
}
// For medium batches, just delegate to inner cache
return $this->innerCache->set(...$items);
}
/**
* Async has operation
*/
private function hasAsync(array $keys): array
{
if (! $this->asyncService) {
return $this->innerCache->has(...$keys);
}
// Similar chunking logic as getAsync
if (count($keys) >= self::LARGE_BATCH_THRESHOLD) {
$chunkSize = max(5, intval(count($keys) / 4));
$chunks = array_chunk($keys, $chunkSize);
$operations = [];
foreach ($chunks as $index => $chunk) {
$operations["chunk_{$index}"] = fn () => $this->innerCache->has(...$chunk);
}
$results = $this->asyncService->parallel($operations)->await();
// Merge has results
$merged = [];
foreach ($results as $chunkResults) {
$merged = array_merge($merged, $chunkResults);
}
return $merged;
}
return $this->innerCache->has(...$keys);
}
/**
* Async forget operation
*/
private function forgetAsync(array $keys): bool
{
if (! $this->asyncService) {
return $this->innerCache->forget(...$keys);
}
// Similar chunking logic
if (count($keys) >= self::LARGE_BATCH_THRESHOLD) {
$chunkSize = max(5, intval(count($keys) / 4));
$chunks = array_chunk($keys, $chunkSize);
$operations = [];
foreach ($chunks as $index => $chunk) {
$operations["chunk_{$index}"] = fn () => $this->innerCache->forget(...$chunk);
}
$results = $this->asyncService->parallel($operations)->await();
return ! in_array(false, $results, true);
}
return $this->innerCache->forget(...$keys);
}
/**
* Get items by pattern using scanning capability
*/
private function getByPattern(CachePattern $pattern): CacheResult
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
// Fallback: return empty result for non-scannable drivers
return CacheResult::empty();
}
try {
// Get matching keys from the driver
$matchingKeys = $cacheDriver->scan($pattern->pattern, 1000);
if (empty($matchingKeys)) {
return CacheResult::empty();
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
// Batch load all matching keys
return $cacheDriver->get(...$cacheKeys);
} catch (\Throwable $e) {
// On any error, return empty result
return CacheResult::empty();
}
}
/**
* Get items by prefix using scanning capability
*/
private function getByPrefix(CachePrefix $prefix): CacheResult
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
// Fallback: return empty result for non-scannable drivers
return CacheResult::empty();
}
try {
// Get matching keys by prefix (more efficient than pattern matching)
$matchingKeys = $cacheDriver->scanPrefix($prefix->value, 1000);
if (empty($matchingKeys)) {
return CacheResult::empty();
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
// Batch load all matching keys
return $cacheDriver->get(...$cacheKeys);
} catch (\Throwable $e) {
// On any error, return empty result
return CacheResult::empty();
}
}
/**
* Get items by tag using tag index system
*/
private function getByTag(CacheTag $tag): CacheResult
{
if (! $this->tagIndex) {
return CacheResult::empty();
}
try {
// Get keys associated with this tag
$keyStrings = $this->tagIndex->getKeysForTag($tag);
if (empty($keyStrings)) {
return CacheResult::empty();
}
// Convert to CacheKey objects and batch load
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
return $this->innerCache->get(...$cacheKeys);
} catch (\Throwable $e) {
return CacheResult::empty();
}
}
/**
* Check existence by pattern using scanning capability
*/
private function hasByPattern(CachePattern $pattern): array
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
return [$pattern->toString() => false];
}
try {
// Get matching keys from the driver
$matchingKeys = $cacheDriver->scan($pattern->pattern, 1000);
$results = [];
foreach ($matchingKeys as $key) {
$results[$key] = true; // All returned keys exist by definition
}
return $results;
} catch (\Throwable $e) {
return [$pattern->toString() => false];
}
}
/**
* Check existence by prefix using scanning capability
*/
private function hasByPrefix(CachePrefix $prefix): array
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
return [$prefix->toString() => false];
}
try {
// Get matching keys by prefix
$matchingKeys = $cacheDriver->scanPrefix($prefix->value, 1000);
$results = [];
foreach ($matchingKeys as $key) {
$results[$key] = true; // All returned keys exist by definition
}
return $results;
} catch (\Throwable $e) {
return [$prefix->toString() => false];
}
}
/**
* Check existence by tag using tag index system
*/
private function hasByTag(CacheTag $tag): array
{
if (! $this->tagIndex) {
return [$tag->toString() => false];
}
try {
// Get keys associated with this tag
$keyStrings = $this->tagIndex->getKeysForTag($tag);
if (empty($keyStrings)) {
return [];
}
// Convert to CacheKey objects and check existence
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
return $this->innerCache->has(...$cacheKeys);
} catch (\Throwable $e) {
return [$tag->toString() => false];
}
}
/**
* Forget items by pattern using scanning capability
*/
private function forgetByPattern(CachePattern $pattern): int
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
// Fallback: cannot delete without scanning capability
return false;
}
try {
// Get matching keys from the driver
$matchingKeys = $cacheDriver->scan($pattern->pattern, 1000);
if (empty($matchingKeys)) {
return 0; // Nothing to delete
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
// Batch delete all matching keys
$cacheDriver->forget(...$cacheKeys);
return count($matchingKeys);
} catch (\Throwable $e) {
return 0;
}
}
/**
* Forget items by prefix using scanning capability
*/
private function forgetByPrefix(CachePrefix $prefix): int
{
$cacheDriver = $this->getCacheDriver();
// Check if the underlying driver supports scanning
if (! $cacheDriver || ! ($cacheDriver instanceof Scannable)) {
// Fallback: cannot delete without scanning capability
return 0;
}
try {
// Get matching keys by prefix
$matchingKeys = $cacheDriver->scanPrefix($prefix->value, 1000);
if (empty($matchingKeys)) {
return 0; // Nothing to delete
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
// Batch delete all matching keys
$cacheDriver->forget(...$cacheKeys);
return count($matchingKeys);
} catch (\Throwable $e) {
return 0;
}
}
/**
* Forget items by tag using tag index system
*/
private function forgetByTag(CacheTag $tag): int
{
if (! $this->tagIndex) {
return 0;
}
try {
// Get keys associated with this tag
$keyStrings = $this->tagIndex->getKeysForTag($tag);
if (empty($keyStrings)) {
return 0; // Nothing to delete
}
// Convert to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
// Delete from cache and remove from tag index
$this->innerCache->forget(...$cacheKeys);
// Remove keys from tag index
foreach ($cacheKeys as $key) {
$this->tagIndex->untagKey($key, $tag);
}
return count($keyStrings);
} catch (\Throwable $e) {
return 0;
}
}
/**
* Merge multiple CacheResult objects efficiently
*/
private function mergeResults(CacheResult ...$results): CacheResult
{
if (empty($results)) {
return CacheResult::empty();
}
if (count($results) === 1) {
return $results[0];
}
$allItems = [];
foreach ($results as $result) {
foreach ($result->getItems() as $item) {
$allItems[] = $item;
}
}
return CacheResult::fromItems(...$allItems);
}
/**
* Compute values synchronously
*/
private function computeSync(array $toCompute, ?Duration $ttl): array
{
$results = [];
foreach ($toCompute as $keyString => $data) {
$key = $data['key'];
$callback = $data['callback'];
$value = $callback();
$item = CacheItem::forSet($key, $value, $ttl);
$this->innerCache->set($item);
$results[$keyString] = CacheItem::hit($key, $value);
}
return $results;
}
/**
* Compute values asynchronously
*/
private function computeAsync(array $toCompute, ?Duration $ttl): array
{
if (! $this->asyncService) {
return $this->computeSync($toCompute, $ttl);
}
$operations = [];
foreach ($toCompute as $keyString => $data) {
$operations[$keyString] = function () use ($data, $ttl) {
$key = $data['key'];
$callback = $data['callback'];
$value = $callback();
$item = CacheItem::forSet($key, $value, $ttl);
$this->innerCache->set($item);
return CacheItem::hit($key, $value);
};
}
return $this->asyncService->parallel($operations)->await();
}
/**
* Get comprehensive stats including smart cache metrics
*/
public function getStats(): array
{
$baseStats = method_exists($this->innerCache, 'getStats') ? $this->innerCache->getStats() : [];
$smartStats = [
'cache_type' => 'SmartCache',
'async_enabled' => $this->asyncEnabled,
'async_available' => $this->asyncService !== null,
'async_threshold' => self::ASYNC_THRESHOLD,
'large_batch_threshold' => self::LARGE_BATCH_THRESHOLD,
'pattern_support' => $this->driverSupports(Scannable::class),
'prefix_support' => $this->driverSupports(Scannable::class),
'tag_support' => $this->tagIndex !== null,
'intelligent_batching' => true,
];
// Add tag index statistics if available
if ($this->tagIndex) {
$smartStats['tag_stats'] = $this->tagIndex->getStats();
}
return array_merge($baseStats, $smartStats);
}
/**
* Get the wrapped cache instance for advanced usage
*/
public function getWrappedCache(): Cache
{
return $this->innerCache;
}
/**
* Get the underlying cache driver using the DriverAccessible interface
*/
private function getCacheDriver(): ?CacheDriver
{
// Use DriverAccessible interface if available
if ($this->innerCache instanceof DriverAccessible) {
return $this->innerCache->getDriver();
}
// If innerCache is directly a driver
if ($this->innerCache instanceof CacheDriver) {
return $this->innerCache;
}
return null;
}
/**
* Get the underlying cache driver (implements DriverAccessible)
*/
public function getDriver(): ?CacheDriver
{
return $this->getCacheDriver();
}
/**
* Check if the underlying driver supports a specific interface
*/
public function driverSupports(string $interface): bool
{
$driver = $this->getDriver();
return $driver !== null && $driver instanceof $interface;
}
/**
* Get the tag index (if available)
*/
public function getTagIndex(): ?TagIndex
{
return $this->tagIndex;
}
/**
* Tag a cache item with one or more tags
*/
public function tag(CacheKey $key, CacheTag ...$tags): bool
{
if (! $this->tagIndex || empty($tags)) {
return false;
}
return $this->tagIndex->tagKey($key, ...$tags);
}
/**
* Remove tags from a cache item
*/
public function untag(CacheKey $key, CacheTag ...$tags): bool
{
if (! $this->tagIndex) {
return false;
}
return $this->tagIndex->untagKey($key, ...$tags);
}
/**
* EMERGENCY: Monitor cache item sizes to identify memory explosion sources
*/
private function monitorCacheItemSize(CacheItem $item): void
{
try {
// Get serialized size to understand cache impact
$serializedValue = serialize($item->value);
$sizeBytes = strlen($serializedValue);
$sizeMB = round($sizeBytes / 1024 / 1024, 2);
// Log large cache items
if ($sizeBytes > 100 * 1024) { // >100KB
$keyString = $item->key->toString();
$valueType = get_debug_type($item->value);
error_log("🚨 SMART CACHE SIZE WARNING: {$sizeMB}MB");
error_log(" 📋 Cache Key: '{$keyString}'");
error_log(" 🏷️ Value Type: {$valueType}");
// If it's an object, get class name and some properties
if (is_object($item->value)) {
$className = get_class($item->value);
$objectInfo = $this->analyzeObject($item->value);
error_log(" 🔍 Object Class: {$className}");
error_log(" 📊 Object Analysis: {$objectInfo}");
}
// Show cache key pattern analysis
$keyAnalysis = $this->analyzeCacheKey($keyString);
error_log(" 🔑 Key Pattern: {$keyAnalysis}");
}
// EMERGENCY: Block extremely large cache items
if ($sizeBytes > 10 * 1024 * 1024) { // >10MB
error_log("🛑 SMART CACHE BLOCK: Refusing to cache {$sizeMB}MB item");
error_log(" 📋 Blocked Key: '{$keyString}'");
error_log(" 🚫 Reason: Exceeds 10MB limit");
throw new \RuntimeException("Cache item too large: {$sizeMB}MB (max 10MB)");
}
} catch (\Throwable $e) {
// Don't break caching, just log the monitoring error
error_log("Cache size monitoring failed: " . $e->getMessage());
}
}
/**
* Analyze object properties to identify memory usage patterns
*/
private function analyzeObject(object $obj): string
{
$info = [];
// Get basic object info
$reflection = new \ReflectionClass($obj);
$info[] = "Properties: " . count($reflection->getProperties());
// Check for common memory-heavy patterns
if ($reflection->hasProperty('cache') || $reflection->hasProperty('container')) {
$info[] = "HAS_HEAVY_DEPENDENCIES";
}
if ($reflection->hasProperty('data') || $reflection->hasProperty('items')) {
$info[] = "HAS_DATA_COLLECTIONS";
}
if (method_exists($obj, '__serialize')) {
$info[] = "CUSTOM_SERIALIZATION";
}
return implode(', ', $info);
}
/**
* Analyze cache key patterns to identify system components
*/
private function analyzeCacheKey(string $key): string
{
$patterns = [];
// Identify common patterns
if (str_contains($key, 'discovery')) {
$patterns[] = 'DISCOVERY_SYSTEM';
}
if (str_contains($key, 'reflection')) {
$patterns[] = 'REFLECTION_CACHE';
}
if (str_contains($key, 'container') || str_contains($key, 'di_')) {
$patterns[] = 'DEPENDENCY_INJECTION';
}
if (str_contains($key, 'route')) {
$patterns[] = 'ROUTING_SYSTEM';
}
if (str_contains($key, 'template')) {
$patterns[] = 'TEMPLATE_SYSTEM';
}
if (str_contains($key, 'meta') || str_contains($key, 'metadata')) {
$patterns[] = 'METADATA_SYSTEM';
}
if (str_contains($key, 'compiled')) {
$patterns[] = 'COMPILED_DATA';
}
if (str_contains($key, 'unified_')) {
$patterns[] = 'UNIFIED_CACHE';
}
if (empty($patterns)) {
$patterns[] = 'UNKNOWN_PATTERN';
}
return implode(', ', $patterns);
}
}

View File

@@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Cache\Contracts\Scannable;
/**
* Tag index system for cache tag management
*
* Manages the mapping between cache tags and their associated keys.
* Uses a separate namespace in the cache to store tag-to-key mappings.
*/
final class TagIndex
{
private const string TAG_INDEX_PREFIX = '__tag_index:';
private const string KEY_TAG_PREFIX = '__key_tags:';
public function __construct(
private readonly CacheDriver $driver
) {
}
/**
* Add a key to one or more tags
*/
public function tagKey(CacheKey $key, CacheTag ...$tags): bool
{
if (empty($tags)) {
return true;
}
$keyString = (string)$key;
$success = true;
// Add key to each tag's index
foreach ($tags as $tag) {
$tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());
// Get existing keys for this tag
$existingKeys = $this->getKeysForTag($tag);
if (! in_array($keyString, $existingKeys, true)) {
$existingKeys[] = $keyString;
// Store updated key list for tag
$tagItem = CacheItem::forSet($tagIndexKey, json_encode($existingKeys));
$success &= $this->driver->set($tagItem);
}
}
// Store tags for this key (for cleanup purposes)
$keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . $keyString);
$tagValues = array_map(fn ($tag) => $tag->toString(), $tags);
$keyTagsItem = CacheItem::forSet($keyTagsKey, json_encode($tagValues));
$success &= $this->driver->set($keyTagsItem);
return (bool)$success;
}
/**
* Remove a key from one or more tags
*/
public function untagKey(CacheKey $key, CacheTag ...$tags): bool
{
if (empty($tags)) {
return $this->removeKeyFromAllTags($key);
}
$keyString = (string)$key;
$success = true;
// Remove key from each specified tag
foreach ($tags as $tag) {
$tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());
$existingKeys = $this->getKeysForTag($tag);
$filteredKeys = array_filter($existingKeys, fn ($k) => $k !== $keyString);
if (count($filteredKeys) !== count($existingKeys)) {
if (empty($filteredKeys)) {
// No keys left for this tag, remove the tag index
$success &= $this->driver->forget($tagIndexKey);
} else {
// Update the tag index
$tagItem = CacheItem::forSet($tagIndexKey, json_encode(array_values($filteredKeys)));
$success &= $this->driver->set($tagItem);
}
}
}
// Update the key's tag list
$this->updateKeyTags($key, $tags, false);
return $success;
}
/**
* Get all keys associated with a tag
*/
public function getKeysForTag(CacheTag $tag): array
{
$tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());
$result = $this->driver->get($tagIndexKey);
$item = $result->getItem($tagIndexKey);
if (! $item->isHit) {
return [];
}
$decoded = json_decode($item->value, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Get all tags associated with a key
*/
public function getTagsForKey(CacheKey $key): array
{
$keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);
$result = $this->driver->get($keyTagsKey);
$item = $result->getItem($keyTagsKey);
if (! $item->isHit) {
return [];
}
$decoded = json_decode($item->value, true);
if (! is_array($decoded)) {
return [];
}
return array_map(fn ($tagValue) => CacheTag::fromString($tagValue), $decoded);
}
/**
* Remove a key from all its tags (used when key is deleted)
*/
public function removeKeyFromAllTags(CacheKey $key): bool
{
$tags = $this->getTagsForKey($key);
if (empty($tags)) {
return true;
}
$success = $this->untagKey($key, ...$tags);
// Remove the key's tag list
$keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);
$success &= $this->driver->forget($keyTagsKey);
return (bool)$success;
}
/**
* Get all available tags
*/
public function getAllTags(): array
{
if (! ($this->driver instanceof Scannable)) {
return [];
}
try {
$tagKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);
return array_map(function ($tagKey) {
$tagValue = substr($tagKey, strlen(self::TAG_INDEX_PREFIX));
return CacheTag::fromString($tagValue);
}, $tagKeys);
} catch (\Throwable $e) {
return [];
}
}
/**
* Clear all tags and their indexes
*/
public function clearAllTags(): bool
{
if (! ($this->driver instanceof Scannable)) {
return false;
}
try {
// Clear tag indexes
$tagIndexKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);
$tagCacheKeys = array_map(fn ($key) => CacheKey::fromString($key), $tagIndexKeys);
// Clear key-tag mappings
$keyTagKeys = $this->driver->scanPrefix(self::KEY_TAG_PREFIX, 1000);
$keyTagCacheKeys = array_map(fn ($key) => CacheKey::fromString($key), $keyTagKeys);
$allKeys = array_merge($tagCacheKeys, $keyTagCacheKeys);
if (empty($allKeys)) {
return true;
}
return $this->driver->forget(...$allKeys);
} catch (\Throwable $e) {
return false;
}
}
/**
* Get statistics about the tag system
*/
public function getStats(): array
{
$stats = [
'total_tags' => 0,
'total_tagged_keys' => 0,
'scannable_driver' => $this->driver instanceof Scannable,
];
if ($this->driver instanceof Scannable) {
try {
$tagKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);
$stats['total_tags'] = count($tagKeys);
$keyTagKeys = $this->driver->scanPrefix(self::KEY_TAG_PREFIX, 1000);
$stats['total_tagged_keys'] = count($keyTagKeys);
} catch (\Throwable $e) {
$stats['error'] = $e->getMessage();
}
}
return $stats;
}
/**
* Update the tag list for a key
*/
private function updateKeyTags(CacheKey $key, array $tags, bool $add): void
{
$existingTags = $this->getTagsForKey($key);
$existingTagValues = array_map(fn ($tag) => $tag->value, $existingTags);
$newTagValues = array_map(fn ($tag) => $tag->value, $tags);
if ($add) {
$updatedTagValues = array_unique(array_merge($existingTagValues, $newTagValues));
} else {
$updatedTagValues = array_diff($existingTagValues, $newTagValues);
}
$keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);
if (empty($updatedTagValues)) {
$this->driver->forget($keyTagsKey);
} else {
$keyTagsItem = CacheItem::forSet($keyTagsKey, json_encode(array_values($updatedTagValues)));
$this->driver->set($keyTagsItem);
}
}
}

View File

@@ -1,11 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\ValueObjects\Duration;
/**
* Tagged cache utility for easy cache tagging operations
*
* Provides a fluent API for working with cache tags on top of SmartCache.
* This is a convenience wrapper that makes tag operations more intuitive.
*/
final readonly class TaggedCache
{
public function __construct(
public Cache $cache
) {}
private SmartCache $cache,
private array $tags = []
) {
}
/**
* Create a new TaggedCache instance with additional tags
*/
public function tags(string|CacheTag ...$tags): self
{
$cacheTags = array_map(fn ($tag) => $tag instanceof CacheTag ? $tag : CacheTag::from($tag), $tags);
$allTags = array_merge($this->tags, $cacheTags);
return new self($this->cache, $allTags);
}
/**
* Store a value with the current tags
*/
public function put(string|CacheKey $key, mixed $value, ?Duration $ttl = null): bool
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$item = CacheItem::forSet($cacheKey, $value, $ttl);
// Store the item
$success = $this->cache->set($item);
// Tag the item if successful and we have tags
if ($success && ! empty($this->tags)) {
$this->cache->tag($cacheKey, ...$this->tags);
}
return $success;
}
/**
* Get a value (no automatic tagging for retrieval)
*/
public function get(string|CacheKey $key): CacheItem
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$result = $this->cache->get($cacheKey);
return $result->getItem($cacheKey);
}
/**
* Remember a value with the current tags
*/
public function remember(string|CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$item = $this->get($cacheKey);
if ($item->isHit) {
return $item;
}
// Compute value and store with tags
$value = $callback();
$this->put($cacheKey, $value, $ttl);
return CacheItem::hit($cacheKey, $value);
}
/**
* Flush all items with any of the current tags
*/
public function flush(): bool
{
if (empty($this->tags)) {
return true;
}
$success = true;
foreach ($this->tags as $tag) {
$success &= $this->cache->forget($tag);
}
return $success;
}
/**
* Get all items tagged with any of the current tags
*/
public function all(): CacheResult
{
if (empty($this->tags)) {
return CacheResult::empty();
}
return $this->cache->get(...$this->tags);
}
/**
* Check if items with the current tags exist
*/
public function exists(): bool
{
if (empty($this->tags)) {
return false;
}
$results = $this->cache->has(...$this->tags);
// Return true if any tagged item exists
return in_array(true, $results, true);
}
/**
* Get statistics for the current tags
*/
public function getTagStats(): array
{
$tagIndex = $this->cache->getTagIndex();
if (! $tagIndex) {
return ['error' => 'Tag index not available'];
}
$stats = [];
foreach ($this->tags as $tag) {
$keys = $tagIndex->getKeysForTag($tag);
$stats[$tag->value] = [
'tag' => $tag->value,
'key_count' => count($keys),
'keys' => $keys,
];
}
return $stats;
}
/**
* Get the underlying SmartCache instance
*/
public function getCache(): SmartCache
{
return $this->cache;
}
/**
* Get the current tags
*/
public function getTags(): array
{
return $this->tags;
}
/**
* Create a tagged cache instance from a regular cache
*/
public static function make(SmartCache $cache): self
{
return new self($cache);
}
}

View File

@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache;
use App\Framework\Core\ValueObjects\Duration;
use InvalidArgumentException;
/**
* Cache-Decorator für Validierung von Keys und Values
*
* Dieser Decorator stellt sicher, dass Cache-Operationen nur mit validen
* Daten durchgeführt werden und kann Sicherheitsrichtlinien durchsetzen.
*/
final readonly class ValidationCacheDecorator implements Cache
{
public function __construct(
private Cache $innerCache,
private array $config = []
) {
}
public function get(CacheIdentifier ...$identifiers): CacheResult
{
// CacheIdentifiers are already validated by their constructors
return $this->innerCache->get(...$identifiers);
}
public function set(CacheItem ...$items): bool
{
foreach ($items as $item) {
// CacheKey is already validated by its constructor
$this->validateValue($item->value);
$this->validateTtl($item->ttl);
}
return $this->innerCache->set(...$items);
}
public function has(CacheIdentifier ...$identifiers): array
{
// CacheIdentifiers are already validated by their constructors
return $this->innerCache->has(...$identifiers);
}
public function forget(CacheIdentifier ...$identifiers): bool
{
// CacheIdentifiers are already validated by their constructors
return $this->innerCache->forget(...$identifiers);
}
public function clear(): bool
{
// Clear operation doesn't need validation
return $this->innerCache->clear();
}
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
// CacheKey is already validated by its constructor
$this->validateTtl($ttl);
return $this->innerCache->remember($key, function () use ($callback) {
$value = $callback();
$this->validateValue($value);
return $value;
}, $ttl);
}
/**
* Validates cache key according to rules
*
* Note: CacheKey objects perform basic validation in their constructor.
* This method provides additional validation based on configuration.
*/
private function validateKey(CacheKey $key): void
{
$keyString = (string)$key;
// Check for forbidden patterns
$forbiddenPatterns = $this->config['forbidden_key_patterns'] ?? [
'/\.\./i', // Directory traversal
'/\/\//i', // Double slashes
'/\x00/i', // Null bytes
];
foreach ($forbiddenPatterns as $pattern) {
if (preg_match($pattern, $keyString)) {
throw new InvalidArgumentException("Cache key contains forbidden pattern: {$keyString}");
}
}
// Check for reserved prefixes
$reservedPrefixes = $this->config['reserved_prefixes'] ?? ['__', 'system:', 'internal:'];
foreach ($reservedPrefixes as $prefix) {
if (str_starts_with($keyString, $prefix)) {
throw new InvalidArgumentException("Cache key uses reserved prefix '{$prefix}': {$keyString}");
}
}
}
/**
* Validates cache value according to rules
*/
private function validateValue(mixed $value): void
{
// Check value size
$maxValueSize = $this->config['max_value_size'] ?? (1024 * 1024); // 1MB default
$valueSize = $this->calculateValueSize($value);
if ($valueSize > $maxValueSize) {
throw new InvalidArgumentException("Cache value too large: {$valueSize} bytes (max: {$maxValueSize})");
}
// Check for forbidden types
$forbiddenTypes = $this->config['forbidden_types'] ?? [];
$valueType = gettype($value);
if (in_array($valueType, $forbiddenTypes)) {
throw new InvalidArgumentException("Cache value type '{$valueType}' is forbidden");
}
// Check for resource types (always forbidden as they can't be serialized)
if (is_resource($value)) {
throw new InvalidArgumentException('Cache values cannot contain resources');
}
// Check for closures (unless explicitly allowed)
if ($value instanceof \Closure && ! ($this->config['allow_closures'] ?? false)) {
throw new InvalidArgumentException('Cache values cannot contain closures');
}
// Check for objects with __wakeup or __sleep methods (potential security risk)
if (is_object($value) && ($this->config['strict_object_validation'] ?? false)) {
$reflection = new \ReflectionClass($value);
if ($reflection->hasMethod('__wakeup') || $reflection->hasMethod('__sleep')) {
throw new InvalidArgumentException('Objects with __wakeup or __sleep methods are not allowed');
}
}
// Custom validation callback
if (isset($this->config['value_validator']) && is_callable($this->config['value_validator'])) {
$isValid = ($this->config['value_validator'])($value);
if (! $isValid) {
throw new InvalidArgumentException('Cache value failed custom validation');
}
}
}
/**
* Validates TTL value
*
* Note: Duration objects perform basic validation in their constructor.
* This method provides additional validation based on configuration.
*/
private function validateTtl(?Duration $ttl): void
{
if ($ttl === null) {
return; // null TTL is valid (uses default)
}
$seconds = $ttl->toCacheSeconds();
// Check maximum TTL
$maxTtl = $this->config['max_ttl'] ?? (365 * 24 * 3600); // 1 year default
if ($seconds > $maxTtl) {
throw new InvalidArgumentException("TTL too large: {$seconds} seconds (max: {$maxTtl})");
}
// Check minimum TTL
$minTtl = $this->config['min_ttl'] ?? 1;
if ($seconds > 0 && $seconds < $minTtl) {
throw new InvalidArgumentException("TTL too small: {$seconds} seconds (min: {$minTtl})");
}
}
/**
* Calculates the approximate size of a value
*/
private function calculateValueSize(mixed $value): int
{
if (is_string($value)) {
return strlen($value);
}
if (is_int($value) || is_float($value)) {
return 8;
}
if (is_bool($value)) {
return 1;
}
if ($value === null) {
return 0;
}
// For arrays and objects, serialize to get accurate size
try {
return strlen(serialize($value));
} catch (\Throwable) {
// Fallback for non-serializable values
return 0;
}
}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,7 @@
{
"cached_at": 1754402205,
"has_attributes": true,
"has_interfaces": false,
"has_routes": false,
"has_templates": true
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,6 @@
{
"attributes": {},
"interfaces": {},
"routes": {},
"templates": {}
}

View File

@@ -0,0 +1,4 @@
gz:x<EFBFBD>՜<EFBFBD>r<EFBFBD>8<EFBFBD><EFBFBD>,޻1<EFBFBD>x<EFBFBD>3=]<EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>T<EFBFBD>&<EFBFBD><EFBFBD><EFBFBD><EFBFBD>3<EFBFBD>T<EFBFBD>}$_1<5F>ؠ#<23>Z5<5A>8?<3F><><EFBFBD><EFBFBD>/<2F>G8<7F>{<7B><><EFBFBD><EFBFBD>+E ~<7E><><EFBFBD><EFBFBD>‚l<C282><6C><EFBFBD><EFBFBD>;f˸`O<><4F>$<24>Q<EFBFBD><51><EFBFBD>a]<5D>|<7C>c<7F><63><EFBFBD>{m<>}(v?`<60><19>ݱ<EFBFBD>F<EFBFBD><46><EFBFBD><EFBFBD>9<><39><EFBFBD><EFBFBD><EFBFBD>%~<7E><><EFBFBD>A<EFBFBD><41><EFBFBD><'<27>b<17><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ތ<EFBFBD>\<10>_><3E>~<15><><EFBFBD><EFBFBD>Q<11><>=\<5C>z<EFBFBD><7A>$<24>l<EFBFBD>xL<78>ÿ<EFBFBD><07><><E282A3><EFBFBD>?<3F>MPL<50>á<EFBFBD>ׄ<EFBFBD>dEY<45><59><EFBFBD>±<15><><EFBFBD><EFBFBD><11> <09><16><>u<EFBFBD><75><EFBFBD><EFBFBD><EFBFBD>i<1E><><0F>S<1E><><EFBFBD><EFBFBD>W<EFBFBD>ʟN<CA9F>s<EFBFBD><73>;<3B>al<61><6C>^u<>g~<7E><EFBFBD><7F>s|<7C>-<05> <0B>2<EFBFBD><32><EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><15><1C>2<EFBFBD>M<EFBFBD>;<3B>mPdl<64>7V<37>c<EFBFBD>X<EFBFBD>kAg*<2A>3<EFBFBD><33><EFBFBD>L<EFBFBD><4C>Z&Ԃ<><D482><EFBFBD><EFBFBD>o<EFBFBD> <20>f<><66><1E><06><18><>o&D<>J<16>"<22><>"4=<06><>Ϡ<EFBFBD>Ñ<EFBFBD>HVa$L<>Ӕ<0F>D
a<EFBFBD><EFBFBD><EFBFBD>J<EFBFBD><EFBFBD><EFBFBD>F<EFBFBD>\<5C><><EFBFBD>"<EFBFBD>L<EFBFBD><06>\<EFBFBD>eiʒk<CA92><6B><08><><EFBFBD><EFBFBD>!b<><C4<43>gyA<12>i-hySi<53>.(2<>4wt1Sj<53><1B><><EFBFBD><EFBFBD>*L45UCi<43>b<EFBFBD>Ζ<EFBFBD><14>|c5<63>o<EFBFBD>v<EFBFBD>tm<74><6D>R6<52>R4<52>
<EFBFBD><EFBFBD>Ca<EFBFBD><EFBFBD>#<23><>j<EFBFBD><6A>Q<EFBFBD><51>
<EFBFBD><EFBFBD>PK<EFBFBD><14>$~<7E><>q<EFBFBD>[&zh<7A><68><EFBFBD>w*<2A><>aNL<4E><4C>d<EFBFBD><64><EFBFBD><03>H<EFBFBD><48>a<16>><3E><>s<EFBFBD><73><EFBFBD>4<EFBFBD>P<EFBFBD>,<2C><>XMR<4D><52>C<>յU6<>F<EFBFBD><46> k<>{[