docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -56,6 +56,11 @@ final readonly class CacheInitializer
// L2 Cache: Persistent cache with compression
try {
// Check if Redis extension is available
if (! extension_loaded('redis')) {
throw new \RuntimeException('Redis extension is not loaded. Please install php-redis extension or use alternative cache drivers.');
}
$redisConfig = new RedisConfig(
host: $this->redisHost,
port: $this->redisPort,

View File

@@ -8,6 +8,7 @@ use App\Framework\Cache\Cache;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ExitCode;
use App\Framework\Core\PathProvider;
use App\Framework\DI\Container;
use App\Framework\Redis\RedisConnectionPool;
@@ -22,7 +23,7 @@ final readonly class ClearCache
}
#[ConsoleCommand("cache:clear", "Clears all caches (application, discovery, routes, opcache, redis)")]
public function __invoke(ConsoleInput $input, ConsoleOutput $output): void
public function __invoke(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$cleared = [];
@@ -58,17 +59,19 @@ final readonly class ClearCache
$cleared[] = 'All cache files';
$output->writeSuccess('Cleared: ' . implode(', ', $cleared));
return ExitCode::SUCCESS;
}
#[ConsoleCommand("redis:flush", "Advanced Redis cache clearing with options")]
public function flushRedis(ConsoleInput $input, ConsoleOutput $output): void
public function flushRedis(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$cleared = [];
if (! $this->container->has(RedisConnectionPool::class)) {
$output->writeError('Redis connection pool not available');
return;
return ExitCode::FAILURE;
}
try {
@@ -86,7 +89,7 @@ final readonly class ClearCache
} else {
$output->writeError("Failed to flush Redis database $database");
return;
return ExitCode::FAILURE;
}
} else {
// Default: FLUSHALL
@@ -96,13 +99,17 @@ final readonly class ClearCache
} else {
$output->writeError('Failed to flush Redis');
return;
return ExitCode::FAILURE;
}
}
$output->writeSuccess('Cleared: ' . implode(', ', $cleared));
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('Redis flush failed: ' . $e->getMessage());
return ExitCode::FAILURE;
}
}

View File

