Skip to content

Commit 3647e4c

Browse files
iyutAbdalsalaamclaudedustinparker
authored
Fix/issue WOOTAX-298 - Prevent StoreApi fatal on sites running old WooCommerce (#2971)
* Fix StoreApi fatal on old WooCommerce by adding class_exists guard * add changelog * Defer Store API extension to woocommerce_blocks_loaded 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. * Register Store API extension on woocommerce_blocks_loaded with class 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> * fix: throttle StoreApi-unavailable notice to once per day extend_store_api() runs on woocommerce_blocks_loaded, which fires on nearly every request. On installs missing the StoreApi class the unavailable notice was written on every page load. Move the logging into log_store_api_unavailable() and gate it behind a daily transient so an affected site logs at most once per day. Also strengthen the tests: value-pin is_store_api_available() to true in the harness so a class-string typo fails, and add a test asserting the notice fires once and is throttled on the second same-day call. --------- Co-authored-by: Abdalsalaam Halawa <abdalsalaamnafez@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Dustin Parker <11618203+dustinparker@users.noreply.github.com>
1 parent 60ab97e commit 3647e4c

4 files changed

Lines changed: 172 additions & 2 deletions

File tree

changelog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
*** WooCommerce Tax Changelog ***
22

3+
= 3.6.8 - 2026-xx-xx =
4+
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
5+
36
= 3.6.7 - 2026-07-06 =
47
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.
58

readme.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ This plugin relies on the following external services:
7070

7171
== Changelog ==
7272

73+
= 3.6.8 - 2026-xx-xx =
74+
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
75+
7376
= 3.6.7 - 2026-07-06 =
7477
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.
7578

tests/php/test-woocommerce-connect-client.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,114 @@ public function test_shipping_zone_method_added() {
247247
$this->assertEquals( 2, did_action( 'wc_connect_shipping_zone_method_added' ) );
248248
}
249249

250+
/**
251+
* When the StoreApi class is present, extend_store_api() registers the
252+
* plugin's Store API extensions.
253+
*
254+
* @covers WC_Connect_Loader::extend_store_api
255+
*/
256+
public function test_extend_store_api_registers_when_store_api_available() {
257+
$sut = $this->getMockBuilder( 'WC_Connect_Loader' )
258+
->disableOriginalConstructor()
259+
->setMethods( array( 'is_store_api_available', 'register_store_api_extensions' ) )
260+
->getMock();
261+
262+
$sut->method( 'is_store_api_available' )->willReturn( true );
263+
$sut->expects( $this->once() )->method( 'register_store_api_extensions' );
264+
265+
$sut->extend_store_api();
266+
}
267+
268+
/**
269+
* On WooCommerce versions without the StoreApi class, extend_store_api()
270+
* skips registration instead of fataling on the missing class. This is the
271+
* guard that prevents the `Class "…\StoreApi" not found` fatal (WOOTAX-298).
272+
*
273+
* @covers WC_Connect_Loader::extend_store_api
274+
*/
275+
public function test_extend_store_api_skips_registration_when_store_api_unavailable() {
276+
$sut = $this->getMockBuilder( 'WC_Connect_Loader' )
277+
->disableOriginalConstructor()
278+
->setMethods( array( 'is_store_api_available', 'register_store_api_extensions' ) )
279+
->getMock();
280+
281+
$sut->method( 'is_store_api_available' )->willReturn( false );
282+
$sut->expects( $this->never() )->method( 'register_store_api_extensions' );
283+
284+
// Must not throw when the StoreApi class is absent.
285+
$this->assertNull( $sut->extend_store_api() );
286+
}
287+
288+
/**
289+
* is_store_api_available() returns true when the StoreApi class the plugin
290+
* depends on is present. The PHPUnit harness loads WooCommerce, so the class
291+
* exists here - pinning to true means a typo or rename in the guarded class
292+
* string would fail this test rather than silently tracking the change.
293+
*
294+
* @covers WC_Connect_Loader::is_store_api_available
295+
*/
296+
public function test_is_store_api_available_returns_true_when_store_api_class_present() {
297+
// Precondition: the harness must actually load the StoreApi class for the
298+
// assertion below to be meaningful.
299+
$this->assertTrue(
300+
class_exists( '\Automattic\WooCommerce\StoreApi\StoreApi' ),
301+
'Test precondition: the StoreApi class must be loaded in the test harness.'
302+
);
303+
304+
$sut = $this->getMockBuilder( 'WC_Connect_Loader' )
305+
->disableOriginalConstructor()
306+
->setMethods( null )
307+
->getMock();
308+
309+
$method = new ReflectionMethod( 'WC_Connect_Loader', 'is_store_api_available' );
310+
$method->setAccessible( true );
311+
312+
$this->assertTrue(
313+
$method->invoke( $sut ),
314+
'is_store_api_available() should return true when the StoreApi class is present.'
315+
);
316+
}
317+
318+
/**
319+
* On the unavailable path, extend_store_api() logs a notice - but at most
320+
* once per day, because it runs on `woocommerce_blocks_loaded` (nearly every
321+
* request). The throttle transient must suppress the second same-day call.
322+
*
323+
* @covers WC_Connect_Loader::extend_store_api
324+
* @covers WC_Connect_Loader::log_store_api_unavailable
325+
*/
326+
public function test_extend_store_api_logs_unavailable_notice_once_per_day() {
327+
delete_transient( 'wcservices_store_api_unavailable_logged' );
328+
329+
// Spy logger injected via the woocommerce_logging_class filter. Returning
330+
// an object bypasses wc_get_logger()'s static cache.
331+
$logger = $this->getMockBuilder( 'WC_Logger_Interface' )->getMock();
332+
$logger->expects( $this->once() )
333+
->method( 'notice' )
334+
->with(
335+
'StoreApi class not found. Store API extensions will not be registered.',
336+
array( 'source' => 'woocommerce-services' )
337+
);
338+
339+
$inject_logger = function () use ( $logger ) {
340+
return $logger;
341+
};
342+
add_filter( 'woocommerce_logging_class', $inject_logger );
343+
344+
$sut = $this->getMockBuilder( 'WC_Connect_Loader' )
345+
->disableOriginalConstructor()
346+
->setMethods( array( 'is_store_api_available', 'register_store_api_extensions' ) )
347+
->getMock();
348+
349+
$sut->method( 'is_store_api_available' )->willReturn( false );
350+
$sut->expects( $this->never() )->method( 'register_store_api_extensions' );
351+
352+
// First skip logs the notice; the second same-day skip is throttled.
353+
$sut->extend_store_api();
354+
$sut->extend_store_api();
355+
356+
remove_filter( 'woocommerce_logging_class', $inject_logger );
357+
delete_transient( 'wcservices_store_api_unavailable_logged' );
358+
}
359+
250360
}

