Files
michaelschiemer/src/Framework/Cache/MultiLevelCache.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

251 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\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_SECONDS = 300;
public function __construct(
private Cache $fastCache, // z.B. ArrayCache
private Cache $slowCache // z.B. RedisCache, FileCache
) {
}
/**
* Returns the default TTL for fast cache as a Duration object
*/
private static function getDefaultFastCacheTTL(): Duration
{
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 (! empty($fastItems)) {
$fastSuccess = $this->fastCache->set(...$fastItems);
}
return $slowSuccess && $fastSuccess;
}
public function forget(CacheIdentifier ...$identifiers): bool
{
if (empty($identifiers)) {
return true;
}
$s1 = $this->fastCache->forget(...$identifiers);
$s2 = $this->slowCache->forget(...$identifiers);
return $s1 && $s2;
}
public function clear(): bool
{
$s1 = $this->fastCache->clear();
$s2 = $this->slowCache->clear();
return $s1 && $s2;
}
public function has(CacheIdentifier ...$identifiers): array
{
if (empty($identifiers)) {
return [];
}
$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
}
}
// 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;
}
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$item = $this->get($key);
if ($item->isHit) {
return $item;
}
// Wert generieren, speichern und zurückgeben
$value = $callback();
$this->set($key, $value, $ttl);
// Erstelle neuen CacheItem als Treffer
return CacheItem::hit($key, $value);
}
private function shouldCacheInFast(mixed $value): bool
{
// Wenn es bereits ein String ist (serialisiert), nutze dessen Länge
if (is_string($value)) {
return strlen($value) <= self::MAX_FAST_CACHE_SIZE;
}
// Für Arrays: Schnelle Heuristik ohne Serialisierung
if (is_array($value)) {
$elementCount = count($value, COUNT_RECURSIVE);
// Grobe Schätzung: 50 Bytes pro Element
$estimatedSize = $elementCount * 50;
return $estimatedSize <= self::MAX_FAST_CACHE_SIZE;
}
// Für Objekte: Konservativ - nicht in fast cache
if (is_object($value)) {
return false;
}
// Primitive Typen: Immer cachen
return true;
}
/**
* 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;
}
}