- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
251 lines
7.5 KiB
PHP
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;
|
|
}
|
|
}
|