Commit 3c65f44a321 for woocommerce
commit 3c65f44a321d04cac34ad1fadd4ed2f4afe59a10
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date: Wed Sep 30 10:49:55 2026 +0100
Add wc_doing_it_wrong() to maybe_init_feature_definitions() (#68922)
* Add doing_it_wrong
* Add changelog entry for feature initialization notice
diff --git a/plugins/woocommerce/changelog/68922-features-controller-reentrancy-notice b/plugins/woocommerce/changelog/68922-features-controller-reentrancy-notice
new file mode 100644
index 00000000000..928823f3000
--- /dev/null
+++ b/plugins/woocommerce/changelog/68922-features-controller-reentrancy-notice
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add a developer notice when feature definitions are accessed while they are being initialized.
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 959d45f541b..6dfb260dfe5 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -131,6 +131,13 @@ class FeaturesController {
*/
private bool $initializing_feature_definitions = false;
+ /**
+ * Flag indicating if a notice for reentrant feature definition access has been issued.
+ *
+ * @var bool
+ */
+ private bool $issued_feature_definitions_notice = false;
+
/**
* Flag indicating if we are currently delaying plugin normalization.
*
@@ -291,7 +298,21 @@ class FeaturesController {
* Initialize hardcoded feature definitions if they have not been initialized yet.
*/
private function maybe_init_feature_definitions(): void {
- if ( ! empty( $this->features ) || $this->initializing_feature_definitions ) {
+ if ( $this->initializing_feature_definitions ) {
+ if ( ! $this->issued_feature_definitions_notice ) {
+ // Set this before issuing the notice because wc_doing_it_wrong() invokes gettext callbacks.
+ $this->issued_feature_definitions_notice = true;
+ wc_doing_it_wrong(
+ __METHOD__,
+ __( 'FeaturesController methods that access feature definitions should not be called while feature definitions are being initialized. Use the woocommerce_init action or later.', 'woocommerce' ),
+ '11.3.0'
+ );
+ }
+
+ return;
+ }
+
+ if ( ! empty( $this->features ) ) {
return;
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
index 36c9faba4fe..73ede7e0e8f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
@@ -287,6 +287,8 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
*/
public function test_feature_definition_initialization_is_reentrant_safe() {
$reflection_class = new \ReflectionClass( $this->sut );
+ $notice_function = FeaturesController::class . '::maybe_init_feature_definitions';
+ $this->setExpectedIncorrectUsage( $notice_function );
$features_property = $reflection_class->getProperty( 'features' );
$features_property->setAccessible( true );
@@ -298,8 +300,9 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
$reentrant_feature_enabled = null;
$translation_count = 0;
+ $notices = array();
- $gettext_filter = function ( $translation, $text, $domain ) use ( &$reentrant_feature_enabled, &$translation_count ) {
+ $gettext_filter = function ( $translation, $text, $domain ) use ( &$reentrant_feature_enabled, &$translation_count ) {
unset( $text, $domain ); // Avoid parameter not used PHPCS errors.
++$translation_count;
@@ -310,17 +313,30 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
return $translation;
};
+ $notice_listener = function ( $function_name, $message, $version ) use ( &$notices, $notice_function ) {
+ if ( $notice_function === $function_name ) {
+ $notices[] = array(
+ 'message' => $message,
+ 'version' => $version,
+ );
+ }
+ };
add_filter( 'gettext', $gettext_filter, 10, 3 );
+ add_action( 'doing_it_wrong_run', $notice_listener, 10, 3 );
try {
$this->sut->get_features( true, false );
} finally {
remove_filter( 'gettext', $gettext_filter, 10 );
+ remove_action( 'doing_it_wrong_run', $notice_listener, 10 );
}
$this->assertFalse( $reentrant_feature_enabled );
$this->assertGreaterThan( 0, $translation_count );
+ $this->assertCount( 1, $notices );
+ $this->assertStringContainsString( 'Use the woocommerce_init action or later.', $notices[0]['message'] );
+ $this->assertSame( '11.3.0', $notices[0]['version'] );
$this->assertNotNull( $this->sut->get_feature_definition( 'order_withdrawal' ) );
}