@@ -7,15 +7,19 @@ namespace App\Framework\Cache;
use App\Framework\Async\AsyncService;
use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Cache\Contracts\Scannable;
use App\Framework\Cache\Strategies\CacheStrategyManager;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Smart cache implementation that replaces AsyncAwareCache
* Smart cache implementation with composable strategy support
*
* Features:
* - Integrated async functionality with intelligent batching
* - Pattern and prefix support for bulk operations
* - Automatic async/sync decision making
* - Composable cache strategies (Adaptive TTL, Heat Mapping, Predictive Warming)
* - Strategy-based cache optimization and intelligence
* - Backward compatibility with existing Cache interface
*/
final class SmartCache implements Cache, DriverAccessible
@@ -24,12 +28,22 @@ final class SmartCache implements Cache, DriverAccessible
private const int LARGE_BATCH_THRESHOLD = 20; // Extra optimization for large batches
private readonly ?TagIndex $tagIndex;
private readonly ?CacheStrategyManager $strategyManager;
public function __construct(
private readonly Cache $innerCache,
private readonly ?AsyncService $asyncService = null,
private readonly bool $asyncEnabled = true
private readonly bool $asyncEnabled = true,
?CacheStrategyManager $strategyManager = null,
private readonly bool $enableDefaultStrategies = true
) {
// Initialize strategy manager with defaults if enabled and none provided
if ($strategyManager === null && $this->enableDefaultStrategies) {
$this->strategyManager = CacheStrategyManager::createDefault();
} else {
$this->strategyManager = $strategyManager;
}
// Initialize tag index if we have access to a scannable driver
$driver = $this->getDriver();
$this->tagIndex = ($driver instanceof Scannable) ? new TagIndex($driver) : null;
@@ -60,11 +74,24 @@ final class SmartCache implements Cache, DriverAccessible
// Handle exact keys with potential async optimization
if (! empty($exactKeys)) {
$startTime = Timestamp::now();
if ($this->shouldUseAsync(count($exactKeys))) {
$results[] = $this->getAsync($exactKeys);
} else {
$results[] = $this->innerCache->get(...$exactKeys);
}
// Notify strategies of cache access
if ($this->strategyManager) {
$endTime = Timestamp::now();
$retrievalTime = $startTime->diff($endTime);
foreach ($exactKeys as $key) {
$isHit = $results[count($results) - 1]->getItem($key)->isHit;
$this->strategyManager->notifyAccess($key, $isHit, $retrievalTime);
}
}
}
// Handle patterns - these require special processing
@@ -97,12 +124,23 @@ final class SmartCache implements Cache, DriverAccessible
$this->monitorCacheItemSize($item);
}
// Use async for large batches
if ($this->shouldUseAsync(count($items))) {
return $this->setAsync($items);
// Apply strategies to modify TTL if needed
$modifiedItems = [];
foreach ($items as $item) {
if ($this->strategyManager) {
$adaptedTtl = $this->strategyManager->notifySet($item->key, $item->value, $item->ttl);
$modifiedItems[] = CacheItem::forSet($item->key, $item->value, $adaptedTtl);
} else {
$modifiedItems[] = $item;
}
}
return $this->innerCache->set(...$items);
// Use async for large batches
if ($this->shouldUseAsync(count($modifiedItems))) {
return $this->setAsync($modifiedItems);
}
return $this->innerCache->set(...$modifiedItems);
}
public function has(CacheIdentifier ...$identifiers): array
@@ -184,6 +222,13 @@ final class SmartCache implements Cache, DriverAccessible
// Handle exact keys
if (! empty($exactKeys)) {
// Notify strategies before forgetting
if ($this->strategyManager) {
foreach ($exactKeys as $key) {
$this->strategyManager->notifyForget($key);
}
}
if ($this->shouldUseAsync(count($exactKeys))) {
$success &= $this->forgetAsync($exactKeys);
} else {
@@ -219,7 +264,23 @@ final class SmartCache implements Cache, DriverAccessible
public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
return $this->innerCache->remember($key, $callback, $ttl);
$startTime = Timestamp::now();
// Apply strategies to TTL if available
if ($this->strategyManager) {
$ttl = $this->strategyManager->notifySet($key, null, $ttl);
}
$result = $this->innerCache->remember($key, $callback, $ttl);
// Notify strategies of access
if ($this->strategyManager) {
$endTime = Timestamp::now();
$retrievalTime = $startTime->diff($endTime);
$this->strategyManager->notifyAccess($key, $result->isHit, $retrievalTime);
}
return $result;
}
/**
@@ -412,7 +473,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $matchingKeys);
// Batch load all matching keys
return $cacheDriver->get(...$cacheKeys);
@@ -445,7 +506,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $matchingKeys);
// Batch load all matching keys
return $cacheDriver->get(...$cacheKeys);
@@ -474,7 +535,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert to CacheKey objects and batch load
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $keyStrings);
return $this->innerCache->get(...$cacheKeys);
@@ -557,7 +618,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert to CacheKey objects and check existence
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $keyStrings);
return $this->innerCache->has(...$cacheKeys);
@@ -588,7 +649,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $matchingKeys);
// Batch delete all matching keys
$cacheDriver->forget(...$cacheKeys);
@@ -622,7 +683,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert string keys back to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $matchingKeys);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $matchingKeys);
// Batch delete all matching keys
$cacheDriver->forget(...$cacheKeys);
@@ -652,7 +713,7 @@ final class SmartCache implements Cache, DriverAccessible
}
// Convert to CacheKey objects
$cacheKeys = array_map(fn (string $key) => CacheKey::from($key), $keyStrings);
$cacheKeys = array_map(fn (string $key) => CacheKey::fromString($key), $keyStrings);
// Delete from cache and remove from tag index
$this->innerCache->forget(...$cacheKeys);
@@ -740,7 +801,7 @@ final class SmartCache implements Cache, DriverAccessible
}
/**
* Get comprehensive stats including smart cache metrics
* Get comprehensive stats including smart cache metrics and strategy stats
*/
public function getStats(): array
{
@@ -756,6 +817,7 @@ final class SmartCache implements Cache, DriverAccessible
'prefix_support' => $this->driverSupports(Scannable::class),
'tag_support' => $this->tagIndex !== null,
'intelligent_batching' => true,
'strategy_support' => $this->strategyManager !== null && $this->strategyManager->isEnabled(),
];
// Add tag index statistics if available
@@ -763,6 +825,13 @@ final class SmartCache implements Cache, DriverAccessible
$smartStats['tag_stats'] = $this->tagIndex->getStats();
}
// Add strategy statistics if available
if ($this->strategyManager) {
$smartStats['strategy_stats'] = $this->strategyManager->getStats();
} else {
$smartStats['strategy_support'] = false;
}
return array_merge($baseStats, $smartStats);
}
@@ -963,4 +1032,100 @@ final class SmartCache implements Cache, DriverAccessible
return implode(', ', $patterns);
}
/**
* Get the strategy manager (if available)
*/
public function getStrategyManager(): ?CacheStrategyManager
{
return $this->strategyManager;
}
/**
* Clear all strategy data
*/
public function clearStrategyData(): void
{
if ($this->strategyManager) {
$this->strategyManager->clearAll();
}
}
/**
* Get strategy-specific statistics
*/
public function getStrategyStats(string $strategyName): ?array
{
if (!$this->strategyManager) {
return null;
}
$strategy = $this->strategyManager->getStrategy($strategyName);
return $strategy?->getStats();
}
/**
* Check if a strategy is enabled
*/
public function isStrategyEnabled(string $strategyName): bool
{
return $this->strategyManager?->isStrategyEnabled($strategyName) ?? false;
}
/**
* Check if strategies are enabled at all
*/
public function hasStrategies(): bool
{
return $this->strategyManager !== null;
}
/**
* Create SmartCache with default strategies (same as constructor default)
*/
public static function withDefaultStrategies(Cache $innerCache, ?AsyncService $asyncService = null): self
{
return new self(
innerCache: $innerCache,
asyncService: $asyncService
// strategyManager will default to CacheStrategyManager::createDefault()
);
}
/**
* Create SmartCache without any strategies
*/
public static function withoutStrategies(Cache $innerCache, ?AsyncService $asyncService = null): self
{
return new self(
innerCache: $innerCache,
asyncService: $asyncService,
strategyManager: null,
enableDefaultStrategies: false
);
}
/**
* Create SmartCache with performance-focused strategies
*/
public static function withPerformanceStrategies(Cache $innerCache, ?AsyncService $asyncService = null): self
{
return new self(
innerCache: $innerCache,
asyncService: $asyncService,
strategyManager: CacheStrategyManager::createPerformanceFocused()
);
}
/**
* Create SmartCache with development-focused strategies
*/
public static function withDevelopmentStrategies(Cache $innerCache, ?AsyncService $asyncService = null): self
{
return new self(
innerCache: $innerCache,
asyncService: $asyncService,
strategyManager: CacheStrategyManager::createDevelopmentFocused()
);
}
}

View File

