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:
@@ -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;
|
||||
}
|
||||
|
||||
170
src/Framework/Cache/CacheBuilder.php
Normal file
170
src/Framework/Cache/CacheBuilder.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
38
src/Framework/Cache/CacheIdentifier.php
Normal file
38
src/Framework/Cache/CacheIdentifier.php
Normal 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;
|
||||
}
|
||||
223
src/Framework/Cache/CacheIdentifierCollection.php
Normal file
223
src/Framework/Cache/CacheIdentifierCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Framework/Cache/CacheIdentifierType.php
Normal file
51
src/Framework/Cache/CacheIdentifierType.php
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
232
src/Framework/Cache/CacheKey.php
Normal file
232
src/Framework/Cache/CacheKey.php
Normal 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);
|
||||
}
|
||||
}
|
||||
169
src/Framework/Cache/CacheKeyCollection.php
Normal file
169
src/Framework/Cache/CacheKeyCollection.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/Framework/Cache/CachePattern.php
Normal file
212
src/Framework/Cache/CachePattern.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
358
src/Framework/Cache/CacheResult.php
Normal file
358
src/Framework/Cache/CacheResult.php
Normal 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;
|
||||
}
|
||||
}
|
||||
137
src/Framework/Cache/CacheTag.php
Normal file
137
src/Framework/Cache/CacheTag.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
34
src/Framework/Cache/Compression/NoCompression.php
Normal file
34
src/Framework/Cache/Compression/NoCompression.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache\Compression;
|
||||
|
||||
use App\Framework\Cache\CompressionAlgorithm;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface CompressionAlgorithm
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
31
src/Framework/Cache/Contracts/DriverAccessible.php
Normal file
31
src/Framework/Cache/Contracts/DriverAccessible.php
Normal 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;
|
||||
}
|
||||
51
src/Framework/Cache/Contracts/Scannable.php
Normal file
51
src/Framework/Cache/Contracts/Scannable.php
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 . '$/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 . '$/';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
137
src/Framework/Cache/EventCacheDecorator.php
Normal file
137
src/Framework/Cache/EventCacheDecorator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Framework/Cache/Events/CacheClear.php
Normal file
25
src/Framework/Cache/Events/CacheClear.php
Normal 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));
|
||||
}
|
||||
}
|
||||
28
src/Framework/Cache/Events/CacheDelete.php
Normal file
28
src/Framework/Cache/Events/CacheDelete.php
Normal 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));
|
||||
}
|
||||
}
|
||||
29
src/Framework/Cache/Events/CacheHit.php
Normal file
29
src/Framework/Cache/Events/CacheHit.php
Normal 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));
|
||||
}
|
||||
}
|
||||
27
src/Framework/Cache/Events/CacheMiss.php
Normal file
27
src/Framework/Cache/Events/CacheMiss.php
Normal 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));
|
||||
}
|
||||
}
|
||||
37
src/Framework/Cache/Events/CacheSet.php
Normal file
37
src/Framework/Cache/Events/CacheSet.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
353
src/Framework/Cache/Metrics/CacheMetrics.php
Normal file
353
src/Framework/Cache/Metrics/CacheMetrics.php
Normal 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;
|
||||
}
|
||||
}
|
||||
63
src/Framework/Cache/Metrics/CacheMetricsInterface.php
Normal file
63
src/Framework/Cache/Metrics/CacheMetricsInterface.php
Normal 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;
|
||||
}
|
||||
119
src/Framework/Cache/Metrics/CacheStatsSnapshot.php
Normal file
119
src/Framework/Cache/Metrics/CacheStatsSnapshot.php
Normal 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;
|
||||
}
|
||||
}
|
||||
327
src/Framework/Cache/Metrics/MetricsDecoratedCache.php
Normal file
327
src/Framework/Cache/Metrics/MetricsDecoratedCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
339
src/Framework/Cache/README.md
Normal file
339
src/Framework/Cache/README.md
Normal 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.**
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Cache;
|
||||
|
||||
interface Serializer
|
||||
{
|
||||
public function serialize(mixed $value): string;
|
||||
public function unserialize(string $value): mixed;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
71
src/Framework/Cache/ServiceCacheDecorator.php
Normal file
71
src/Framework/Cache/ServiceCacheDecorator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
966
src/Framework/Cache/SmartCache.php
Normal file
966
src/Framework/Cache/SmartCache.php
Normal 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);
|
||||
}
|
||||
}
|
||||
261
src/Framework/Cache/TagIndex.php
Normal file
261
src/Framework/Cache/TagIndex.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
209
src/Framework/Cache/ValidationCacheDecorator.php
Normal file
209
src/Framework/Cache/ValidationCacheDecorator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/Framework/Cache/storage/cache/02a69f7062580b50df6dd4eda9bc9b33.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/02a69f7062580b50df6dd4eda9bc9b33.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/098f6bcd4621d373cade4e832627b4f6.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/098f6bcd4621d373cade4e832627b4f6.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1755347e5f6a762b84a3f6512a3e4e53.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1755347e5f6a762b84a3f6512a3e4e53.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1c09494c44cb3a063dcb5fe722da46d4.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1c09494c44cb3a063dcb5fe722da46d4.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1e3bf3d5b77401b7c083b3aa94b59c45.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/1e3bf3d5b77401b7c083b3aa94b59c45.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/28030a9ad25c2b193f8059b59a3a5fdc.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/28030a9ad25c2b193f8059b59a3a5fdc.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/288f499134e560466949c5a12e33d6d0.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/288f499134e560466949c5a12e33d6d0.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/3233f75ec08eb6a05332109190308b93.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/3233f75ec08eb6a05332109190308b93.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/3482b4cfc68c7c6509b6ee961f58625f.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/3482b4cfc68c7c6509b6ee961f58625f.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/372d10fb65a33c37f271cd543fd111b9.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/372d10fb65a33c37f271cd543fd111b9.lock
vendored
Normal file
6
src/Framework/Cache/storage/cache/4aeaa0e03d3c560579f8d592a5618d4f_1754560769.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/4aeaa0e03d3c560579f8d592a5618d4f_1754560769.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
0
src/Framework/Cache/storage/cache/4ba5a0a3aa6074527838cd19e52b4ae3.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/4ba5a0a3aa6074527838cd19e52b4ae3.lock
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/4ba5a0a3aa6074527838cd19e52b4ae3_1754405805.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/4ba5a0a3aa6074527838cd19e52b4ae3_1754405805.cache.php
vendored
Normal file
Binary file not shown.
0
src/Framework/Cache/storage/cache/5e40d09fa0529781afd1254a42913847.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/5e40d09fa0529781afd1254a42913847.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/60d6bed9b7fa777f0c4c98283d82e335.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/60d6bed9b7fa777f0c4c98283d82e335.lock
vendored
Normal file
7
src/Framework/Cache/storage/cache/60d6bed9b7fa777f0c4c98283d82e335_1754405805.cache.php
vendored
Normal file
7
src/Framework/Cache/storage/cache/60d6bed9b7fa777f0c4c98283d82e335_1754405805.cache.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"cached_at": 1754402205,
|
||||
"has_attributes": true,
|
||||
"has_interfaces": false,
|
||||
"has_routes": false,
|
||||
"has_templates": true
|
||||
}
|
||||
0
src/Framework/Cache/storage/cache/68ae3d10fab3f6b375684f7613545710.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/68ae3d10fab3f6b375684f7613545710.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/726bc5d2beb8afd990ce2e0af1d03960.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/726bc5d2beb8afd990ce2e0af1d03960.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/83ac9fea13ab9226eeb19e8f761bbc31.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/83ac9fea13ab9226eeb19e8f761bbc31.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/8bba037f81ea98d4b839ee54b6e76124.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/8bba037f81ea98d4b839ee54b6e76124.lock
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754527340.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754527340.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754529550.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754529550.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754529825.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754529825.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530255.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530255.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530382.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530382.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530582.cache.php
vendored
Normal file
6
src/Framework/Cache/storage/cache/b7521349ebf0861fc24ec6dabd43fd46_1754530582.cache.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"attributes": {},
|
||||
"interfaces": {},
|
||||
"routes": {},
|
||||
"templates": {}
|
||||
}
|
||||
0
src/Framework/Cache/storage/cache/bef9b553a8da5dea95ef78c8c1760c91.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/bef9b553a8da5dea95ef78c8c1760c91.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/c03385318179b20d7396fcdb7ce73d10.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/c03385318179b20d7396fcdb7ce73d10.lock
vendored
Normal file
4
src/Framework/Cache/storage/cache/c03385318179b20d7396fcdb7ce73d10_1754405805.cache.php
vendored
Normal file
4
src/Framework/Cache/storage/cache/c03385318179b20d7396fcdb7ce73d10_1754405805.cache.php
vendored
Normal 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<>{[
|
||||
0
src/Framework/Cache/storage/cache/cf510c48d23b2a5dba6277b3576924b7.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/cf510c48d23b2a5dba6277b3576924b7.lock
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/cf510c48d23b2a5dba6277b3576924b7_1754405805.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/cf510c48d23b2a5dba6277b3576924b7_1754405805.cache.php
vendored
Normal file
Binary file not shown.
0
src/Framework/Cache/storage/cache/d86db53068ba5c0311e7629678f6f89a.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/d86db53068ba5c0311e7629678f6f89a.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/e131d33027d2847b2a01fcc5eeccf0b1.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/e131d33027d2847b2a01fcc5eeccf0b1.lock
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e131d33027d2847b2a01fcc5eeccf0b1_1754405805.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e131d33027d2847b2a01fcc5eeccf0b1_1754405805.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754527340.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754527340.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754529570.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754529570.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754529845.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754529845.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530275.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530275.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530402.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530402.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530403.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530403.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530602.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754530602.cache.php
vendored
Normal file
Binary file not shown.
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754560789.cache.php
vendored
Normal file
BIN
src/Framework/Cache/storage/cache/e6a042d1b4901cad73d0325b817d83e1_1754560789.cache.php
vendored
Normal file
Binary file not shown.
0
src/Framework/Cache/storage/cache/f45811804508ef3ee80955742aa06c15.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/f45811804508ef3ee80955742aa06c15.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/fed36e93a0509e20f2dc96cbbd85b678.lock
vendored
Normal file
0
src/Framework/Cache/storage/cache/fed36e93a0509e20f2dc96cbbd85b678.lock
vendored
Normal file
Reference in New Issue
Block a user