Commit 6613a54af0 for woocommerce
commit 6613a54af032ba8f872573b4330dc86078f02679
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Mon Nov 24 14:38:40 2025 +0000
Prevent fatal errors when getting orders containing deleted products (#62074)
* Set up fallback values for a product on an order in case it is null
* Add tests for deleted product on order schema
* Add changelog
* Update namespace
diff --git a/plugins/woocommerce/changelog/wooplug-5887-uncaught-error-call-to-a-member-function b/plugins/woocommerce/changelog/wooplug-5887-uncaught-error-call-to-a-member-function
new file mode 100644
index 0000000000..d3e123874a
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-5887-uncaught-error-call-to-a-member-function
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent fatal errors when retrieving orders with deleted products on Store API
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
index f813c7953d..375c7f7610 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
@@ -33,6 +33,48 @@ class OrderItemSchema extends ItemSchema {
$order = $order_item->get_order();
$product = $order_item->get_product();
+ $product_properties = [
+ 'short_description' => '',
+ 'description' => '',
+ 'sku' => '',
+ 'permalink' => '',
+ 'catalog_visibility' => 'hidden',
+ 'prices' => [
+ 'price' => '',
+ 'regular_price' => '',
+ 'sale_price' => '',
+ 'price_range' => null,
+ 'currency_code' => '',
+ 'currency_symbol' => '',
+ 'currency_minor_unit' => 2,
+ 'currency_decimal_separator' => '.',
+ 'currency_thousand_separator' => ',',
+ 'currency_prefix' => '',
+ 'currency_suffix' => '',
+ 'raw_prices' => [
+ 'precision' => 6,
+ 'price' => '',
+ 'regular_price' => '',
+ 'sale_price' => '',
+ ],
+ ],
+ 'sold_individually' => false,
+ 'images' => [],
+ 'variation' => [],
+ ];
+
+ if ( is_a( $product, 'WC_Product' ) ) {
+ $product_properties['short_description'] = $product->get_short_description();
+ $product_properties['description'] = $product->get_description();
+ $product_properties['sku'] = $product->get_sku();
+ $product_properties['permalink'] = $product->get_permalink();
+ $product_properties['catalog_visibility'] = $product->get_catalog_visibility();
+ $product_properties['prices'] = $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) );
+ $product_properties['sold_individually'] = $product->is_sold_individually();
+ $product_properties['images'] = $this->get_images( $product );
+ $product_properties['variation'] = $this->format_variation_data( $product->get_attributes(), $product );
+ }
+
return [
'key' => $order->get_order_key(),
'id' => $order_item->get_id(),
@@ -44,20 +86,20 @@ class OrderItemSchema extends ItemSchema {
'editable' => false,
),
'name' => $order_item->get_name(),
- 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
- 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
- 'sku' => $this->prepare_html_response( $product->get_sku() ),
+ 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product_properties['short_description'] ) ) ),
+ 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product_properties['description'] ) ) ),
+ 'sku' => $this->prepare_html_response( $product_properties['sku'] ),
'low_stock_remaining' => null,
'backorders_allowed' => false,
'show_backorder_badge' => false,
- 'sold_individually' => $product->is_sold_individually(),
- 'permalink' => $product->get_permalink(),
- 'images' => $this->get_images( $product ),
- 'variation' => $this->format_variation_data( $product->get_attributes(), $product ),
+ 'sold_individually' => $product_properties['sold_individually'] ?? false,
+ 'permalink' => $product_properties['permalink'],
+ 'images' => $product_properties['images'],
+ 'variation' => $product_properties['variation'],
'item_data' => $order_item->get_all_formatted_meta_data(),
- 'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
+ 'prices' => (object) $product_properties['prices'],
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order_item ) ),
- 'catalog_visibility' => $product->get_catalog_visibility(),
+ 'catalog_visibility' => $product_properties['catalog_visibility'],
];
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
new file mode 100644
index 0000000000..ec04cbff33
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Schemas/OrderItemSchemaTest.php
@@ -0,0 +1,162 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Schemas;
+
+use Automattic\WooCommerce\StoreApi\Schemas\V1\OrderItemSchema;
+use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
+use Automattic\WooCommerce\StoreApi\SchemaController;
+use Automattic\WooCommerce\StoreApi\Formatters;
+use Automattic\WooCommerce\StoreApi\Formatters\MoneyFormatter;
+use Automattic\WooCommerce\StoreApi\Formatters\HtmlFormatter;
+use Automattic\WooCommerce\StoreApi\Formatters\CurrencyFormatter;
+use WC_Helper_Order;
+use Yoast\PHPUnitPolyfills\TestCases\TestCase;
+
+/**
+ * OrderItemSchemaTest class.
+ */
+class OrderItemSchemaTest extends TestCase {
+ /**
+ * The system under test.
+ *
+ * @var OrderItemSchema
+ */
+ private $sut;
+
+ /**
+ * ExtendSchema instance.
+ *
+ * @var ExtendSchema
+ */
+ private $mock_extend;
+
+ /**
+ * SchemaController instance.
+ *
+ * @var SchemaController
+ */
+ private $schema_controller;
+
+ /**
+ * Set up before test.
+ *
+ * @return void
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ // Set up formatters and extend schema.
+ $formatters = new Formatters();
+ $formatters->register( 'money', MoneyFormatter::class );
+ $formatters->register( 'html', HtmlFormatter::class );
+ $formatters->register( 'currency', CurrencyFormatter::class );
+ $this->mock_extend = new ExtendSchema( $formatters );
+ $this->schema_controller = new SchemaController( $this->mock_extend );
+ $this->sut = $this->schema_controller->get( OrderItemSchema::IDENTIFIER );
+ }
+
+ /**
+ * Tear down after test.
+ */
+ public function tearDown(): void {
+ parent::tearDown();
+ $this->sut = null;
+ $this->schema_controller = null;
+ $this->mock_extend = null;
+ }
+
+ /**
+ * Test that get_item_response handles deleted products gracefully.
+ *
+ * Reproduces the issue: when a product is deleted and an order
+ * containing that product is fetched via StoreAPI, it should not
+ * throw an error.
+ */
+ public function test_get_item_response_with_deleted_product(): void {
+ // Arrange - Create order with a product, then delete the product.
+ $order = WC_Helper_Order::create_order();
+ $items = $order->get_items();
+ $item = reset( $items );
+ $product = $item->get_product();
+
+ // Verify product exists initially.
+ $this->assertInstanceOf( 'WC_Product', $product );
+
+ // Delete the product.
+ $product_id = $product->get_id();
+ wp_delete_post( $product_id, true );
+
+ // Verify product is now deleted (get_product returns false).
+ $item_after_delete = new \WC_Order_Item_Product( $item->get_id() );
+ $this->assertFalse( $item_after_delete->get_product() );
+
+ // Act - Get item response for order item with deleted product.
+ $result = $this->sut->get_item_response( $item_after_delete );
+
+ // Verify key fields exist.
+ $this->assertArrayHasKey( 'id', $result );
+ $this->assertArrayHasKey( 'name', $result );
+ $this->assertArrayHasKey( 'short_description', $result );
+ $this->assertArrayHasKey( 'description', $result );
+ $this->assertArrayHasKey( 'sku', $result );
+ $this->assertArrayHasKey( 'permalink', $result );
+ $this->assertArrayHasKey( 'catalog_visibility', $result );
+ $this->assertArrayHasKey( 'prices', $result );
+ $this->assertArrayHasKey( 'images', $result );
+ $this->assertArrayHasKey( 'variation', $result );
+ $this->assertArrayHasKey( 'sold_individually', $result );
+
+ // Verify product-specific fields have safe defaults.
+ $this->assertEquals( '', $result['sku'] );
+ $this->assertEquals( '', $result['permalink'] );
+ $this->assertEquals( 'hidden', $result['catalog_visibility'] );
+ $this->assertFalse( $result['sold_individually'] );
+ $this->assertIsArray( $result['images'] );
+ $this->assertEmpty( $result['images'] );
+ $this->assertIsArray( $result['variation'] );
+ $this->assertEmpty( $result['variation'] );
+
+ // Verify order-level data is preserved.
+ $this->assertEquals( $item_after_delete->get_id(), $result['id'] );
+ $this->assertEquals( $item_after_delete->get_name(), $result['name'] );
+ $this->assertEquals( $item_after_delete->get_quantity(), $result['quantity'] );
+ }
+
+ /**
+ * Test that get_item_response works correctly with existing product.
+ *
+ * Ensures the fix doesn't break normal behavior.
+ */
+ public function test_get_item_response_with_existing_product(): void {
+ // Arrange - Create order with a product.
+ $order = WC_Helper_Order::create_order();
+ $items = $order->get_items();
+ $item = reset( $items );
+ $product = $item->get_product();
+
+ // Verify product exists.
+ $this->assertInstanceOf( 'WC_Product', $product );
+
+ // Act - Get item response.
+ $result = $this->sut->get_item_response( $item );
+
+ // Verify key fields exist.
+ $this->assertArrayHasKey( 'id', $result );
+ $this->assertArrayHasKey( 'name', $result );
+ $this->assertArrayHasKey( 'sku', $result );
+ $this->assertArrayHasKey( 'permalink', $result );
+ $this->assertArrayHasKey( 'prices', $result );
+
+ // Verify product data is populated.
+ $this->assertEquals( $product->get_sku(), $result['sku'] );
+ $this->assertEquals( $product->get_permalink(), $result['permalink'] );
+ $this->assertEquals( $product->get_catalog_visibility(), $result['catalog_visibility'] );
+ $this->assertEquals( $product->is_sold_individually(), $result['sold_individually'] );
+
+ // Verify order data.
+ $this->assertEquals( $item->get_id(), $result['id'] );
+ $this->assertEquals( $item->get_name(), $result['name'] );
+ $this->assertEquals( $item->get_quantity(), $result['quantity'] );
+ }
+}