$config The "page_cache" section of runtime.php. * @param PageStore $store Page store. * @param ResponseEmitter $emitter Output. * @param HitCounter|null $counter Hit/miss counter. */ public function __construct( private readonly array $config, private readonly PageStore $store, private readonly ResponseEmitter $emitter, private readonly ?HitCounter $counter = null, ) { $this->rules = CacheRules::fromArray( is_array( $config['rules'] ?? null ) ? $config['rules'] : [] ); } /** * Handles a request. * * @param RequestContext $request Request. * @param array $headers Request headers keyed by lowercase name. */ public function handle( RequestContext $request, array $headers ): void { if ( true !== ( $this->config['enabled'] ?? false ) || $this->isPreload( $headers ) ) { return; } $decision = ( new CacheabilityPolicy( $this->rules ) )->forRequest( $request ); if ( ! $decision->isCacheable() ) { $this->emitter->header( 'X-Akantio-Cache', 'BYPASS' ); $this->emitter->header( 'X-Akantio-Cache-Reason', (string) $decision->reason()?->value ); return; } $key = CacheKey::fromRequest( $request, $this->rules ); $page = $this->store->fetch( $key, $headers['accept-encoding'] ?? '' ); if ( null === $page ) { $this->emitter->header( 'X-Akantio-Cache', 'MISS' ); $this->counter?->record( 'miss' ); return; } $this->counter?->record( 'hit' ); $this->sendHit( $request, $key, $page, $headers ); } /** * Sends a cache hit. * * @param RequestContext $request Request. * @param CacheKey $key Key. * @param CachedPage $page Page. * @param array $headers Request headers. */ private function sendHit( RequestContext $request, CacheKey $key, CachedPage $page, array $headers ): void { $this->emitter->header( 'Content-Type', 'text/html; charset=UTF-8' ); $this->emitter->header( 'Vary', 'Accept-Encoding' ); $this->emitter->header( 'Last-Modified', gmdate( 'D, d M Y H:i:s', $page->modified ) . ' GMT' ); $this->emitter->header( 'Cache-Control', 'no-cache' ); $this->emitter->header( 'CDN-Cache-Control', 'max-age=' . (int) ( $this->config['cdn_max_age'] ?? 0 ) ); $this->emitter->header( 'Cache-Tag', $key->host() ); $this->emitter->header( 'X-Akantio-Cache', 'HIT' ); $since = isset( $headers['if-modified-since'] ) ? strtotime( $headers['if-modified-since'] ) : false; if ( false !== $since && $since >= $page->modified ) { $this->emitter->status( 304 ); $this->emitter->terminate(); return; } if ( null !== $page->encoding ) { $this->emitter->header( 'Content-Encoding', $page->encoding ); } if ( 'HEAD' !== strtoupper( $request->method ) ) { $this->emitter->sendFile( $page->path ); } $this->emitter->terminate(); } /** * Authentic preload requests must regenerate the page. * * @param array $headers Request headers. */ private function isPreload( array $headers ): bool { $token = (string) ( $this->config['preload_token'] ?? '' ); return '' !== $token && isset( $headers[ self::PRELOAD_HEADER ] ) && hash_equals( $token, $headers[ self::PRELOAD_HEADER ] ); } }