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