Commit 01c4f37ebe3 for woocommerce
commit 01c4f37ebe343b7a00bbfa87cca1cd94b6d78f6d
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Mon Jun 1 19:46:40 2026 +0800
fix: guard analytics cart handlers against missing cart item data (#64963)
diff --git a/packages/php/woocommerce-analytics/changelog/fix-cart-item-warnings b/packages/php/woocommerce-analytics/changelog/fix-cart-item-warnings
new file mode 100644
index 00000000000..dd67075060f
--- /dev/null
+++ b/packages/php/woocommerce-analytics/changelog/fix-cart-item-warnings
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+Guard cart hook handlers against missing cart item data to prevent PHP warnings.
diff --git a/packages/php/woocommerce-analytics/src/class-universal.php b/packages/php/woocommerce-analytics/src/class-universal.php
index d4ce3b072b5..f1a28dbff78 100644
--- a/packages/php/woocommerce-analytics/src/class-universal.php
+++ b/packages/php/woocommerce-analytics/src/class-universal.php
@@ -134,6 +134,10 @@ class Universal {
public function capture_remove_from_cart( $cart_item_key, $cart ) {
$item = $cart->removed_cart_contents[ $cart_item_key ] ?? null;
+ if ( ! is_array( $item ) || ! isset( $item['product_id'], $item['quantity'] ) ) {
+ return;
+ }
+
WC_Analytics_Tracking::record_event(
'remove_from_cart',
$this->get_cart_checkout_event_properties(
@@ -156,6 +160,10 @@ class Universal {
* @return void
*/
public function capture_cart_quantity_update( $cart_item_key, $quantity, $old_quantity, $cart ) {
+ if ( ! isset( $cart->cart_contents[ $cart_item_key ]['product_id'] ) ) {
+ return;
+ }
+
$product_id = $cart->cart_contents[ $cart_item_key ]['product_id'];
if ( $quantity > $old_quantity ) {
WC_Analytics_Tracking::record_event(
diff --git a/packages/php/woocommerce-analytics/tests/php/Universal_Test.php b/packages/php/woocommerce-analytics/tests/php/Universal_Test.php
index 915d5dbf31c..85e856940d9 100644
--- a/packages/php/woocommerce-analytics/tests/php/Universal_Test.php
+++ b/packages/php/woocommerce-analytics/tests/php/Universal_Test.php
@@ -90,4 +90,84 @@ class Universal_Test extends BaseTestCase {
// If we get here without errors, the method completed without processing a non-existent order.
$this->assertTrue( true, 'order_process should handle a missing order without throwing an exception.' );
}
+
+ /**
+ * capture_remove_from_cart should return early — without raising a PHP
+ * warning or queueing a pixel — when the cart key is not present in
+ * removed_cart_contents. Reproduces the warnings reported on
+ * line 141/142 of class-universal.php where third-party code fires the
+ * woocommerce_cart_item_removed action with a key that was never copied
+ * into removed_cart_contents.
+ */
+ public function test_capture_remove_from_cart_skips_when_item_missing(): void {
+ $cart = new \stdClass();
+ $cart->removed_cart_contents = array();
+
+ $this->reset_pixel_batch_queue();
+
+ $universal = new Universal();
+ $universal->capture_remove_from_cart( 'unknown_key', $cart );
+
+ $this->assertSame( array(), $this->get_pixel_batch_queue(), 'No pixel should be queued when the removed cart item is missing.' );
+ }
+
+ /**
+ * capture_remove_from_cart should also bail when the entry exists but is
+ * missing the product_id/quantity keys the event payload requires.
+ */
+ public function test_capture_remove_from_cart_skips_when_item_missing_keys(): void {
+ $cart = new \stdClass();
+ $cart->removed_cart_contents = array(
+ 'partial_key' => array( 'product_id' => 5 ), // No quantity key.
+ );
+
+ $this->reset_pixel_batch_queue();
+
+ $universal = new Universal();
+ $universal->capture_remove_from_cart( 'partial_key', $cart );
+
+ $this->assertSame( array(), $this->get_pixel_batch_queue(), 'No pixel should be queued when the removed cart item lacks required keys.' );
+ }
+
+ /**
+ * capture_cart_quantity_update should return early — without raising a
+ * PHP warning or queueing a pixel — when the cart key is not present in
+ * cart_contents. Reproduces the warning reported on line 159 of
+ * class-universal.php.
+ */
+ public function test_capture_cart_quantity_update_skips_when_item_missing(): void {
+ $cart = new \stdClass();
+ $cart->cart_contents = array();
+
+ $this->reset_pixel_batch_queue();
+
+ $universal = new Universal();
+ $universal->capture_cart_quantity_update( 'unknown_key', 2, 1, $cart );
+
+ $this->assertSame( array(), $this->get_pixel_batch_queue(), 'No pixel should be queued when the cart item is missing.' );
+ }
+
+ /**
+ * Reset the WC_Analytics_Tracking::$pixel_batch_queue static so each
+ * test starts from a known-empty state.
+ */
+ private function reset_pixel_batch_queue(): void {
+ $reflection = new \ReflectionClass( WC_Analytics_Tracking::class );
+ $queue = $reflection->getProperty( 'pixel_batch_queue' );
+ $queue->setAccessible( true );
+ $queue->setValue( null, array() );
+ }
+
+ /**
+ * Read WC_Analytics_Tracking::$pixel_batch_queue via reflection to
+ * verify whether a pixel was queued.
+ *
+ * @return array
+ */
+ private function get_pixel_batch_queue(): array {
+ $reflection = new \ReflectionClass( WC_Analytics_Tracking::class );
+ $property = $reflection->getProperty( 'pixel_batch_queue' );
+ $property->setAccessible( true );
+ return $property->getValue();
+ }
}