*/ private $summary; /** * @param (callable(string): ?bool)|null $cachedLookup * @param array $summary */ private function __construct(?callable $cachedLookup, array $summary) { $this->cachedLookup = $cachedLookup; $this->summary = $summary; } /** Read the opcode cache's state for this request. */ public static function read(): self { $summary = self::unavailableSummary(); if (self::apiRestricted()) { $summary['reason'] = 'opcache-api-restricted'; return new self(null, $summary); } $status = ABJ_404_Solution_OpcacheAdapter::status(false); if (!is_array($status) || (array_key_exists('opcache_enabled', $status) && !$status['opcache_enabled'])) { return new self(null, $summary); } return new self( static function (string $path): ?bool { return ABJ_404_Solution_OpcacheAdapter::isScriptCached($path); }, self::summaryFromStatus($summary, $status)); } /** * Build a probe over a known set of cached paths. The seam a test uses to * drive a specific cache state without needing a host whose opcode cache * is in that state; null means "no per-script data". * * @param array|null $cachedPaths */ public static function forCachedPaths(?array $cachedPaths): self { $summary = self::unavailableSummary(); if ($cachedPaths === null) { return new self(null, $summary); } $summary['reason'] = 'available'; $cached = array_fill_keys(array_map('strval', $cachedPaths), true); return new self( static function (string $path) use ($cached): bool { return isset($cached[$path]); }, $summary); } /** * Constant-cost OPcache evidence for one boundary module. * * Early boot checkpoints record only whether this one module is cached * plus the timestamp-validation policy that controls its freshness. The * compiled build marker beside this snapshot provides the exact * generation comparison. * * @return array */ public static function boundarySnapshot(string $path): array { $reason = 'opcache-unavailable'; $cached = null; if (self::apiRestricted()) { $reason = 'opcache-api-restricted'; } else { $cached = ABJ_404_Solution_OpcacheAdapter::isScriptCached($path); if ($cached !== null) { $reason = 'available'; } } return array( 'reason' => $reason, 'cached' => $cached, 'validate_timestamps' => self::iniBoolean(ini_get('opcache.validate_timestamps')), 'revalidate_freq' => self::numericInteger(ini_get('opcache.revalidate_freq')), ); } /** * The per-request summary for the journal. * * @return array */ public function summary(): array { return $this->summary; } /** Whether per-script state is available at all. False means every answer is "unknown". */ public function hasPerScriptData(): bool { return $this->cachedLookup !== null; } /** * Whether OPcache holds this file. Null (not false) when there is no * per-script data, so an unavailable status API is never reported as an * uncached file. */ public function isCached(string $path): ?bool { if ($this->cachedLookup === null) { return null; } return $path !== '' ? ($this->cachedLookup)($path) : false; } /** * Annotate loaded-file fingerprints with their opcode-cache state, keyed * by the `path` each entry already carries. * * @param array> $files * @return array> */ public function annotate(array $files): array { foreach ($files as &$file) { $file['opcache_cached'] = $this->isCached(is_string($file['path'] ?? null) ? $file['path'] : ''); } unset($file); return $files; } /** * Whether opcache.restrict_api bars this plugin from the status API. * Checked before any call because a restricted call answers false with a * warning, which would read as "not cached". */ private static function apiRestricted(): bool { $restrictApi = ini_get('opcache.restrict_api'); return function_exists('abj404_opcache_api_is_restricted') ? abj404_opcache_api_is_restricted($restrictApi, __FILE__) : (is_string($restrictApi) && trim($restrictApi) !== ''); } /** @return array */ private static function unavailableSummary(): array { return array( 'reason' => 'opcache-unavailable', 'validate_timestamps' => self::iniBoolean(ini_get('opcache.validate_timestamps')), 'revalidate_freq' => self::numericInteger(ini_get('opcache.revalidate_freq')), 'restart_pending' => null, 'restart_in_progress' => null, 'start_time' => null, 'last_restart_time' => null, 'restart_counts' => array('oom' => null, 'hash' => null, 'manual' => null), ); } /** * @param array $summary * @param array $status * @return array */ private static function summaryFromStatus(array $summary, array $status): array { $statistics = is_array($status['opcache_statistics'] ?? null) ? $status['opcache_statistics'] : array(); $summary['reason'] = 'available'; $summary['restart_pending'] = isset($status['restart_pending']) ? (bool)$status['restart_pending'] : null; $summary['restart_in_progress'] = isset($status['restart_in_progress']) ? (bool)$status['restart_in_progress'] : null; $summary['start_time'] = self::numericInteger($statistics['start_time'] ?? null); $summary['last_restart_time'] = self::numericInteger($statistics['last_restart_time'] ?? null); $summary['restart_counts'] = array( 'oom' => self::numericInteger($statistics['oom_restarts'] ?? null), 'hash' => self::numericInteger($statistics['hash_restarts'] ?? null), 'manual' => self::numericInteger($statistics['manual_restarts'] ?? null), ); return $summary; } /** @param mixed $value */ private static function iniBoolean($value): ?bool { if ($value === false || $value === null || $value === '' || !is_scalar($value)) { return null; } return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); } /** @param mixed $value */ private static function numericInteger($value): ?int { return ABJ_404_Solution_ExactInteger::read($value, PHP_INT_MIN); } }