woocommerce-services.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,7 @@ function () {
702702
}
703703

704704
add_action( 'woocommerce_blocks_loaded', array( $this, 'register_blocks_integration' ) );
705+
add_action( 'woocommerce_blocks_loaded', array( $this, 'extend_store_api' ) );
705706
add_action( 'before_woocommerce_init', array( $this, 'pre_wc_init' ) );
706707
}
707708

@@ -817,7 +818,6 @@ public function after_wc_init() {
817818
$this->service_settings_store->migrate_legacy_services();
818819
$this->attach_hooks();
819820
$this->init_store_notices();
820-
$this->extend_store_api();
821821
}
822822

823823
/**
@@ -976,9 +976,63 @@ public function init_store_notices() {
976976
}
977977

978978
/**
979-
* Extend the Store API.
979+
* Extend the Store API when it is available.
980+
*
981+
* Registered on `woocommerce_blocks_loaded` - the same hook the block
982+
* integration uses - because the Store API only exists once Blocks load and
983+
* our extension only surfaces notices in the block cart/checkout. On a
984+
* WooCommerce too old to ship Blocks the hook never fires, so nothing runs.
985+
*
986+
* The `class_exists()` guard covers the in-between case: WooCommerce versions
987+
* that ship Blocks (firing this hook) but predate the top-level
988+
* `Automattic\WooCommerce\StoreApi\StoreApi` class this plugin depends on.
989+
* Without the guard those versions would still fatal with `Class not found`.
980990
*/
981991
public function extend_store_api() {
992+
if ( ! $this->is_store_api_available() ) {
993+
$this->log_store_api_unavailable();
994+
return;
995+
}
996+
997+
$this->register_store_api_extensions();
998+
}
999+
1000+
/**
1001+
* Whether the WooCommerce Store API is available on this installation.
1002+
*
1003+
* @return bool
1004+
*/
1005+
protected function is_store_api_available() {
1006+
return class_exists( '\Automattic\WooCommerce\StoreApi\StoreApi' );
1007+
}
1008+
1009+
/**
1010+
* Log, at most once per day, that the Store API is unavailable.
1011+
*
1012+
* `extend_store_api()` runs on `woocommerce_blocks_loaded`, which fires on
1013+
* nearly every request. On installs missing the StoreApi class the notice
1014+
* would otherwise be written on every page load, so it is throttled to once
1015+
* per day via a transient.
1016+
*/
1017+
protected function log_store_api_unavailable() {
1018+
$transient_key = 'wcservices_store_api_unavailable_logged';
1019+
1020+
if ( get_transient( $transient_key ) ) {
1021+
return;
1022+
}
1023+
1024+
wc_get_logger()->notice(
1025+
'StoreApi class not found. Store API extensions will not be registered.',
1026+
array( 'source' => 'woocommerce-services' )
1027+
);
1028+
1029+
set_transient( $transient_key, 1, DAY_IN_SECONDS );
1030+
}
1031+
1032+
/**
1033+
* Register the plugin's Store API extensions.
1034+
*/
1035+
protected function register_store_api_extensions() {
9821036
$store_api_extend_schema = StoreApiExtendSchema::instance();
9831037
$store_api_extension_controller = new StoreApiExtensionController( $store_api_extend_schema );
9841038

0 commit comments

Comments
 (0)