-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fatal error: PHP class_exists() bail out at top of class files is not robust across PHP versions. #58467
Comments
This issue has happened before, e.g. see #54103. |
Possible solutions are: Approach 1: Move the
|
Just to clarify, using a
However by the time PHP "executes" that
|
+1 preference for Approach 2. This is an established pattern for handling this situation (e.g. Minimizing code changes when syncing to Core and not needing to modify the class file better supports that this code is Core code waiting for merge, and I'm sure the smaller diff would be welcome to reviewers and committers. |
In my opinion, approaches 1 and 2 do not necessarily have to conflict with each other. Approach 1 is already implemented; it is simply necessary to prohibit the use of the |
I've raised #58454 which can act as a stub for documenting whichever route we approve |
I'm in favor of Approach 2 as it drastically simplifies syncing code between the Gutenberg plugin and WordPress core. It also makes the review process less intense. |
Heads up: A PR and commit was done yesterday that implements Approach 1. That approach can be iterated upon if Approach 2, which is currently the favorite so far, is selected instead. |
Seems everybody prefers approach 2, another +1 for it. Lest just do it :) The |
Let me reflect on what happened over the last 3 days: if ( class_exists( 'WP_Class' ) ) {
// Return early to avoid loading the class.
// Yes, this approach doesn't always work.
return;
} The only way to guard them now is to wrap them in: if ( ! class_exists( 'WP_Class' ) ) {
// class implementation
} Again, this was already in place; the PR simply disabled the "return early" approach that was possible before the changes introduced by #58500 (1st example above). On a personal note, I am open to approach 2 and am currently in the process of implementing it. |
This problem has come up before and seems to be limited to early PHP 7 versions, but I still don't understand exactly what is happening and why it's manifesting in these particular places with these particular versions. Is there a good explanation of what exactly is happening to trigger this error? I understand the redeclared class error, but why in these files and in these PHP versions? That pattern is not new and not limited to
I've been unable to create a case where php 7.0 and 8.3 behave differently. Here's a gist with some of the things I've been testing. To be clear, I support these changes. I think it's an improvement to move the class check to conditionally require the files. However, I would like to have a better understanding of what is causing this problem in order to avoid it in the future. |
That's a very good question, @sirreal. function foo() {} to the very bottom of the |
I also suspected functions and classes were treated similarly, after reading this section of the
But, as you mention, they're clearly treated differently. Swapping classes and <?php
// Fatal error: Cannot redeclare f()
function f() {}
if (function_exists('f')) { return; }
function f() {} <?php
// No errors
class C {}
if (class_exists('C')) { return; }
class C {} All that to say, I still don't understand how these errors were caused 🙂 |
Same here, @sirreal. I am also unable to reproduce the error. And thank you for raising this issue. Therefore, if anyone would like to test it, I suggest cloning this repository: https://github.com/anton-vlasenko/test_bail_out_using_return. It includes a UPD: I will look into it tomorrow, as the issue could be related to a specific environment, as mentioned by @hellofromtonya. |
@sirreal Hmm, seems to throw a fatal error here regardless if functions or classes, only I'm actually including a file in another file. Are you testing in PHP 7.x? Testing in 7.4.33 here. Also tried this:
<?php
class A {
public function aa() {
echo 'A::aa() was called<br>';
}
}
return;
class B {
public function bb() {
echo 'B::bb() was called<br>';
}
}
<?php
/**
* Plugin Name: Private tests
*/
include plugin_dir_path( __file__ ) . '/incl.php';
$a = new A;
$a->aa();
$b = new B;
$b->bb();
exit; Activating the Private tests plugin shows:
(The Then changing the plugin.php file to: class B {
public function bbb() {
echo 'B::bbb() was called<br>';
}
}
include plugin_dir_path( __file__ ) . '/incl.php';
$a = new A;
$a->aa();
$b = new B;
$b->bb();
exit; outputs:
|
I've created a repo to make this easier. It's currently running PHP 7.0, 7.4, and 8.3. I can define a class, include/require another file that redeclares the same class after an early return, and there's no error. I've been trying to reproduce this in a minimal setup to better understand what's happening, so I'm invoking PHP on the command line. Could there be some difference there? Is there something special about running in WordPress or inside a plugin? I don't understand how PHP can behave so differently in some cases. I'd really like to reach a minimal reproduction case. I understand the error, I understand how to fix it, but I still haven't seen a compelling explanation for why this happens only under very specific circumstances. |
@sirreal It appears that the processing of included files varies depending on the way PHP is used. |
Why isn't it an option to just prefix all those classes with |
Thanks for bringing that up, @swissspidy. |
As I've started working on updating the
if ( class_exists( 'WP_Class' ) ) {
// Return early to avoid loading the class.
return;
} in the class files. In light of these concerns, I propose pausing further development until the issues I outlined above are discussed/resolved. I'd happy to hear any feedback or suggestions regarding this matter. |
Can‘t we just update the PHPCS config to exclude/disable that sniff for certain files? To me that‘s what configs are for. |
Yes, this approach can be taken, thank you for sharing. Having the check inside a class file is 100% reliable and should always work. |
Description
Problem Statement
The current
class_exists()
guard at the top of the plugin's class files is not adequate enough to avoid name conflict fatal errors across all WP supported PHP versions.A
Fatal error: Cannot declare class
can happen on PHP 7.0, 7.1, and 7.3.More Context
A fatal error happened today on
ma.tt
(which runs on WPtrunk
) after packages were updated in Core yesterday:The
WP_Navigation_Block_Renderer
file had aclass_exists()
guard at the top to bail out if that class is already loaded into memory (i.e. that class exists in Core). That guard works on PHP 7.4+, but does not on 7.0, 7.1, and 7.3.Step-by-step reproduction instructions
trunk
.Screenshots, screen recording, code snippet
Environment info
trunk
Please confirm that you have searched existing issues in the repo.
Yes
Please confirm that you have tested with all plugins deactivated except Gutenberg.
Yes
The text was updated successfully, but these errors were encountered: