array, 'html' => string]` per the * {@see \Affilun\Application\Cache\RenderCache::get()} * contract; on hit the same shape is returned verbatim. * * The adapter is layer-isolated: callers are expected to inject a * dedicated {@see InMemoryCacheStore} instance per layer so a * flush of the render layer does not also drop entries belonging * to the search, lookup, or provider-health layers. Sharing a * single store across multiple adapters is permitted but is the * caller's responsibility. * * @package Affilun\Infrastructure\Cache */ declare(strict_types=1); namespace Affilun\Infrastructure\Cache; use Affilun\Application\Cache\RenderCache; /** * In-memory {@see RenderCache} backed by {@see InMemoryCacheStore}. * * See the file-level docblock for the entry shape, the * layer-isolation rule, and the §14 prefix-wildcard invalidation * hook used by the `CacheInvalidator`. */ final class InMemoryRenderCache implements RenderCache { /** * Backing generic store; carries the entries, the clock, and * the TTL eviction logic shared across cache layers. */ private InMemoryCacheStore $store; /** * Construct the adapter. * * @param InMemoryCacheStore $store Backing in-memory store. */ public function __construct( InMemoryCacheStore $store ) { $this->store = $store; } /** * Read the cached `['view_model' => array, 'html' => string]` * entry, or null on miss / expired. * * @param string $key Cache key. * * @return array{view_model: array, html: string}|null */ public function get( string $key ): ?array { $entry = $this->store->getRaw( $key ); if ( null === $entry ) { return null; } $value = $entry->value(); if ( ! is_array( $value ) ) { return null; } if ( ! array_key_exists( 'view_model', $value ) || ! array_key_exists( 'html', $value ) ) { return null; } $viewModel = $value['view_model']; $html = $value['html']; if ( ! is_array( $viewModel ) || ! is_string( $html ) ) { return null; } /** @var array $viewModel */ return array( 'view_model' => $viewModel, 'html' => $html, ); } /** * Write the rendered entry with the given TTL in seconds. * * The stored value is the two-key associative array * `['view_model' => $viewModel, 'html' => $html]` per the * {@see RenderCache::get()} contract. Delegates TTL * validation to {@see InMemoryCacheStore::putRaw()}. * * @param string $key Cache key. * @param array $viewModel View model snapshot. * @param string $html Rendered HTML. * @param int $ttlSeconds TTL in seconds; must be > 0. */ public function put( string $key, array $viewModel, string $html, int $ttlSeconds ): void { $this->store->putRaw( $key, array( 'view_model' => $viewModel, 'html' => $html, ), $ttlSeconds ); } /** * Remove the entry stored under `$key`. Idempotent. * * @param string $key Cache key. */ public function forget( string $key ): void { $this->store->forget( $key ); } /** * Drop every entry stored in this layer's backing store. */ public function flush(): void { $this->store->flush(); } /** * Drop every entry whose key begins with `$prefix` and return * the count of removed entries. * * Delegates to {@see InMemoryCacheStore::forgetByPrefix()}, * which rejects an empty `$prefix` as a defensive guard * against accidental layer-wide flushes. * * @param string $prefix Cache-key prefix. * * @return int Count of removed entries (>= 0). */ public function forgetByPrefix( string $prefix ): int { return $this->store->forgetByPrefix( $prefix ); } }