From 80410dae138a10bdcf34d43453f1c060dfe33639 Mon Sep 17 00:00:00 2001 From: Andy Miller Date: Fri, 5 Dec 2025 20:59:46 -0700 Subject: [PATCH] opcache fix in CompiledFile Signed-off-by: Andy Miller --- composer.lock | 44 ++-- system/src/Grav/Common/File/CompiledFile.php | 53 +++-- .../Common/Service/ConfigServiceProvider.php | 199 ++++++++++++++++-- 3 files changed, 239 insertions(+), 57 deletions(-) diff --git a/composer.lock b/composer.lock index a5e35b43e..4435ae842 100644 --- a/composer.lock +++ b/composer.lock @@ -4846,16 +4846,16 @@ }, { "name": "codeception/stub", - "version": "4.2.0", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "19014cec368cefc0579499779c451551cd288557" + "reference": "0c573cd5c62a828dadadc41bc56f8434860bb7bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/19014cec368cefc0579499779c451551cd288557", - "reference": "19014cec368cefc0579499779c451551cd288557", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/0c573cd5c62a828dadadc41bc56f8434860bb7bb", + "reference": "0c573cd5c62a828dadadc41bc56f8434860bb7bb", "shasum": "" }, "require": { @@ -4881,9 +4881,9 @@ "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", "support": { "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/4.2.0" + "source": "https://github.com/Codeception/Stub/tree/4.2.1" }, - "time": "2025-08-01T08:15:29+00:00" + "time": "2025-12-05T13:37:14+00:00" }, { "name": "getgrav/markdowndocs", @@ -5451,11 +5451,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -5500,7 +5500,7 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -5886,16 +5886,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.45", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "faf5fff4fb9beb290affa53f812b05380819c51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/faf5fff4fb9beb290affa53f812b05380819c51a", + "reference": "faf5fff4fb9beb290affa53f812b05380819c51a", "shasum": "" }, "require": { @@ -5967,7 +5967,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.45" }, "funding": [ { @@ -5991,7 +5991,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2025-12-01T07:38:43+00:00" }, { "name": "psr/http-client", @@ -6126,16 +6126,16 @@ }, { "name": "rector/rector", - "version": "2.2.9", + "version": "2.2.11", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "0b8e49ec234877b83244d2ecd0df7a4c16471f05" + "reference": "7bd21a40b0332b93d4bfee284093d7400696902d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/0b8e49ec234877b83244d2ecd0df7a4c16471f05", - "reference": "0b8e49ec234877b83244d2ecd0df7a4c16471f05", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/7bd21a40b0332b93d4bfee284093d7400696902d", + "reference": "7bd21a40b0332b93d4bfee284093d7400696902d", "shasum": "" }, "require": { @@ -6174,7 +6174,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.9" + "source": "https://github.com/rectorphp/rector/tree/2.2.11" }, "funding": [ { @@ -6182,7 +6182,7 @@ "type": "github" } ], - "time": "2025-11-28T14:21:22+00:00" + "time": "2025-12-02T11:23:46+00:00" }, { "name": "sebastian/cli-parser", diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php index 20f1be914..be0acb5c8 100644 --- a/system/src/Grav/Common/File/CompiledFile.php +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -28,6 +28,7 @@ trait CompiledFile /** * Get/set parsed file contents. * + * @param mixed $var * @return array */ public function content(mixed $var = null) @@ -36,19 +37,47 @@ trait CompiledFile $filename = $this->filename; // If nothing has been loaded, attempt to get pre-compiled version of the file first. if ($var === null && $this->raw === null && $this->content === null) { - $key = md5((string) $filename); + $key = md5($filename); $file = PhpFile::instance(CACHE_DIR . "compiled/files/{$key}{$this->extension}.php"); + $cacheFilename = $file->filename(); - $modified = $this->modified(); - if (!$modified) { + // Improved support for opcache + // Do not check timestamp if opcache.validate_timestamps = 0 + // https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.validate-timestamps + $modified = false; + + if (!filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) || + filter_var(ini_get('opcache.validate_timestamps'), \FILTER_VALIDATE_BOOLEAN)) { + $modified = $this->modified(); + } + + // Improved support for opcache + // + // If not modified and file exists in opcache + // This requires opcache.enable_file_override = 1 + // https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enable-file-override + if (!$modified && is_file($cacheFilename)) { try { - return $this->decode($this->raw()); + // Include the file directly to trigger loading from opcache + $var = (array) include $cacheFilename; + + if (is_array($var) && isset($var['data'])) { + $var = $var['data']; + } else { + $var = null; + } + + if (!is_array($var)) { + $var = $this->decode($this->raw()); + } + + return $var; } catch (Throwable) { // If the compiled file is broken, we can safely ignore the error and continue. } } - $class = $this::class; + $class = get_class($this); $size = filesize($filename); $cache = $file->exists() ? $file->content() : null; @@ -88,11 +117,9 @@ trait CompiledFile // Compile cached file into bytecode cache if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { - $lockName = $file->filename(); - // Silence error if function exists, but is restricted. - @opcache_invalidate($lockName, true); - @opcache_compile_file($lockName); + @opcache_invalidate($cacheFilename, true); + @opcache_compile_file($cacheFilename); } } } @@ -134,7 +161,7 @@ trait CompiledFile if ($locked) { $modified = $this->modified(); $filename = $this->filename; - $class = $this::class; + $class = get_class($this); $size = filesize($filename); // windows doesn't play nicely with this as it can't read when locked @@ -158,10 +185,10 @@ trait CompiledFile // Compile cached file into bytecode cache if (function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { - $lockName = $file->filename(); + $cacheFilename = $file->filename(); // Silence error if function exists, but is restricted. - @opcache_invalidate($lockName, true); - @opcache_compile_file($lockName); + @opcache_invalidate($cacheFilename, true); + @opcache_compile_file($cacheFilename); } } } diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index fb3959b12..ecf38286a 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -20,6 +20,7 @@ use Grav\Common\Language\Language; use Grav\Framework\Mime\MimeTypes; use Pimple\Container; use Pimple\ServiceProviderInterface; +use RocketTheme\Toolbox\File\PhpFile; use RocketTheme\Toolbox\File\YamlFile; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; @@ -85,15 +86,24 @@ class ConfigServiceProvider implements ServiceProviderInterface /** @var UniformResourceLocator $locator */ $locator = $container['locator']; - $cache = $locator->findResource('cache://compiled/blueprints', true, true); + $cache = $locator->findResource('cache://compiled/blueprints', true, true); - $files = []; - $paths = $locator->findResources('blueprints://config'); - $files += (new ConfigFileFinder)->locateFiles($paths); - $paths = $locator->findResources('plugins://'); - $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints'); - $paths = $locator->findResources('themes://'); - $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths, 'blueprints'); + // Try to load cached file list to avoid filesystem scanning on every request + $files = static::loadCachedFileList($locator, $cache, 'blueprints', $setup->environment); + + if ($files === null) { + // Cache miss - scan filesystem for blueprint files + $files = []; + $paths = $locator->findResources('blueprints://config'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints'); + $paths = $locator->findResources('themes://'); + $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths, 'blueprints'); + + // Save file list cache for next request + static::saveCachedFileList($locator, $cache, 'blueprints', $setup->environment, $files); + } $blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT); @@ -112,15 +122,24 @@ class ConfigServiceProvider implements ServiceProviderInterface /** @var UniformResourceLocator $locator */ $locator = $container['locator']; - $cache = $locator->findResource('cache://compiled/config', true, true); + $cache = $locator->findResource('cache://compiled/config', true, true); - $files = []; - $paths = $locator->findResources('config://'); - $files += (new ConfigFileFinder)->locateFiles($paths); - $paths = $locator->findResources('plugins://'); - $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); - $paths = $locator->findResources('themes://'); - $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths); + // Try to load cached file list to avoid filesystem scanning on every request + $files = static::loadCachedFileList($locator, $cache, 'config', $setup->environment); + + if ($files === null) { + // Cache miss - scan filesystem for config files + $files = []; + $paths = $locator->findResources('config://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); + $paths = $locator->findResources('themes://'); + $files += (new ConfigFileFinder)->setBase('themes')->locateInFolders($paths); + + // Save file list cache for next request + static::saveCachedFileList($locator, $cache, 'config', $setup->environment, $files); + } $compiled = new CompiledConfig($cache, $files, GRAV_ROOT); $compiled->setBlueprints(fn() => $container['blueprints']); @@ -151,12 +170,22 @@ class ConfigServiceProvider implements ServiceProviderInterface // Process languages only if enabled in configuration. if ($config->get('system.languages.translations', true)) { - $paths = $locator->findResources('languages://'); - $files += (new ConfigFileFinder)->locateFiles($paths); - $paths = $locator->findResources('plugins://'); - $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages'); - $paths = static::pluginFolderPaths($paths, 'languages'); - $files += (new ConfigFileFinder)->locateFiles($paths); + // Try to load cached file list to avoid filesystem scanning on every request + $files = static::loadCachedFileList($locator, $cache, 'languages', $setup->environment); + + if ($files === null) { + // Cache miss - scan filesystem for language files + $files = []; + $paths = $locator->findResources('languages://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages'); + $paths = static::pluginFolderPaths($paths, 'languages'); + $files += (new ConfigFileFinder)->locateFiles($paths); + + // Save file list cache for next request + static::saveCachedFileList($locator, $cache, 'languages', $setup->environment, $files); + } } $languages = new CompiledLanguages($cache, $files, GRAV_ROOT); @@ -195,4 +224,130 @@ class ConfigServiceProvider implements ServiceProviderInterface } return $paths; } + + /** + * Load cached file list if still valid (based on directory mtimes). + * + * @param UniformResourceLocator $locator + * @param string $cacheDir + * @param string $type + * @param string $environment + * @return array|null Returns cached files array or null if cache is invalid + */ + protected static function loadCachedFileList(UniformResourceLocator $locator, string $cacheDir, string $type, string $environment): ?array + { + $cacheFile = "{$cacheDir}/filelist-{$type}-{$environment}.php"; + + if (!file_exists($cacheFile)) { + return null; + } + + $cache = include $cacheFile; + + if (!is_array($cache) || !isset($cache['directories'], $cache['files'])) { + return null; + } + + // Validate cache by checking directory mtimes + foreach ($cache['directories'] as $dir => $mtime) { + // Check if directory still exists and mtime hasn't changed + $currentMtime = @filemtime($dir); + if ($currentMtime === false || $currentMtime !== $mtime) { + return null; + } + } + + return $cache['files']; + } + + /** + * Save file list to cache with directory mtimes for validation. + * + * @param UniformResourceLocator $locator + * @param string $cacheDir + * @param string $type + * @param string $environment + * @param array $files + * @return void + */ + protected static function saveCachedFileList(UniformResourceLocator $locator, string $cacheDir, string $type, string $environment, array $files): void + { + // Collect all directories that were scanned based on type + $directories = []; + + // Type-specific base directories + if ($type === 'config') { + $basePaths = $locator->findResources('config://'); + foreach ($basePaths as $path) { + if (is_dir($path)) { + $directories[$path] = filemtime($path); + } + } + } elseif ($type === 'blueprints') { + $basePaths = $locator->findResources('blueprints://config'); + foreach ($basePaths as $path) { + if (is_dir($path)) { + $directories[$path] = filemtime($path); + } + } + } elseif ($type === 'languages') { + $basePaths = $locator->findResources('languages://'); + foreach ($basePaths as $path) { + if (is_dir($path)) { + $directories[$path] = filemtime($path); + } + } + } + + // Get plugin directories (used by all types) + $pluginPaths = $locator->findResources('plugins://'); + foreach ($pluginPaths as $path) { + if (is_dir($path)) { + $directories[$path] = filemtime($path); + // Also track individual plugin directories for granular invalidation + $iterator = new DirectoryIterator($path); + foreach ($iterator as $dir) { + if ($dir->isDir() && !$dir->isDot()) { + $directories[$dir->getPathname()] = $dir->getMTime(); + } + } + } + } + + // Get theme directories (used by config and blueprints) + if ($type !== 'languages') { + $themePaths = $locator->findResources('themes://'); + foreach ($themePaths as $path) { + if (is_dir($path)) { + $directories[$path] = filemtime($path); + // Also track individual theme directories + $iterator = new DirectoryIterator($path); + foreach ($iterator as $dir) { + if ($dir->isDir() && !$dir->isDot()) { + $directories[$dir->getPathname()] = $dir->getMTime(); + } + } + } + } + } + + $cache = [ + '@class' => static::class, + 'type' => $type, + 'environment' => $environment, + 'timestamp' => time(), + 'directories' => $directories, + 'files' => $files, + ]; + + // Ensure cache directory exists + if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0775, true); + } + + $cacheFile = "{$cacheDir}/filelist-{$type}-{$environment}.php"; + $file = PhpFile::instance($cacheFile); + $file->save($cache); + $file->free(); + } }