Commit 8cb8ad18c82 for woocommerce

commit 8cb8ad18c82d09803eb06917781044d69c555917
Author: James Kemp <me@jckemp.com>
Date:   Wed May 6 15:09:57 2026 +0100

    Fix PHP 8.5 fatal error in StoreApi AbstractSchema (#63905)

    * Fix PHP 8.5 fatal error in AbstractSchema::remove_arg_options()

    Add type check to prevent unsetting string offsets in PHP 8.4+.
    The method now returns non-array properties unchanged instead of
    attempting to unset array keys on them.

    Fixes WOOPLUG-6417

    Co-authored-by: James Kemp <jamesckemp@users.noreply.github.com>

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Remove unused ReflectionMethod import from AbstractSchemaTest

    Co-authored-by: James Kemp <jamesckemp@users.noreply.github.com>

    * Remove useless tearDown override in AbstractSchemaTest

    Co-authored-by: James Kemp <jamesckemp@users.noreply.github.com>

    * Fix AbstractSchemaTest: use actual ExtendSchema instance instead of mock

    ExtendSchema is declared final and cannot be mocked. Create actual
    instances of Formatters and ExtendSchema to avoid mock failures.

    Co-authored-by: James Kemp <jamesckemp@users.noreply.github.com>

    * Fix missing trailing newline in changelog entry

    https://claude.ai/code/session_017Mwo2ZfL4zQ3Pdnvf8sJ1G

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Fix check-changelogger-use.php regex to match pnpm-workspace.yaml

    Add `m` flag to the `^packages:` regex so it matches `packages:` at the
    start of any line, not just the start of the file. Since the workspace
    YAML now begins with `useNodeVersion:` and `catalogs:`, the old regex
    never matched, causing all projects to be silently ignored and the
    changelog validation to fail.

    https://claude.ai/code/session_017Mwo2ZfL4zQ3Pdnvf8sJ1G

    ---------

    Co-authored-by: Cursor Agent <cursoragent@cursor.com>
    Co-authored-by: James Kemp <jamesckemp@users.noreply.github.com>
    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Claude <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/63905-cursor-WOOPLUG-6417-php-8-5-unset-string-4251 b/plugins/woocommerce/changelog/63905-cursor-WOOPLUG-6417-php-8-5-unset-string-4251
new file mode 100644
index 00000000000..2d28a96ce6f
--- /dev/null
+++ b/plugins/woocommerce/changelog/63905-cursor-WOOPLUG-6417-php-8-5-unset-string-4251
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix PHP 8.5 fatal error when unsetting string offsets in StoreApi AbstractSchema
\ No newline at end of file
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractSchema.php
index 482de6ebfa1..83894ac28c9 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/AbstractSchema.php
@@ -90,6 +90,9 @@ abstract class AbstractSchema {
 	protected function remove_arg_options( $properties ) {
 		return array_map(
 			function( $property ) {
+				if ( ! is_array( $property ) ) {
+					return $property;
+				}
 				if ( isset( $property['properties'] ) ) {
 					$property['properties'] = $this->remove_arg_options( $property['properties'] );
 				} elseif ( isset( $property['items']['properties'] ) ) {
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/Schemas/V1/AbstractSchemaTest.php b/plugins/woocommerce/tests/php/src/StoreApi/Schemas/V1/AbstractSchemaTest.php
new file mode 100644
index 00000000000..354418b3a9f
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/StoreApi/Schemas/V1/AbstractSchemaTest.php
@@ -0,0 +1,167 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\StoreApi\Schemas\V1;
+
+use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
+use Automattic\WooCommerce\StoreApi\SchemaController;
+use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
+use Automattic\WooCommerce\StoreApi\Formatters;
+use WC_Unit_Test_Case;
+use ReflectionClass;
+
+/**
+ * Tests for the AbstractSchema class.
+ *
+ * @since 10.8.0
+ */
+class AbstractSchemaTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var AbstractSchema
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$formatters = new Formatters();
+		$extend     = new ExtendSchema( $formatters );
+		$controller = $this->createMock( SchemaController::class );
+
+		$this->sut = $this->getMockForAbstractClass(
+			AbstractSchema::class,
+			array( $extend, $controller )
+		);
+	}
+
+	/**
+	 * @testdox Should remove arg_options from properties array.
+	 */
+	public function test_removes_arg_options_from_properties(): void {
+		$properties = array(
+			'name' => array(
+				'type'        => 'string',
+				'arg_options' => array( 'default' => 'test' ),
+			),
+		);
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertArrayNotHasKey( 'arg_options', $result['name'], 'arg_options should be removed' );
+		$this->assertSame( 'string', $result['name']['type'], 'type property should remain' );
+	}
+
+	/**
+	 * @testdox Should handle nested properties recursively.
+	 */
+	public function test_handles_nested_properties_recursively(): void {
+		$properties = array(
+			'address' => array(
+				'type'       => 'object',
+				'properties' => array(
+					'city' => array(
+						'type'        => 'string',
+						'arg_options' => array( 'default' => 'Los Angeles' ),
+					),
+				),
+			),
+		);
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertArrayNotHasKey( 'arg_options', $result['address']['properties']['city'], 'nested arg_options should be removed' );
+		$this->assertSame( 'string', $result['address']['properties']['city']['type'], 'nested type property should remain' );
+	}
+
+	/**
+	 * @testdox Should handle items with properties recursively.
+	 */
+	public function test_handles_items_properties_recursively(): void {
+		$properties = array(
+			'tags' => array(
+				'type'  => 'array',
+				'items' => array(
+					'type'       => 'object',
+					'properties' => array(
+						'name' => array(
+							'type'        => 'string',
+							'arg_options' => array( 'default' => 'tag' ),
+						),
+					),
+				),
+			),
+		);
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertArrayNotHasKey( 'arg_options', $result['tags']['items']['properties']['name'], 'items properties arg_options should be removed' );
+		$this->assertSame( 'string', $result['tags']['items']['properties']['name']['type'], 'items properties type should remain' );
+	}
+
+	/**
+	 * @testdox Should handle non-array property values without errors in PHP 8.4+.
+	 */
+	public function test_handles_non_array_property_values(): void {
+		$properties = array(
+			'title'       => 'Product Schema',
+			'description' => array(
+				'type'        => 'string',
+				'arg_options' => array( 'default' => 'test' ),
+			),
+		);
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertSame( 'Product Schema', $result['title'], 'string property should remain unchanged' );
+		$this->assertArrayNotHasKey( 'arg_options', $result['description'], 'arg_options should be removed from array property' );
+	}
+
+	/**
+	 * @testdox Should handle empty properties array.
+	 */
+	public function test_handles_empty_properties_array(): void {
+		$properties = array();
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertIsArray( $result, 'result should be an array' );
+		$this->assertEmpty( $result, 'result should be empty' );
+	}
+
+	/**
+	 * @testdox Should handle properties without arg_options.
+	 */
+	public function test_handles_properties_without_arg_options(): void {
+		$properties = array(
+			'id' => array(
+				'type'     => 'integer',
+				'readonly' => true,
+			),
+		);
+
+		$result = $this->invoke_remove_arg_options( $properties );
+
+		$this->assertSame( 'integer', $result['id']['type'], 'type property should remain' );
+		$this->assertTrue( $result['id']['readonly'], 'readonly property should remain' );
+	}
+
+	/**
+	 * Invoke the protected remove_arg_options method.
+	 *
+	 * @param array $properties Properties array.
+	 * @return array Result from remove_arg_options.
+	 */
+	private function invoke_remove_arg_options( array $properties ): array {
+		$reflection = new ReflectionClass( $this->sut );
+		$method     = $reflection->getMethod( 'remove_arg_options' );
+		$method->setAccessible( true );
+
+		return $method->invoke( $this->sut, $properties );
+	}
+}
diff --git a/tools/monorepo/check-changelogger-use.php b/tools/monorepo/check-changelogger-use.php
index 87fb69fa0b9..91c2f8c04b7 100644
--- a/tools/monorepo/check-changelogger-use.php
+++ b/tools/monorepo/check-changelogger-use.php
@@ -82,7 +82,7 @@ $base_path = dirname( dirname( __DIR__ ) );

 $workspace_paths = array();
 $workspace_yaml = file_get_contents( $base_path . '/pnpm-workspace.yaml' );
-if ( preg_match( '/^packages:((\n\s+.+)+)/', $workspace_yaml, $matches ) ) {
+if ( preg_match( '/^packages:((\n\s+.+)+)/m', $workspace_yaml, $matches ) ) {
         $packages_config = $matches[1];
         if ( preg_match_all( "/^\s+-\s?'([^']+)'/m", $packages_config, $matches ) ) {
                 $workspace_paths = $matches[1];