@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Adaptive TTL Cache Strategy
*
* Dynamically adjusts TTL based on access patterns to optimize cache efficiency.
* Frequently accessed items get longer TTL, rarely accessed items get shorter TTL.
*/
final class AdaptiveTtlCacheStrategy implements CacheStrategy
{
private const int DEFAULT_LEARNING_WINDOW = 100;
private const float DEFAULT_EXTENSION_FACTOR = 1.5;
private const float DEFAULT_REDUCTION_FACTOR = 0.8;
private const int ACCESS_THRESHOLD_HIGH = 10;
private const int ACCESS_THRESHOLD_LOW = 2;
/** @var array<string, AccessPattern> */
private array $accessPatterns = [];
/** @var array<string, AdaptiveTtlStats> */
private array $itemStats = [];
private readonly Duration $minTtl;
private readonly Duration $maxTtl;
public function __construct(
private readonly bool $enabled = true,
private readonly int $learningWindow = self::DEFAULT_LEARNING_WINDOW,
private readonly float $extensionFactor = self::DEFAULT_EXTENSION_FACTOR,
private readonly float $reductionFactor = self::DEFAULT_REDUCTION_FACTOR,
?Duration $minTtl = null,
?Duration $maxTtl = null
) {
$this->minTtl = $minTtl ?? Duration::fromSeconds(60);
$this->maxTtl = $maxTtl ?? Duration::fromSeconds(86400);
}
public function onCacheAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
// Record access pattern
if (!isset($this->accessPatterns[$keyString])) {
$this->accessPatterns[$keyString] = new AccessPattern($this->learningWindow);
}
$this->accessPatterns[$keyString]->recordAccess();
// Record hit/miss statistics
if (!isset($this->itemStats[$keyString])) {
$this->itemStats[$keyString] = new AdaptiveTtlStats();
}
$this->itemStats[$keyString]->recordHitMiss($isHit);
}
public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
{
if (!$this->enabled) {
return $originalTtl ?? Duration::fromHours(1);
}
return $this->calculateAdaptiveTtl($key, $originalTtl);
}
public function onCacheForget(CacheKey $key): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
unset($this->accessPatterns[$keyString]);
unset($this->itemStats[$keyString]);
}
public function getStats(): array
{
$stats = [
'strategy' => 'AdaptiveTtlCacheStrategy',
'enabled' => $this->enabled,
'total_tracked_keys' => count($this->accessPatterns),
'learning_window' => $this->learningWindow,
'ttl_bounds' => [
'min_seconds' => $this->minTtl->toSeconds(),
'max_seconds' => $this->maxTtl->toSeconds(),
],
'adaptation_factors' => [
'extension_factor' => $this->extensionFactor,
'reduction_factor' => $this->reductionFactor,
],
'key_patterns' => []
];
// Include top accessed keys and their patterns
$keysByAccess = [];
foreach ($this->accessPatterns as $keyString => $pattern) {
$keysByAccess[$keyString] = $pattern->getTotalAccesses();
}
arsort($keysByAccess);
$topKeys = array_slice($keysByAccess, 0, 10, true);
foreach ($topKeys as $keyString => $totalAccesses) {
$pattern = $this->accessPatterns[$keyString];
$itemStats = $this->itemStats[$keyString] ?? null;
$stats['key_patterns'][$keyString] = [
'total_accesses' => $totalAccesses,
'recent_accesses' => $pattern->getRecentAccessCount(),
'access_frequency' => round($pattern->getAccessFrequency(), 3),
'hit_rate' => $itemStats ? round($itemStats->getHitRate(), 3) : null,
'total_requests' => $itemStats?->getTotalRequests() ?? 0,
];
}
return $stats;
}
public function getName(): string
{
return 'adaptive_ttl';
}
public function clear(): void
{
$this->accessPatterns = [];
$this->itemStats = [];
}
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Calculate adaptive TTL based on access patterns and original TTL
*/
private function calculateAdaptiveTtl(CacheKey $key, ?Duration $originalTtl): Duration
{
$keyString = $key->toString();
$pattern = $this->accessPatterns[$keyString] ?? null;
// Use original TTL as base, or default if not provided
$baseTtl = $originalTtl ?? Duration::fromHours(1);
$baseSeconds = $baseTtl->toSeconds();
if (!$pattern) {
// No access pattern yet, use original TTL
return $this->enforceTtlBounds($baseTtl);
}
$recentAccesses = $pattern->getRecentAccessCount();
$accessFrequency = $pattern->getAccessFrequency();
$hitRate = $this->itemStats[$keyString]?->getHitRate() ?? 1.0;
// Calculate adaptation factor based on access patterns
$adaptationFactor = 1.0;
// High access count and good hit rate = extend TTL
if ($recentAccesses >= self::ACCESS_THRESHOLD_HIGH && $hitRate > 0.8) {
$adaptationFactor = $this->extensionFactor;
}
// Low access count or poor hit rate = reduce TTL
elseif ($recentAccesses <= self::ACCESS_THRESHOLD_LOW || $hitRate < 0.3) {
$adaptationFactor = $this->reductionFactor;
}
// Moderate access = adjust based on frequency trend
else {
// If access frequency is increasing, slightly extend TTL
// If decreasing, slightly reduce TTL
$trendFactor = max(0.5, min(2.0, $accessFrequency));
$adaptationFactor = 0.8 + ($trendFactor * 0.4); // Range: 0.8 to 1.2
}
$adaptedSeconds = (int) round($baseSeconds * $adaptationFactor);
$adaptedTtl = Duration::fromSeconds($adaptedSeconds);
return $this->enforceTtlBounds($adaptedTtl);
}
/**
* Ensure TTL stays within configured bounds
*/
private function enforceTtlBounds(Duration $ttl): Duration
{
$seconds = $ttl->toSeconds();
$minSeconds = $this->minTtl->toSeconds();
$maxSeconds = $this->maxTtl->toSeconds();
$boundedSeconds = max($minSeconds, min($maxSeconds, $seconds));
return Duration::fromSeconds($boundedSeconds);
}
}
/**
* Access pattern tracking for adaptive TTL calculation
*/
final class AccessPattern
{
/** @var array<int, Timestamp> */
private array $accessTimes = [];
public function __construct(
private readonly int $windowSize
) {}
public function recordAccess(): void
{
$this->accessTimes[] = Timestamp::now();
// Keep only recent accesses within the window
if (count($this->accessTimes) > $this->windowSize) {
array_shift($this->accessTimes);
}
}
public function getRecentAccessCount(): int
{
return count($this->accessTimes);
}
public function getTotalAccesses(): int
{
return count($this->accessTimes);
}
/**
* Calculate access frequency (accesses per hour)
*/
public function getAccessFrequency(): float
{
if (count($this->accessTimes) < 2) {
return 0.0;
}
$oldest = $this->accessTimes[0];
$newest = end($this->accessTimes);
$timeSpan = $newest->diff($oldest);
if ($timeSpan->toSeconds() <= 0) {
return 0.0;
}
$hours = $timeSpan->toSeconds() / 3600.0;
return count($this->accessTimes) / $hours;
}
}
/**
* Statistics for adaptive TTL optimization
*/
final class AdaptiveTtlStats
{
private int $hits = 0;
private int $misses = 0;
public function recordHitMiss(bool $isHit): void
{
if ($isHit) {
$this->hits++;
} else {
$this->misses++;
}
}
public function getHitRate(): float
{
$total = $this->hits + $this->misses;
return $total > 0 ? ($this->hits / $total) : 0.0;
}
public function getTotalRequests(): int
{
return $this->hits + $this->misses;
}
public function getHits(): int
{
return $this->hits;
}
public function getMisses(): int
{
return $this->misses;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
/**
* Unified Cache Strategy Interface
*
* Allows implementing various caching strategies that can be composed together
* in SmartCache for enhanced cache behavior.
*/
interface CacheStrategy
{
/**
* Called on every cache access (hit or miss)
*
* @param CacheKey $key The cache key being accessed
* @param bool $isHit Whether the access was a cache hit
* @param Duration|null $retrievalTime Time taken to retrieve the value
*/
public function onCacheAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void;
/**
* Called when setting a cache item - can modify the TTL
*
* @param CacheKey $key The cache key being set
* @param mixed $value The value being cached
* @param Duration|null $originalTtl The original TTL requested
* @return Duration The TTL to actually use (can be modified by strategy)
*/
public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration;
/**
* Called when a cache item is forgotten/deleted
*
* @param CacheKey $key The cache key being deleted
*/
public function onCacheForget(CacheKey $key): void;
/**
* Get strategy-specific statistics and metrics
*
* @return array Strategy statistics
*/
public function getStats(): array;
/**
* Get the strategy name/identifier
*
* @return string Strategy name
*/
public function getName(): string;
/**
* Clear all strategy data and reset state
*/
public function clear(): void;
/**
* Check if strategy is enabled/active
*
* @return bool Whether strategy is active
*/
public function isEnabled(): bool;
}

View File

@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
/**
* Cache Strategy Manager
*
* Orchestrates multiple cache strategies and coordinates their execution.
* Provides a unified interface for strategy management and composition.
*/
final class CacheStrategyManager
{
/** @var array<string, CacheStrategy> */
private array $strategies = [];
/** @var array<string> */
private array $enabledStrategies = [];
public function __construct(
private readonly bool $enabled = true
) {}
/**
* Register a cache strategy
*/
public function addStrategy(CacheStrategy $strategy): self
{
$name = $strategy->getName();
$this->strategies[$name] = $strategy;
if ($strategy->isEnabled()) {
$this->enabledStrategies[] = $name;
}
return $this;
}
/**
* Remove a strategy
*/
public function removeStrategy(string $name): self
{
unset($this->strategies[$name]);
$this->enabledStrategies = array_filter(
$this->enabledStrategies,
fn($strategyName) => $strategyName !== $name
);
return $this;
}
/**
* Enable a strategy
*/
public function enableStrategy(string $name): self
{
if (isset($this->strategies[$name]) && !in_array($name, $this->enabledStrategies)) {
$this->enabledStrategies[] = $name;
}
return $this;
}
/**
* Disable a strategy
*/
public function disableStrategy(string $name): self
{
$this->enabledStrategies = array_filter(
$this->enabledStrategies,
fn($strategyName) => $strategyName !== $name
);
return $this;
}
/**
* Notify all enabled strategies of cache access
*/
public function notifyAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void
{
if (!$this->enabled) {
return;
}
foreach ($this->getEnabledStrategies() as $strategy) {
try {
$strategy->onCacheAccess($key, $isHit, $retrievalTime);
} catch (\Throwable $e) {
// Log error but don't break cache operations
error_log("Cache strategy {$strategy->getName()} access notification failed: " . $e->getMessage());
}
}
}
/**
* Notify all enabled strategies of cache set and get final TTL
*/
public function notifySet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
{
if (!$this->enabled) {
return $originalTtl ?? Duration::fromHours(1);
}
$finalTtl = $originalTtl ?? Duration::fromHours(1);
foreach ($this->getEnabledStrategies() as $strategy) {
try {
$finalTtl = $strategy->onCacheSet($key, $value, $finalTtl);
} catch (\Throwable $e) {
// Log error but continue with current TTL
error_log("Cache strategy {$strategy->getName()} set notification failed: " . $e->getMessage());
}
}
return $finalTtl;
}
/**
* Notify all enabled strategies of cache forget
*/
public function notifyForget(CacheKey $key): void
{
if (!$this->enabled) {
return;
}
foreach ($this->getEnabledStrategies() as $strategy) {
try {
$strategy->onCacheForget($key);
} catch (\Throwable $e) {
// Log error but don't break cache operations
error_log("Cache strategy {$strategy->getName()} forget notification failed: " . $e->getMessage());
}
}
}
/**
* Get comprehensive statistics from all strategies
*/
public function getStats(): array
{
$stats = [
'strategy_manager' => [
'enabled' => $this->enabled,
'total_strategies' => count($this->strategies),
'enabled_strategies' => count($this->enabledStrategies),
'strategy_names' => array_keys($this->strategies),
'enabled_strategy_names' => $this->enabledStrategies,
],
'strategies' => []
];
foreach ($this->strategies as $name => $strategy) {
try {
$stats['strategies'][$name] = $strategy->getStats();
} catch (\Throwable $e) {
$stats['strategies'][$name] = [
'error' => 'Failed to get stats: ' . $e->getMessage()
];
}
}
return $stats;
}
/**
* Clear all strategy data
*/
public function clearAll(): void
{
foreach ($this->strategies as $strategy) {
try {
$strategy->clear();
} catch (\Throwable $e) {
error_log("Failed to clear strategy {$strategy->getName()}: " . $e->getMessage());
}
}
}
/**
* Get a specific strategy by name
*/
public function getStrategy(string $name): ?CacheStrategy
{
return $this->strategies[$name] ?? null;
}
/**
* Get all registered strategies
*/
public function getAllStrategies(): array
{
return $this->strategies;
}
/**
* Get only enabled strategies
*/
public function getEnabledStrategies(): array
{
$enabledStrategies = [];
foreach ($this->enabledStrategies as $name) {
if (isset($this->strategies[$name])) {
$enabledStrategies[] = $this->strategies[$name];
}
}
return $enabledStrategies;
}
/**
* Check if a strategy is enabled
*/
public function isStrategyEnabled(string $name): bool
{
return in_array($name, $this->enabledStrategies);
}
/**
* Get strategy count
*/
public function getStrategyCount(): int
{
return count($this->strategies);
}
/**
* Check if manager is enabled
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Create a pre-configured strategy manager with common strategies
*/
public static function createDefault(): self
{
$manager = new self();
// Add default strategies
$manager->addStrategy(new AdaptiveTtlCacheStrategy());
$manager->addStrategy(new HeatMapCacheStrategy());
$manager->addStrategy(new PredictiveCacheStrategy());
return $manager;
}
/**
* Create a performance-focused strategy manager
*/
public static function createPerformanceFocused(): self
{
$manager = new self();
// Add performance-optimized strategies
$manager->addStrategy(new AdaptiveTtlCacheStrategy(
enabled: true,
learningWindow: 50, // Smaller window for faster adaptation
extensionFactor: 2.0, // More aggressive extension
reductionFactor: 0.6 // More aggressive reduction
));
$manager->addStrategy(new HeatMapCacheStrategy(
enabled: true,
maxTrackedKeys: 5000, // Smaller tracking for performance
hotThreshold: 30, // Higher threshold for hot keys
coldThreshold: 1 // Lower threshold for cold keys (must be int)
));
return $manager;
}
/**
* Create a development-focused strategy manager with detailed tracking
*/
public static function createDevelopmentFocused(): self
{
$manager = new self();
// Add development-friendly strategies with detailed tracking
$manager->addStrategy(new AdaptiveTtlCacheStrategy(
enabled: true,
learningWindow: 200, // Larger window for detailed analysis
extensionFactor: 1.3, // Conservative extension
reductionFactor: 0.9 // Conservative reduction
));
$manager->addStrategy(new HeatMapCacheStrategy(
enabled: true,
maxTrackedKeys: 20000, // More detailed tracking
hotThreshold: 10, // Lower threshold to catch more patterns
coldThreshold: 2, // Higher threshold for cold detection
analysisWindowHours: 6 // Longer analysis window
));
$manager->addStrategy(new PredictiveCacheStrategy(
enabled: true,
predictionWindowHours: 48, // Longer prediction window
confidenceThreshold: 0.5, // Lower threshold for more predictions
maxConcurrentWarming: 10 // More concurrent warming
));
return $manager;
}
}

View File

@@ -0,0 +1,430 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Heat Map Cache Strategy
*
* Tracks cache usage patterns to identify hot/cold keys and performance bottlenecks.
* Provides optimization insights and recommendations.
*/
final class HeatMapCacheStrategy implements CacheStrategy
{
private const int DEFAULT_MAX_TRACKED_KEYS = 10000;
private const int DEFAULT_HOT_THRESHOLD = 20; // accesses per hour
private const int DEFAULT_COLD_THRESHOLD = 1; // accesses per hour
private const int DEFAULT_ANALYSIS_WINDOW_HOURS = 2;
/** @var array<string, HeatMapEntry> */
private array $heatMap = [];
/** @var array<string, WriteOperation> */
private array $writeOperations = [];
public function __construct(
private readonly bool $enabled = true,
private readonly int $maxTrackedKeys = self::DEFAULT_MAX_TRACKED_KEYS,
private readonly int $hotThreshold = self::DEFAULT_HOT_THRESHOLD,
private readonly int $coldThreshold = self::DEFAULT_COLD_THRESHOLD,
private readonly int $analysisWindowHours = self::DEFAULT_ANALYSIS_WINDOW_HOURS
) {}
public function onCacheAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
// Prevent memory overflow by limiting tracked keys
if (!isset($this->heatMap[$keyString]) && count($this->heatMap) >= $this->maxTrackedKeys) {
$this->evictOldestEntry();
}
if (!isset($this->heatMap[$keyString])) {
$this->heatMap[$keyString] = new HeatMapEntry($key);
}
$this->heatMap[$keyString]->recordAccess($isHit, $retrievalTime);
}
public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
{
if (!$this->enabled) {
return $originalTtl ?? Duration::fromHours(1);
}
$keyString = $key->toString();
$valueSize = $this->calculateValueSize($value);
$setTime = Duration::fromMilliseconds(1); // Minimal set time for now
$this->writeOperations[$keyString] = new WriteOperation(
$key,
$valueSize,
$setTime,
Timestamp::now()
);
return $originalTtl ?? Duration::fromHours(1);
}
public function onCacheForget(CacheKey $key): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
unset($this->heatMap[$keyString]);
unset($this->writeOperations[$keyString]);
}
public function getStats(): array
{
$analysis = $this->getHeatMapAnalysis();
$bottlenecks = $this->getPerformanceBottlenecks();
return [
'strategy' => 'HeatMapCacheStrategy',
'enabled' => $this->enabled,
'total_tracked_keys' => count($this->heatMap),
'max_tracked_keys' => $this->maxTrackedKeys,
'thresholds' => [
'hot_threshold' => $this->hotThreshold,
'cold_threshold' => $this->coldThreshold,
'analysis_window_hours' => $this->analysisWindowHours,
],
'analysis' => $analysis,
'performance_bottlenecks' => $bottlenecks,
];
}
public function getName(): string
{
return 'heat_map';
}
public function clear(): void
{
$this->heatMap = [];
$this->writeOperations = [];
}
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Get comprehensive heat map analysis
*/
public function getHeatMapAnalysis(): array
{
$hotKeys = [];
$coldKeys = [];
$performanceInsights = [];
$cutoffTime = Timestamp::now()->subtract(Duration::fromHours($this->analysisWindowHours));
foreach ($this->heatMap as $keyString => $entry) {
$recentAccesses = $entry->getRecentAccesses($cutoffTime);
$accessesPerHour = $this->calculateAccessesPerHour($recentAccesses);
if ($accessesPerHour >= $this->hotThreshold) {
$hotKeys[] = [
'key' => $keyString,
'accesses_per_hour' => round($accessesPerHour, 2),
'hit_rate' => round($entry->getHitRate(), 3),
'avg_retrieval_time_ms' => round($entry->getAverageRetrievalTime() * 1000, 2),
'total_accesses' => count($recentAccesses),
];
} elseif ($accessesPerHour <= $this->coldThreshold) {
$coldKeys[] = [
'key' => $keyString,
'accesses_per_hour' => round($accessesPerHour, 2),
'hit_rate' => round($entry->getHitRate(), 3),
'last_access' => $entry->getLastAccessTime()?->format('Y-m-d H:i:s'),
];
}
// Performance insights
if ($entry->getAverageRetrievalTime() > 0.1) { // >100ms
$performanceInsights[] = [
'type' => 'slow_retrieval',
'key' => $keyString,
'avg_time_ms' => round($entry->getAverageRetrievalTime() * 1000, 2),
'recommendation' => 'Consider optimizing data source or adding caching layer',
];
}
if ($entry->getHitRate() < 0.5) { // <50% hit rate
$performanceInsights[] = [
'type' => 'low_hit_rate',
'key' => $keyString,
'hit_rate' => round($entry->getHitRate(), 3),
'recommendation' => 'Review TTL settings or access patterns',
];
}
}
// Sort by priority
usort($hotKeys, fn($a, $b) => $b['accesses_per_hour'] <=> $a['accesses_per_hour']);
usort($coldKeys, fn($a, $b) => $a['accesses_per_hour'] <=> $b['accesses_per_hour']);
return [
'total_tracked_keys' => count($this->heatMap),
'hot_keys' => array_slice($hotKeys, 0, 10),
'cold_keys' => array_slice($coldKeys, 0, 10),
'performance_insights' => $performanceInsights,
'analysis_window_hours' => $this->analysisWindowHours,
];
}
/**
* Get performance bottlenecks with impact scoring
*/
public function getPerformanceBottlenecks(): array
{
$bottlenecks = [];
foreach ($this->heatMap as $keyString => $entry) {
$avgRetrievalTime = $entry->getAverageRetrievalTime();
$hitRate = $entry->getHitRate();
$accessCount = $entry->getTotalAccesses();
// Calculate impact score (higher = more critical)
$impactScore = 0;
// Slow retrieval impact
if ($avgRetrievalTime > 0.1) {
$impactScore += ($avgRetrievalTime * 10) * $accessCount;
}
// Low hit rate impact
if ($hitRate < 0.7) {
$impactScore += (1 - $hitRate) * $accessCount * 5;
}
// High miss rate with frequent access
if ($hitRate < 0.5 && $accessCount > 50) {
$impactScore += 100;
}
if ($impactScore > 10) {
$bottlenecks[] = [
'key' => $keyString,
'impact_score' => $impactScore,
'type' => $this->classifyBottleneck($avgRetrievalTime, $hitRate),
'avg_retrieval_time_ms' => round($avgRetrievalTime * 1000, 2),
'hit_rate' => round($hitRate, 3),
'access_count' => $accessCount,
'recommendation' => $this->getBottleneckRecommendation($avgRetrievalTime, $hitRate),
];
}
}
// Sort by impact score (highest first)
usort($bottlenecks, fn($a, $b) => $b['impact_score'] <=> $a['impact_score']);
return array_slice($bottlenecks, 0, 20); // Top 20 bottlenecks
}
/**
* Get hot keys (most frequently accessed)
*/
public function getHotKeys(int $limit = 10): array
{
$hotKeys = [];
$cutoffTime = Timestamp::now()->subtract(Duration::fromHours($this->analysisWindowHours));
foreach ($this->heatMap as $keyString => $entry) {
$recentAccesses = $entry->getRecentAccesses($cutoffTime);
$accessesPerHour = $this->calculateAccessesPerHour($recentAccesses);
if ($accessesPerHour >= $this->hotThreshold) {
$hotKeys[$keyString] = $accessesPerHour;
}
}
arsort($hotKeys);
return array_slice($hotKeys, 0, $limit, true);
}
private function evictOldestEntry(): void
{
if (empty($this->heatMap)) {
return;
}
$oldestKey = null;
$oldestTime = null;
foreach ($this->heatMap as $keyString => $entry) {
$lastAccess = $entry->getLastAccessTime();
if ($oldestTime === null || ($lastAccess && $lastAccess->isBefore($oldestTime))) {
$oldestTime = $lastAccess;
$oldestKey = $keyString;
}
}
if ($oldestKey !== null) {
unset($this->heatMap[$oldestKey]);
}
}
private function calculateValueSize(mixed $value): int
{
try {
return strlen(serialize($value));
} catch (\Throwable) {
return 0;
}
}
private function calculateAccessesPerHour(array $accesses): float
{
if (empty($accesses)) {
return 0.0;
}
$hours = $this->analysisWindowHours;
return count($accesses) / $hours;
}
private function classifyBottleneck(float $retrievalTime, float $hitRate): string
{
if ($retrievalTime > 0.2) {
return 'slow_retrieval';
}
if ($hitRate < 0.3) {
return 'very_low_hit_rate';
}
if ($hitRate < 0.7) {
return 'low_hit_rate';
}
return 'performance_issue';
}
private function getBottleneckRecommendation(float $retrievalTime, float $hitRate): string
{
if ($retrievalTime > 0.2) {
return 'Optimize data source or add intermediate caching layer';
}
if ($hitRate < 0.3) {
return 'Review cache key patterns and TTL settings';
}
if ($hitRate < 0.7) {
return 'Consider increasing TTL or improving data freshness strategy';
}
return 'Monitor and analyze access patterns';
}
}
/**
* Heat map entry for tracking cache key usage
*/
final class HeatMapEntry
{
/** @var array<array{timestamp: Timestamp, is_hit: bool, retrieval_time: ?float}> */
private array $accesses = [];
private int $totalHits = 0;
private int $totalMisses = 0;
private float $totalRetrievalTime = 0.0;
private int $retrievalTimeCount = 0;
public function __construct(
private readonly CacheKey $key
) {}
public function recordAccess(bool $isHit, ?Duration $retrievalTime = null): void
{
$this->accesses[] = [
'timestamp' => Timestamp::now(),
'is_hit' => $isHit,
'retrieval_time' => $retrievalTime?->toSeconds(),
];
if ($isHit) {
$this->totalHits++;
} else {
$this->totalMisses++;
}
if ($retrievalTime !== null) {
$this->totalRetrievalTime += $retrievalTime->toSeconds();
$this->retrievalTimeCount++;
}
// Keep only recent data to prevent memory bloat
$cutoff = Timestamp::now()->subtract(Duration::fromHours(48)); // Keep 48 hours
$this->accesses = array_filter(
$this->accesses,
fn($access) => $access['timestamp']->isAfter($cutoff)
);
}
public function getRecentAccesses(Timestamp $since): array
{
return array_filter(
$this->accesses,
fn($access) => $access['timestamp']->isAfter($since)
);
}
public function getHitRate(): float
{
$total = $this->totalHits + $this->totalMisses;
return $total > 0 ? ($this->totalHits / $total) : 0.0;
}
public function getAverageRetrievalTime(): float
{
return $this->retrievalTimeCount > 0 ? ($this->totalRetrievalTime / $this->retrievalTimeCount) : 0.0;
}
public function getTotalAccesses(): int
{
return count($this->accesses);
}
public function getLastAccessTime(): ?Timestamp
{
if (empty($this->accesses)) {
return null;
}
return end($this->accesses)['timestamp'];
}
public function getKey(): CacheKey
{
return $this->key;
}
}
/**
* Write operation tracking
*/
final readonly class WriteOperation
{
public function __construct(
public CacheKey $key,
public int $valueSize,
public Duration $writeTime,
public Timestamp $timestamp
) {}
}

