opcache fix in CompiledFile

Signed-off-by: Andy Miller <rhuk@mac.com>
This commit is contained in:
Andy Miller
2025-12-05 20:59:46 -07:00
parent fae70e5fc9
commit 80410dae13
3 changed files with 239 additions and 57 deletions

44
composer.lock generated
View File

@@ -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",

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}