Skip to content

Commit 085c9b4

Browse files
committed
Merge branch 'trunk' into fix/issue-wooship-2256
2 parents ff5436c + 3647e4c commit 085c9b4

4 files changed

Lines changed: 167 additions & 10 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
= 3.6.8 - 2026-xx-xx =
44
* Fix - TaxJar tax lines wiped when REST API order update includes address change.
5+
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
56

67
= 3.6.7 - 2026-07-06 =
78
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ This plugin relies on the following external services:
7272

7373
= 3.6.8 - 2026-xx-xx =
7474
* Fix - TaxJar tax lines wiped when REST API order update includes address change.
75+
* Fix - Prevent fatal error on sites running WooCommerce versions without StoreApi support.
7576

7677
= 3.6.7 - 2026-07-06 =
7778
* Fix - Prevent fatal error on Atomic sites caused by incorrect path resolution when loading the API client class.

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

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ public function mockLoaderAndActiveShippingMethods() {
8686
public function test_class_exists() {
8787

8888
$this->assertTrue( class_exists( 'WC_Connect_Loader' ) );
89-
9089
}
9190

9291
/**
@@ -104,7 +103,6 @@ public function test_init_hook_attached_in_constructor() {
104103

105104
$attached = has_action( 'before_woocommerce_init', array( $loader, 'pre_wc_init' ) );
106105
$this->assertNotFalse( $attached, 'WC_Connect_Loader::pre_wc_init() not attached to `before_woocommerce_init`.' );
107-
108106
}
109107

110108
/**
@@ -121,7 +119,6 @@ public function test_logger_getter_setter() {
121119
$loader->set_logger( $logger );
122120

123121
$this->assertEquals( $logger, $loader->get_logger() );
124-
125122
}
126123

127124
/**
@@ -138,7 +135,6 @@ public function test_api_client_getter_setter() {
138135
$loader->set_api_client( $client );
139136

140137
$this->assertEquals( $client, $loader->get_api_client() );
141-
142138
}
143139

144140
/**
@@ -156,7 +152,6 @@ public function test_services_store_getter_setter() {
156152
$loader->set_service_schemas_store( $store );
157153

158154
$this->assertEquals( $store, $loader->get_service_schemas_store() );
159-
160155
}
161156

162157
/**
@@ -173,7 +168,6 @@ public function test_services_validator_getter_setter() {
173168
$loader->set_service_schemas_validator( $validator );
174169

175170
$this->assertEquals( $validator, $loader->get_service_schemas_validator() );
176-
177171
}
178172

179173
/**
@@ -218,7 +212,6 @@ public function test_init_service() {
218212
$this->assertEquals( $loader->get_logger(), $method->get_logger() );
219213
$this->assertEquals( $loader->get_api_client(), $method->get_api_client() );
220214
$this->assertEquals( $service_data, $method->get_service_schema() );
221-
222215
}
223216

224217
/**
@@ -229,7 +222,6 @@ public function test_is_wc_connect_shipping_service() {
229222

230223
$this->assertTrue( $loader->is_wc_connect_shipping_service( 'test_method_that_is_from_wc_connect' ) );
231224
$this->assertFalse( $loader->is_wc_connect_shipping_service( 'test_method_that_is_not_from_wc_connect' ) );
232-
233225
}
234226

235227
/**
@@ -247,4 +239,113 @@ public function test_shipping_zone_method_added() {
247239
$this->assertEquals( 2, did_action( 'wc_connect_shipping_zone_method_added' ) );
248240
}
249241

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

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)