Hi everyone
We recently received a bug report regarding the behavior of
opcache_compile_file()
[1]. The documentation specifies:
https://www.php.net/manual/en/function.opcache-compile-file.php
This function compiles a PHP script and adds it to the opcode cache without executing it. This can be used to prime the cache after a Web server restart by pre-caching files that will be included in later requests.
Arguably, "without executing it" implies that, aside from putting the
script into opcache, there are no other observable side-effects. This
assumption is currently incorrect. To be more specific,
opcache_compile_file()
differs from require in two ways:
- The "main" function of the script (containing all the code at the
top-level) is not executed. - Classes will not be added to the class table.
Confusingly, top-level functions will be added to the function
table. This has some weird consequences:
-
opcache_compile_file()
can be called multiple times on files
containing classes. However, the same is not true for files containing
functions. The second call will lead to a function redeclaration
error.
// index.php
opcache_compile_file(__DIR__ . '/test.php');
opcache_compile_file(__DIR__ . '/test.php');
// test.php
class Foo {} // No problem
function foo() {} // Fatal error: Cannot redeclare function foo()
- Similarly, after calling
opcache_compile_file()
on files containing
classes, the same file may later be required without issues. This does
not work for files containing functions for the same reason.
// index.php
opcache_compile_file(__DIR__ . '/test.php');
require __DIR__ . '/test.php';
// test.php
class Foo {} // No problem
function foo() {} // Fatal error: Cannot redeclare function foo()
- Mixing functions with classes is incompatible. An attempt to use one
of the classes from one of the functions will either error because of
an undeclared class, or trigger the autoloader and include the file
again, leading to a function redeclaration error.
// index.php
spl_autoload_register(function ($name) {
if ($name === 'Foo') {
require __DIR__ . '/b.php';
}
});
opcache_compile_file(__DIR__ . '/test.php');
foo();
// test.php
class Foo {}
function foo() {
// Triggers the autoloader, the autoloader fails due to:
// Fatal error: Cannot redeclare function foo()
var_dump(new Foo());
}
- Functions that are conditionally declared will not be added to the
function table, since they are not top-level functions, even if the
condition always evaluates to true.
// index.php
opcache_compile_file(__DIR__ . '/test.php');
foo(); // Uncaught Error: Call to undefined function foo()
// test.php
if (true) {
function foo() {}
}
This behavior is inconsistent and confusing. Arguably, the correct
behavior is to never register functions in opcache_compile_file()
to
begin with, since opcache_compile_file()
exists to prime the cache,
rather than execute code. I created a PR with this change [2]. This is
breaking, since code using opcache_compile_file()
might currently
depend on functions being declared and then calling them directly.
Such code would have to be adjusted to use require instead.
Are there any concerns with making this change for 8.5? Are there any
use-cases this would break?
Of note is that it may be beneficial to provide a related function
that allows compiling files and declaring symbols without executing
the "main" function. This could be useful static analysis tools that
want to reflect on files without executing them. That said, there are
existing solutions in userland that solve this problem in a better
way, like BetterReflection [3].
Ilija
[1] https://github.com/php/php-src/issues/16668
[2] https://github.com/php/php-src/pull/16862
[3] https://github.com/Roave/BetterReflection