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();
+	}
 }