View File

@@ -0,0 +1,546 @@
<?php
declare(strict_types=1);
namespace App\Framework\Cache\Strategies;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Predictive Cache Strategy
*
* Uses machine learning patterns to predict cache access and proactively warm cache.
* Analyzes time-based patterns, frequency patterns, and dependency relationships.
*/
final class PredictiveCacheStrategy implements CacheStrategy
{
private const int DEFAULT_PREDICTION_WINDOW_HOURS = 24;
private const int MIN_PATTERN_OCCURRENCES = 3;
private const float DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
private const int MAX_CONCURRENT_WARMING = 5;
/** @var array<string, PredictionPattern> */
private array $patterns = [];
/** @var array<string, WarmingJob> */
private array $activeWarmingJobs = [];
/** @var array<string, WarmingResult> */
private array $warmingHistory = [];
/** @var array<string, callable> */
private array $warmingCallbacks = [];
public function __construct(
private readonly bool $enabled = true,
private readonly int $predictionWindowHours = self::DEFAULT_PREDICTION_WINDOW_HOURS,
private readonly float $confidenceThreshold = self::DEFAULT_CONFIDENCE_THRESHOLD,
private readonly int $maxConcurrentWarming = self::MAX_CONCURRENT_WARMING
) {}
public function onCacheAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void
{
if (!$this->enabled) {
return;
}
$this->recordAccess($key, [
'is_hit' => $isHit,
'retrieval_time_ms' => $retrievalTime?->toMilliseconds() ?? 0,
'time_of_day' => (int) Timestamp::now()->format('H'),
'day_of_week' => (int) Timestamp::now()->format('N'),
]);
}
public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
{
// Predictive strategy doesn't modify TTL
return $originalTtl ?? Duration::fromHours(1);
}
public function onCacheForget(CacheKey $key): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
unset($this->patterns[$keyString]);
unset($this->activeWarmingJobs[$keyString]);
unset($this->warmingHistory[$keyString]);
unset($this->warmingCallbacks[$keyString]);
}
public function getStats(): array
{
$totalPatterns = count($this->patterns);
$activeJobs = count($this->activeWarmingJobs);
$completedWarming = count($this->warmingHistory);
$successfulWarming = 0;
$totalWarmingTime = 0;
foreach ($this->warmingHistory as $result) {
if ($result->successful) {
$successfulWarming++;
}
$totalWarmingTime += $result->duration->toMilliseconds();
}
$avgWarmingTime = $completedWarming > 0 ? ($totalWarmingTime / $completedWarming) : 0;
$successRate = $completedWarming > 0 ? ($successfulWarming / $completedWarming) : 0;
return [
'strategy' => 'PredictiveCacheStrategy',
'enabled' => $this->enabled,
'total_patterns' => $totalPatterns,
'active_warming_jobs' => $activeJobs,
'completed_warming_operations' => $completedWarming,
'successful_warming_operations' => $successfulWarming,
'warming_success_rate' => round($successRate, 3),
'avg_warming_time_ms' => round($avgWarmingTime, 2),
'confidence_threshold' => $this->confidenceThreshold,
'prediction_window_hours' => $this->predictionWindowHours,
];
}
public function getName(): string
{
return 'predictive';
}
public function clear(): void
{
$this->patterns = [];
$this->warmingHistory = [];
$this->warmingCallbacks = [];
// Don't clear active jobs as they're still running
}
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Record access for pattern learning
*/
public function recordAccess(CacheKey $key, array $context = []): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
$timestamp = Timestamp::now();
if (!isset($this->patterns[$keyString])) {
$this->patterns[$keyString] = new PredictionPattern($key);
}
$this->patterns[$keyString]->recordAccess($timestamp, $context);
}
/**
* Record cache dependency relationship
*/
public function recordDependency(CacheKey $primaryKey, CacheKey $dependentKey): void
{
if (!$this->enabled) {
return;
}
$primaryString = $primaryKey->toString();
if (!isset($this->patterns[$primaryString])) {
$this->patterns[$primaryString] = new PredictionPattern($primaryKey);
}
$this->patterns[$primaryString]->addDependency($dependentKey);
}
/**
* Generate predictions based on learned patterns
*/
public function generatePredictions(): array
{
if (!$this->enabled) {
return [];
}
$predictions = [];
$now = Timestamp::now();
foreach ($this->patterns as $keyString => $pattern) {
$keyPredictions = $this->predictKeyAccess($pattern, $now);
$predictions = array_merge($predictions, $keyPredictions);
}
// Sort by confidence score
usort($predictions, fn($a, $b) => $b['confidence'] <=> $a['confidence']);
return $predictions;
}
/**
* Perform predictive warming based on patterns
*/
public function performPredictiveWarming(): array
{
if (!$this->enabled) {
return [];
}
$predictions = $this->generatePredictions();
$warmingResults = [];
foreach ($predictions as $prediction) {
if ($prediction['confidence'] >= $this->confidenceThreshold) {
$result = $this->warmCacheKey(
$prediction['key'],
$prediction['callback'] ?? null,
$prediction['reason']
);
$warmingResults[] = $result;
// Limit concurrent warming operations
if (count($this->activeWarmingJobs) >= $this->maxConcurrentWarming) {
break;
}
}
}
return $warmingResults;
}
/**
* Register a warming callback for a specific key
*/
public function registerWarmingCallback(CacheKey $key, callable $callback): void
{
if (!$this->enabled) {
return;
}
$keyString = $key->toString();
$this->warmingCallbacks[$keyString] = $callback;
if (!isset($this->patterns[$keyString])) {
$this->patterns[$keyString] = new PredictionPattern($key);
}
$this->patterns[$keyString]->setWarmingCallback($callback);
}
/**
* Predict access patterns for a specific key
*/
private function predictKeyAccess(PredictionPattern $pattern, Timestamp $now): array
{
$predictions = [];
$key = $pattern->getKey();
// Time-based prediction (daily patterns)
$timeBasedConfidence = $this->calculateTimeBasedConfidence($pattern, $now);
if ($timeBasedConfidence > 0.3) {
$predictions[] = [
'key' => $key,
'confidence' => $timeBasedConfidence,
'reason' => 'time_based_pattern',
'predicted_access_time' => $now->add(Duration::fromMinutes(30)),
'callback' => $pattern->getWarmingCallback()
];
}
// Frequency-based prediction
$frequencyConfidence = $this->calculateFrequencyConfidence($pattern, $now);
if ($frequencyConfidence > 0.4) {
$predictions[] = [
'key' => $key,
'confidence' => $frequencyConfidence,
'reason' => 'access_frequency_pattern',
'predicted_access_time' => $now->add(Duration::fromMinutes(15)),
'callback' => $pattern->getWarmingCallback()
];
}
// Dependency-based prediction
$dependencyPredictions = $this->predictDependencyAccess($pattern, $now);
$predictions = array_merge($predictions, $dependencyPredictions);
return $predictions;
}
/**
* Calculate confidence based on time patterns (daily/hourly)
*/
private function calculateTimeBasedConfidence(PredictionPattern $pattern, Timestamp $now): float
{
$accesses = $pattern->getRecentAccesses($this->predictionWindowHours);
if (count($accesses) < self::MIN_PATTERN_OCCURRENCES) {
return 0.0;
}
$currentHour = (int) $now->format('H');
$currentDayOfWeek = (int) $now->format('N'); // 1 = Monday, 7 = Sunday
$hourMatches = 0;
$dayMatches = 0;
$totalAccesses = count($accesses);
foreach ($accesses as $access) {
$accessHour = (int) $access['timestamp']->format('H');
$accessDay = (int) $access['timestamp']->format('N');
// Hour pattern matching (±1 hour tolerance)
if (abs($accessHour - $currentHour) <= 1) {
$hourMatches++;
}
// Day pattern matching
if ($accessDay === $currentDayOfWeek) {
$dayMatches++;
}
}
$hourConfidence = $hourMatches / $totalAccesses;
$dayConfidence = $dayMatches / $totalAccesses;
// Combined confidence with hour pattern weighted more heavily
return ($hourConfidence * 0.7) + ($dayConfidence * 0.3);
}
/**
* Calculate confidence based on access frequency patterns
*/
private function calculateFrequencyConfidence(PredictionPattern $pattern, Timestamp $now): float
{
$accesses = $pattern->getRecentAccesses($this->predictionWindowHours);
if (count($accesses) < self::MIN_PATTERN_OCCURRENCES) {
return 0.0;
}
// Calculate average time between accesses
$intervals = [];
for ($i = 1; $i < count($accesses); $i++) {
$interval = $accesses[$i]['timestamp']->diff($accesses[$i-1]['timestamp']);
$intervals[] = $interval->toSeconds();
}
if (empty($intervals)) {
return 0.0;
}
$avgInterval = array_sum($intervals) / count($intervals);
$lastAccess = end($accesses)['timestamp'];
$timeSinceLastAccess = $now->diff($lastAccess)->toSeconds();
// Confidence increases as we approach the expected next access time
$expectedNextAccess = $avgInterval;
$confidence = 1.0 - abs($timeSinceLastAccess - $expectedNextAccess) / $expectedNextAccess;
return max(0.0, min(1.0, $confidence));
}
/**
* Predict access for dependent keys
*/
private function predictDependencyAccess(PredictionPattern $pattern, Timestamp $now): array
{
$predictions = [];
$dependencies = $pattern->getDependencies();
if (empty($dependencies)) {
return $predictions;
}
// If the primary key is likely to be accessed, warm dependencies too
$primaryConfidence = max(
$this->calculateTimeBasedConfidence($pattern, $now),
$this->calculateFrequencyConfidence($pattern, $now)
);
if ($primaryConfidence > 0.5) {
foreach ($dependencies as $dependentKey) {
$predictions[] = [
'key' => $dependentKey,
'confidence' => $primaryConfidence * 0.8, // Slightly lower confidence for dependencies
'reason' => 'dependency_pattern',
'predicted_access_time' => $now->add(Duration::fromMinutes(5)),
'callback' => $this->warmingCallbacks[$dependentKey->toString()] ?? null
];
}
}
return $predictions;
}
/**
* Warm a specific cache key
*/
private function warmCacheKey(CacheKey $key, ?callable $callback, string $reason): array
{
$startTime = Timestamp::now();
$keyString = $key->toString();
// Check if already warming this key
if (isset($this->activeWarmingJobs[$keyString])) {
return [
'key' => $keyString,
'status' => 'already_warming',
'reason' => $reason,
'timestamp' => $startTime->format('H:i:s')
];
}
// Create warming job
$job = new WarmingJob($key, $callback, $reason, $startTime);
$this->activeWarmingJobs[$keyString] = $job;
try {
// If no callback provided, we can't warm the cache
if (!$callback) {
return [
'key' => $keyString,
'status' => 'no_callback',
'reason' => $reason,
'timestamp' => $startTime->format('H:i:s')
];
}
// Execute warming (this would need cache access in real implementation)
$value = $callback();
$endTime = Timestamp::now();
$duration = $startTime->diff($endTime);
// Record warming result
$result = new WarmingResult($key, true, $duration, $reason);
$this->warmingHistory[$keyString] = $result;
return [
'key' => $keyString,
'status' => 'warmed',
'reason' => $reason,
'duration_ms' => $duration->toMilliseconds(),
'timestamp' => $startTime->format('H:i:s')
];
} catch (\Throwable $e) {
$endTime = Timestamp::now();
$duration = $startTime->diff($endTime);
return [
'key' => $keyString,
'status' => 'error',
'reason' => $reason,
'error' => $e->getMessage(),
'duration_ms' => $duration->toMilliseconds(),
'timestamp' => $startTime->format('H:i:s')
];
} finally {
// Remove from active jobs
unset($this->activeWarmingJobs[$keyString]);
}
}
}
/**
* Prediction pattern for a cache key
*/
final class PredictionPattern
{
/** @var array<array{timestamp: Timestamp, context: array}> */
private array $accesses = [];
/** @var array<CacheKey> */
private array $dependencies = [];
private mixed $warmingCallback = null;
public function __construct(
private readonly CacheKey $key
) {}
public function recordAccess(Timestamp $timestamp, array $context = []): void
{
$this->accesses[] = [
'timestamp' => $timestamp,
'context' => $context
];
// Keep only recent accesses to prevent memory bloat
$cutoff = $timestamp->subtract(Duration::fromHours(168)); // 1 week
$this->accesses = array_filter(
$this->accesses,
fn($access) => $access['timestamp']->isAfter($cutoff)
);
}
public function addDependency(CacheKey $dependentKey): void
{
$this->dependencies[] = $dependentKey;
}
public function setWarmingCallback(callable $callback): void
{
$this->warmingCallback = $callback;
}
public function getKey(): CacheKey
{
return $this->key;
}
public function getRecentAccesses(int $hours): array
{
$cutoff = Timestamp::now()->subtract(Duration::fromHours($hours));
return array_filter(
$this->accesses,
fn($access) => $access['timestamp']->isAfter($cutoff)
);
}
public function getDependencies(): array
{
return $this->dependencies;
}
public function getWarmingCallback(): ?callable
{
return $this->warmingCallback;
}
}
/**
* Active warming job
*/
final readonly class WarmingJob
{
public function __construct(
public CacheKey $key,
public mixed $callback,
public string $reason,
public Timestamp $startTime
) {}
}
/**
* Warming operation result
*/
final readonly class WarmingResult
{
public function __construct(
public CacheKey $key,
public bool $successful,
public Duration $duration,
public string $reason
) {}
}

View File

@@ -36,7 +36,7 @@ final readonly class TaggedCache
*/
public function put(string|CacheKey $key, mixed $value, ?Duration $ttl = null): bool
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::fromString($key);
$item = CacheItem::forSet($cacheKey, $value, $ttl);
// Store the item
@@ -55,7 +55,7 @@ final readonly class TaggedCache
*/
public function get(string|CacheKey $key): CacheItem
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::fromString($key);
$result = $this->cache->get($cacheKey);
return $result->getItem($cacheKey);
@@ -66,7 +66,7 @@ final readonly class TaggedCache
*/
public function remember(string|CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
{
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::from($key);
$cacheKey = $key instanceof CacheKey ? $key : CacheKey::fromString($key);
$item = $this->get($cacheKey);
if ($item->isHit) {