Skip to content

Fix/issue WOOTAX-298 - Prevent StoreApi fatal on sites running old WooCommerce#2971

Open
iyut wants to merge 5 commits into
trunkfrom
fix/issue-wootax-298
Open

Fix/issue WOOTAX-298 - Prevent StoreApi fatal on sites running old WooCommerce#2971
iyut wants to merge 5 commits into
trunkfrom
fix/issue-wootax-298

Conversation

@iyut

@iyut iyut commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Description

WooCommerce Tax was throwing a PHP fatal error on sites running an older WooCommerce version that does not include the Automattic\WooCommerce\StoreApi\StoreApi class (introduced when the Blocks StoreApi was merged into WooCommerce core). The plugin's stated minimum WC version (10.7) is advisory — WordPress does not enforce it at runtime — so sites that lagged behind on WooCommerce updates were still loading the plugin and fatalling inside extend_store_api().

The fix has two parts:

  1. Register on woocommerce_blocks_loaded. extend_store_api() now runs on the woocommerce_blocks_loaded action (the same hook the block integration already uses at line 704) instead of during woocommerce_init. Our extension only surfaces notices in the block cart/checkout, so it only needs to run once Blocks load. On a WooCommerce too old to ship Blocks the hook never fires, so nothing runs and there is no fatal.

  2. Guard on class_exists(). WooCommerce versions between ~5.0 and ~6.3 ship Blocks (so they fire woocommerce_blocks_loaded) but predate the top-level Automattic\WooCommerce\StoreApi\StoreApi class this plugin imports — the hook alone would still fatal there. So extend_store_api() also guards on class_exists( '\Automattic\WooCommerce\StoreApi\StoreApi' ). If the class is absent, a notice is logged via wc_get_logger() and the method returns early. When the class is present the extension registers as before.

This addresses the review suggestion to fix the fatal structurally via the hook, while keeping the class check so the in-between versions are covered too.

Related issue(s)

Fixes WOOTAX-298

Steps to reproduce & screenshots/GIFs

Before (to reproduce the fatal):

  1. Install WooCommerce at a version that predates the top-level Automattic\WooCommerce\StoreApi\StoreApi class (before ~6.4). This includes both WC too old to ship Blocks at all, and WC ~5.0–6.3 that ships Blocks but under the older Blocks\StoreApi namespace.
  2. Install or update WooCommerce Tax to this version.
  3. Observe PHP fatal: Uncaught Error: Class "Automattic\WooCommerce\StoreApi\StoreApi" not found in StoreApiExtendSchema.php:41.

After:

  1. Same setup.
  2. No fatal in either case:
    • WC with no Blocks: woocommerce_blocks_loaded never fires, so registration never runs.
    • WC with Blocks but old StoreApi namespace: the hook fires, the class_exists() guard is false, and WC logs a notice: StoreApi class not found. Store API extensions will not be registered.
  3. On a supported WooCommerce the extension registers as before. The rest of the plugin loads normally in all cases.

Checklist

  • unit tests
  • changelog.txt entry added
  • readme.txt entry added

@iyut iyut self-assigned this Jul 1, 2026
@iyut iyut marked this pull request as ready for review July 2, 2026 02:27
@dustinparker

Copy link
Copy Markdown
Collaborator

This works, but I think there's a cleaner way to get the same result and wanted your take.

Instead of guarding extend_store_api() with class_exists() on woocommerce_init, we could register the Store API extension on woocommerce_blocks_loaded. Our extension only surfaces tax notices in the block cart/checkout, so it's only useful once the blocks load, and we already do this for register_blocks_integration() (line 704).

The upside is it fixes the fatal structurally instead of catching it. On a WooCommerce too old to ship the Store API, woocommerce_blocks_loaded never fires, so the code never runs. It also drops the per-request class_exists() check.

We need to confirm StoreApiExtendSchema::instance() resolves cleanly at that hook and that the schema registers before the cart endpoint gets hit. Since register_blocks_integration() already runs there, we're probably fine.

What do you think?

@CezaryDrewniak CezaryDrewniak self-requested a review July 2, 2026 09:26

@CezaryDrewniak CezaryDrewniak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Fix is correct, minimal, and follows existing patterns

What works:

  • Right placement — StoreApi::container() at src/StoreApi/StoreApiExtendSchema.php:41 is the only call site, and extend_store_api() is the only path that reaches it.
  • Matches codebase style — same class_exists() pattern, same wc_get_logger()->... + source array used elsewhere in the repo.
  • notice log level is right — tax still works, only the store-notice extension is missing.
  • Changelog/readme format matches prior entries.

Concerns:

  1. No unit test. Easy to add: call extend_store_api() without Automattic\WooCommerce\StoreApi\StoreApi loaded and assert no fatal. Without it, a future refactor could silently drop the guard.
  2. Guard is call-site-level. Moving it into StoreApiExtendSchema::__construct() would protect any future caller too. Not blocking, just more robust.

@dustinparker dustinparker assigned Abdalsalaam and unassigned iyut Jul 3, 2026
Abdalsalaam and others added 2 commits July 3, 2026 14:50
Register the Store API extension on the woocommerce_blocks_loaded action
instead of guarding extend_store_api() with a per-request class_exists()
check. On WooCommerce versions too old to ship the Store API that action
never fires, so the extension is skipped structurally rather than caught,
and the class_exists() check is removed. When Blocks have already loaded
the extension is registered inline as before.

Add unit tests covering both the deferred and already-loaded paths.
…guard

Register extend_store_api() on woocommerce_blocks_loaded, the same hook
the block integration already uses, instead of calling it during
woocommerce_init. The extension only surfaces notices in the block
cart/checkout, and on a WooCommerce too old to ship Blocks the hook never
fires, so nothing runs.

Keep a class_exists() guard on the top-level
Automattic\WooCommerce\StoreApi\StoreApi class inside the callback. That
covers the in-between versions that ship Blocks (so they fire the hook)
but predate that class, which would otherwise still fatal with "Class not
found". When the class is absent the method logs a notice and returns.

Replace the two deferral tests with tests that exercise the guard itself:
registration runs when the class is present, is skipped when it is
absent, and is_store_api_available() tracks the real class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread woocommerce-services.php
@iyut

iyut commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

This works, but I think there's a cleaner way to get the same result and wanted your take.

Instead of guarding extend_store_api() with class_exists() on woocommerce_init, we could register the Store API extension on woocommerce_blocks_loaded. Our extension only surfaces tax notices in the block cart/checkout, so it's only useful once the blocks load, and we already do this for register_blocks_integration() (line 704).

The upside is it fixes the fatal structurally instead of catching it. On a WooCommerce too old to ship the Store API, woocommerce_blocks_loaded never fires, so the code never runs. It also drops the per-request class_exists() check.

We need to confirm StoreApiExtendSchema::instance() resolves cleanly at that hook and that the schema registers before the cart endpoint gets hit. Since register_blocks_integration() already runs there, we're probably fine.

What do you think?

@dustinparker, I've looked into WooCommerce commit history and it seems moving the extend_store_api() to woocommerce_blocks_loaded does not automatically fix the fatal error without guarding it with class_exists().

Because woocommerce_blocks_loaded already exists since 2.5.0 and StoreAPI exists since around version 7. We will still need to guard it. But I agree that calling it on woocommerce_blocks_loaded is more efficient than in woocommerce_init.

And the implementation that @Abdalsalaam has made is looking good to me! Thank you for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants