Fix crash on PHP 8.3 when a file is missing
| Q | A |
|---|---|
| Is bugfix? | ✔️ |
| New feature? | ❌ |
| Breaks BC? | ❌ |
| Fixed issues |
This fixes a problem I had after migrating Craft CMS (based on Yii2) to PHP 8.3
Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 64.44%. Comparing base (
7037fd4) to head (dd9bc77). Report is 57 commits behind head on master.
Additional details and impacted files
@@ Coverage Diff @@
## master #20358 +/- ##
============================================
- Coverage 64.85% 64.44% -0.41%
- Complexity 11445 11572 +127
============================================
Files 431 433 +2
Lines 37208 37599 +391
============================================
+ Hits 24132 24232 +100
- Misses 13076 13367 +291
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
- :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
How does the crash looks like? Stacktrace?
Error: filemtime(): stat failed. Backtrace:
2025-04-21 19:24:34 [web.INFO] [yii\db\Connection::open] Opening DB connection: mysql:host=127.0.0.1;dbname=craft_blog;port=3306 {"memory":1163464}
2025-04-21 19:24:34 [web.INFO] [yii\web\Session::open] Session started {"memory":1730536}
2025-04-21 19:24:34 [web.INFO] [nystudio107\codeeditor\CodeEditor::bootstrap] CodeEditor module bootstrapped {"memory":1758792}
2025-04-21 19:24:34 [web.ERROR] [yii\base\ErrorException:2] yii\base\ErrorException: filemtime(): stat failed for /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/cache/3b/CraftCMS--c2e6b7e4-c13e-492e-9c09-b5252006db833b35b093591572843edcc341f17455a0.bin in /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php:113
Stack trace:
#0 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/ErrorHandler.php(115): craft\web\ErrorHandler->handleError(code: '...', message: '...', file: '...', line: '...')
#1 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php(113): craft\web\ErrorHandler->handleError(code: '...', message: '...', file: '...', line: '...')
#2 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php(113): ::filemtime(filename: '...')
#3 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/Cache.php(134): craft\cache\FileCache->getValue(key: '...')
#4 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/services/ProjectConfig.php(1700): craft\cache\FileCache->get(key: '...')
#5 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/services/ProjectConfig.php(695): craft\services\ProjectConfig->getHadFileWriteIssues()
#6 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/helpers/Cp.php(207): craft\services\ProjectConfig->areChangesPending(path: '...', force: '...')
#7 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/twig/variables/Cp.php(529): craft\helpers\Cp::alerts(path: '...', fetch: '...')
#8 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Extension/CoreExtension.php(1861): craft\web\twig\variables\Cp->getAlerts()
#9 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/helpers/Template.php(148): Twig\Extension\CoreExtension::getAttribute(env: '...', source: '...', object: '...', item: '...', arguments: '...', type: '...', isDefinedTest: '...', ignoreStrictCheck: '...', sandboxed: '...', lineno: '...')
#10 /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/compiled_templates/11/11779b7006abc2b2e3d49db10de92a46.php(46): craft\helpers\Template::attribute(env: '...', source: '...', object: '...', item: '...', arguments: '...', type: '...', isDefinedTest: '...', ignoreStrictCheck: '...', sandboxed: '...', lineno: '...')
#11 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Template.php(343): __TwigTemplate_e1a825c815db98fb38f1f494a5134d31->doDisplay(context: '...', blocks: '...')
#12 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Template.php(358): __TwigTemplate_909b19cdb5d17880fc3de7ef93bebdc0->display(context: '...', blocks: '...')
#13 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/TemplateWrapper.php(35): __TwigTemplate_909b19cdb5d17880fc3de7ef93bebdc0->render(context: '...')
#14 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/twig/twig/src/Environment.php(320): Twig\TemplateWrapper->render(context: '...')
#15 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/View.php(581): craft\web\twig\Environment->render(name: '...', context: '...')
#16 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/View.php(634): craft\web\View->renderTemplate(template: '...', variables: '...', templateMode: '...')
#17 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/TemplateResponseFormatter.php(57): craft\web\View->renderPageTemplate(template: '...', variables: '...', templateMode: '...')
#18 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/web/Response.php(1109): craft\web\TemplateResponseFormatter->format(response: '...')
#19 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/craftcms/cms/src/web/Response.php(341): craft\web\Response->prepare()
#20 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/web/Response.php(340): craft\web\Response->prepare()
#21 /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/base/Application.php(390): craft\web\Response->send()
#22 /mnt/projects/sites/blog.ekhlakovy.ru/www/webhome/index.php(12): craft\web\Application->run()
#23 {main} {"memory":10969160,"exception":"[object] (yii\\base\\ErrorException(code: 2): filemtime(): stat failed for /mnt/projects/sites/blog.ekhlakovy.ru/www/storage/runtime/cache/3b/CraftCMS--c2e6b7e4-c13e-492e-9c09-b5252006db833b35b093591572843edcc341f17455a0.bin at /mnt/projects/sites/blog.ekhlakovy.ru/www/vendor/yiisoft/yii2/caching/FileCache.php:113)"}
Warning Prior to PHP 8.0.0, the error_reporting() called inside the custom error handler always returned 0 if the error was suppressed by the @ operator. As of PHP 8.0.0, it returns the value of this (bitwise) expression: E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.
As per code yii\base\ErrorHandler::handleError
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$exception = new ErrorException($message, $code, $code, $file, $line);
if (PHP_VERSION_ID < 70400) {
// prior to PHP 7.4 we can't throw exceptions inside of __toString() - it will result a fatal error
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] === '__toString') {
$this->handleException($exception);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
}
throw $exception;
}
return false;
}
So the handleError will show error page at the end for PHP ver >= 8.0.0
for example:
set_error_handler(function($severity, $message, $file, $line) {
echo "Error handler called! Message: $message\n";
echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+
return true; // Prevent PHP's default error handler from running
});
$filename = "non_existent_file.txt";
$mtime = @filemtime($filename);
if ($mtime === false) {
echo "filemtime failed (returned false)\n";
}
As of PHP 8.0.0, it returns the value of this (bitwise) expression: E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.
filemtime() emits E_WARNING, so this change doesn't affect this call.
set_error_handler(function($severity, $message, $file, $line) { echo "Error handler called! Message: $message\n"; echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+ return true; // Prevent PHP's default error handler from running });
$filename = "non_existent_file.txt"; $mtime = @filemtime($filename);
if ($mtime === false) { echo "filemtime failed (returned false)\n"; }
I'm not sure, maybe because my error_level still contain E_WARNING
Error handler called! Message: filemtime(): stat failed for non_existent_file.txt
error_reporting() inside handler: 4437
filemtime failed (returned false)
But I tried the simple test on 8.2, it is shown the error
set_error_handler(function($severity, $message, $file, $line) {
echo "Error handler called! Message: $message\n";
echo "error_reporting() inside handler: " . error_reporting() . "\n"; // 4437 in PHP 8+
return true; // Prevent PHP's default error handler from running
});
$filename = "non_existent_file.txt";
$mtime = @filemtime($filename);
if ($mtime === false) {
echo "filemtime failed (returned false)\n";
}
Related #19773 :-)
I’ve updated this PR to address the filemtime() warning issue in a more efficient way. The changes:
-
Replaced @filemtime with set_error_handler to gracefully catch "file not found" warnings without costly file_exists() checks. -
Ensured PHP 7.3+ compatibility by using strpos() instead of str_contains(). -
Fixed both occurrences of this pattern in the codebase.
Why this matters:
-
In Craft CMS’s DEV mode, even warnings (like missing files) trigger exceptions, breaking execution. -
This solution avoids disk-heavy operations while maintaining silent failure for missing files. -
No performance penalty vs. @, but more explicit and debuggable.
Let me know if you’d like any adjustments!
IMO - setting and restoring error handler for each cached file is a terrible approach. Did you check performance penalty? If you are not OK with extra non-atomic check is_file() then best leave as it is.
To provide perfect solution this can hardly be solved at user-code level ie. with file lock .. or introduce new non-atomic command ie. filemtime_if_exists() :-)
Can someone explain what specific Craft CMS is doing, that silenced warnings are converted to exceptions?
@brandonkelly would you please point us to the error handing routine of CraftCMS? Thanks.