Commit d6dc8bb20bf for woocommerce
commit d6dc8bb20bfb923ae69b8e065fa12f090e588c3f
Author: Raluca Stan <ralucastn@gmail.com>
Date: Fri Sep 18 10:35:40 2026 +0200
Fix double block registration when a description renders before init (#68522)
* Fix register_blocks() re-registering block types on init after an on-demand run
register_blocks() set $register_blocks_has_run but never read it. Only the
on-demand call site in Bootstrap checked the flag, so when an extension
rendered a product description before init, the queued init callback ran
the full registration a second time: every block class was constructed
again and hooked a second set of callbacks, and the registry emitted a
_doing_it_wrong notice per block.
Return early from register_blocks() itself so both entry paths share the
guard.
* Stop expecting the block re-registration notice in the email-editor tests
These tests fire init a second time in the same process and swallowed the
WP_Block_Type_Registry::register notice that the second run produced. With
register_blocks() guarding itself, blocks register once per process and the
notice no longer fires, so the expectation would fail as untriggered.
* Add a regression test for the init registration after an on-demand run
Render a description with a block before init, then call register_blocks()
as the queued init callback would. Count the enqueue_block_editor_assets
callbacks, which every constructed block instance hooks, and assert the
count is unchanged after the second call. Without the fix a double run
hooks a second set of callbacks.
* Add changelog entry for the register_blocks() double-run fix
diff --git a/plugins/woocommerce/changelog/fix-register-blocks-double-run b/plugins/woocommerce/changelog/fix-register-blocks-double-run
new file mode 100644
index 00000000000..83d3e7c4a23
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-register-blocks-double-run
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent WooCommerce block types from being registered twice when a product description is rendered before init.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index 21c3ac94c89..2dac45e144d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -131,8 +131,11 @@ final class BlockTypesController {
* Register blocks, hooking up assets and render functions as needed.
*/
public function register_blocks() {
- // Set before registering rather than after: it guards against re-entry through the on-demand
- // registration in Bootstrap, and a registration failure must not be retried on later filter fires.
+ if ( self::$register_blocks_has_run ) {
+ return;
+ }
+
+ // Set before registering rather than after, so a registration failure is not retried on a later call.
self::$register_blocks_has_run = true;
$this->register_block_metadata();
$block_types = $this->get_block_types();
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
index ae532bda43b..216ca11d94d 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
@@ -4,6 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\Domain;
use Automattic\WooCommerce\Blocks\BlockTypesController;
+use Automattic\WooCommerce\Blocks\Package;
use WC_Unit_Test_Case;
use WP_Block_Type_Registry;
@@ -368,4 +369,51 @@ HTML;
'Block types should remain registered and must not be re-registered on demand.'
);
}
+
+ /**
+ * @testdox The init registration is a no-op after a description registered the block types on demand before init.
+ */
+ public function test_register_blocks_is_a_no_op_after_on_demand_registration(): void {
+ $registry = WP_Block_Type_Registry::get_instance();
+ $controller = Package::container()->get( BlockTypesController::class );
+ $this->assertFalse( $registry->is_registered( self::SAMPLE_BLOCK ), 'Blocks should start unregistered.' );
+
+ // An extension renders a description before init on a request where register_blocks() is queued on init.
+ apply_filters( 'woocommerce_short_description', 'Intro <!-- wp:woocommerce/product-price /--> outro' );
+ $this->assertTrue( $controller->register_blocks_has_run(), 'The filter should have run register_blocks() on demand.' );
+ $this->assertTrue( $registry->is_registered( self::SAMPLE_BLOCK ), 'The filter should register block types on demand.' );
+ $hooked_block_instances = $this->count_block_editor_asset_callbacks();
+ $this->assertGreaterThan( 0, $hooked_block_instances, 'Each registered block instance should have hooked its editor assets.' );
+
+ // Then the queued init callback fires. The registry rejects duplicate block types with a doing_it_wrong
+ // notice (a test failure), but a second run would still construct every block class again and hook each
+ // new instance a second time.
+ $controller->register_blocks();
+
+ $this->assertSame(
+ $hooked_block_instances,
+ $this->count_block_editor_asset_callbacks(),
+ 'A second register_blocks() call must not construct the block types again and hook duplicate callbacks.'
+ );
+ $this->assertTrue( $registry->is_registered( self::SAMPLE_BLOCK ), 'Block types should remain registered.' );
+ }
+
+ /**
+ * Count the callbacks hooked to enqueue_block_editor_assets.
+ *
+ * Every block instance that register_blocks() constructs hooks its enqueue_editor_assets method there, so
+ * the count grows by one per block type each time the registration runs.
+ *
+ * @return int Number of hooked callbacks.
+ */
+ private function count_block_editor_asset_callbacks(): int {
+ global $wp_filter;
+
+ $count = 0;
+ foreach ( $wp_filter['enqueue_block_editor_assets']->callbacks ?? array() as $callbacks_at_priority ) {
+ $count += count( $callbacks_at_priority );
+ }
+
+ return $count;
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateDivergenceDetectorTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateDivergenceDetectorTest.php
index 21d78667701..33880e07257 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateDivergenceDetectorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailTemplateDivergenceDetectorTest.php
@@ -563,12 +563,11 @@ class WCEmailTemplateDivergenceDetectorTest extends \WC_Unit_Test_Case {
* `init`-time hooks (notably `WCEmailTemplateDivergenceDetector::register_meta`)
* register on the global hook table, the `woo_email` post type is registered, and
* `init` fires so the meta-registration callback runs. Swallows the doing-it-wrong
- * notices that the full chain triggers when re-registering already-registered
- * blocks / integrations during a unit-test process; those notices are unrelated
+ * notice that the full chain triggers when re-registering already-registered
+ * integrations during a unit-test process; that notice is unrelated
* to the meta-registration wiring under test.
*/
private function initialize_email_editor_integration(): void {
- $this->setExpectedIncorrectUsage( 'WP_Block_Type_Registry::register' );
$this->setExpectedIncorrectUsage( 'Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry::register' );
add_option( 'woocommerce_feature_block_email_editor_enabled', 'yes' );