From 19ebc1ce3ad5197b1c478101a453b3383ea24178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:02:33 +0200 Subject: [PATCH 1/2] View config: Align the `wp_block` author column to the start. Adds the missing `align` style to the author column of the `wp_block` table layout, which was left out when the "Site patterns" rename was synced in [64499]. Synced from https://github.com/WordPress/gutenberg/pull/84680. See #65981. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/view-config.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/view-config.php b/src/wp-includes/view-config.php index e87558331bc49..bf10553500907 100644 --- a/src/wp-includes/view-config.php +++ b/src/wp-includes/view-config.php @@ -342,6 +342,7 @@ function _wp_get_entity_view_config_posttype_wp_block( $data ) { 'styles' => array( 'author' => array( 'width' => '1%', + 'align' => 'start', ), ), ), From f6409b9f25ba6a6b63f0171736e8ee4104b9f867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:03:12 +0200 Subject: [PATCH 2/2] REST API: Extract the View Config empty object casting into a helper. Moves the logic of `WP_REST_View_Config_Controller::cast_empty_objects()` into a new private `_rest_cast_empty_objects_from_schema()` function, so it can be shared with the upcoming Entity Fields controller (#66229). The protected method is kept as a wrapper for backward compatibility. Synced from https://github.com/WordPress/gutenberg/pull/84078. See #65981. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/rest-api.php | 74 ++++++++ .../class-wp-rest-view-config-controller.php | 74 +------- .../restCastEmptyObjectsFromSchema.php | 163 ++++++++++++++++++ 3 files changed, 245 insertions(+), 66 deletions(-) create mode 100644 tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 36b5b23de9f00..d8517d2ddff71 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -3227,6 +3227,80 @@ function rest_default_additional_properties_to_false( $schema ) { return $schema; } +/** + * Recursively casts empty arrays to objects where the schema types them as objects. + * + * PHP cannot distinguish an empty associative array from an empty list, so + * `json_encode()` always serializes `array()` as a JSON array (`[]`). A REST + * schema, however, may type a value as an object, which must encode as `{}`. + * This walks the value against its schema and casts any empty, object-typed + * array to an object. Non-empty associative arrays already encode as objects, + * so they are left as arrays and only recursed into to fix any nested empty + * objects. + * + * Union schemas (`oneOf`/`anyOf`) are handled only for the empty-array case: + * an empty value is cast to an object when any branch allows an object. Such + * values are not recursed into. + * + * @since 7.2.0 + * @access private + * + * @param mixed $value The value to normalize. + * @param array $schema The schema node describing the value. + * @return mixed The normalized value, with empty object-typed arrays cast to objects. + */ +function _rest_cast_empty_objects_from_schema( $value, $schema ) { + if ( ! is_array( $value ) || ! is_array( $schema ) ) { + return $value; + } + + if ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) ) { + $branches = $schema['oneOf'] ?? $schema['anyOf']; + if ( array() === $value ) { + foreach ( $branches as $branch ) { + if ( is_array( $branch ) && in_array( 'object', (array) ( $branch['type'] ?? array() ), true ) ) { + return (object) array(); + } + } + } + return $value; + } + + $types = (array) ( $schema['type'] ?? array() ); + + if ( in_array( 'array', $types, true ) && isset( $schema['items'] ) ) { + foreach ( $value as $index => $item ) { + $value[ $index ] = _rest_cast_empty_objects_from_schema( $item, $schema['items'] ); + } + return $value; + } + + if ( in_array( 'object', $types, true ) ) { + if ( isset( $schema['properties'] ) ) { + foreach ( $schema['properties'] as $property => $property_schema ) { + if ( array_key_exists( $property, $value ) ) { + $value[ $property ] = _rest_cast_empty_objects_from_schema( $value[ $property ], $property_schema ); + } + } + } + if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { + foreach ( $value as $key => $item ) { + if ( isset( $schema['properties'][ $key ] ) ) { + continue; + } + $value[ $key ] = _rest_cast_empty_objects_from_schema( $item, $schema['additionalProperties'] ); + } + } + + // Empty object-typed arrays must serialize as {} to match the schema. + if ( array() === $value ) { + return (object) array(); + } + } + + return $value; +} + /** * Gets the REST API route for a post. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php index da379cc6c84e2..44de983ab4300 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php @@ -155,10 +155,10 @@ public function get_items( $request ) { 'kind' => $kind, 'name' => $name, 'version' => WP_View_Config_Data::LATEST_VERSION, - 'default_view' => $this->cast_empty_objects( $config['default_view'], $schema['properties']['default_view'] ), - 'default_layouts' => $this->cast_empty_objects( $config['default_layouts'], $schema['properties']['default_layouts'] ), - 'view_list' => $this->cast_empty_objects( $config['view_list'], $schema['properties']['view_list'] ), - 'form' => $this->cast_empty_objects( $config['form'], $schema['properties']['form'] ), + 'default_view' => _rest_cast_empty_objects_from_schema( $config['default_view'], $schema['properties']['default_view'] ), + 'default_layouts' => _rest_cast_empty_objects_from_schema( $config['default_layouts'], $schema['properties']['default_layouts'] ), + 'view_list' => _rest_cast_empty_objects_from_schema( $config['view_list'], $schema['properties']['view_list'] ), + 'form' => _rest_cast_empty_objects_from_schema( $config['form'], $schema['properties']['form'] ), ); return rest_ensure_response( $response ); @@ -168,75 +168,17 @@ public function get_items( $request ) { * Recursively casts empty arrays to objects where the schema types them as * objects. * - * PHP cannot distinguish an empty associative array from an empty list, so - * `json_encode()` always serializes `array()` as a JSON array (`[]`). The - * REST schema, however, types several values as objects, which must encode - * as `{}`. This walks the value against its schema and casts any empty, - * object-typed array to an object. Non-empty associative arrays already - * encode as objects, so they are left as arrays and only recursed into to - * fix any nested empty objects. - * - * Union schemas (`oneOf`/`anyOf`) are handled only for the empty-array case: - * an empty value is cast to an object when any branch allows an object. Such - * values are not recursed into, which is sufficient for the form schema - * where they never contain empty nested objects. - * * @since 7.1.0 + * @since 7.2.0 Moved the logic to _rest_cast_empty_objects_from_schema(). + * + * @see _rest_cast_empty_objects_from_schema() * * @param mixed $value The value to normalize. * @param array $schema The schema node describing the value. * @return mixed The normalized value, with empty object-typed arrays cast to objects. */ protected function cast_empty_objects( $value, $schema ) { - if ( ! is_array( $value ) || ! is_array( $schema ) ) { - return $value; - } - - if ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) ) { - $branches = $schema['oneOf'] ?? $schema['anyOf']; - if ( array() === $value ) { - foreach ( $branches as $branch ) { - if ( is_array( $branch ) && in_array( 'object', (array) ( $branch['type'] ?? array() ), true ) ) { - return (object) array(); - } - } - } - return $value; - } - - $types = (array) ( $schema['type'] ?? array() ); - - if ( in_array( 'array', $types, true ) && isset( $schema['items'] ) ) { - foreach ( $value as $index => $item ) { - $value[ $index ] = $this->cast_empty_objects( $item, $schema['items'] ); - } - return $value; - } - - if ( in_array( 'object', $types, true ) ) { - if ( isset( $schema['properties'] ) ) { - foreach ( $schema['properties'] as $property => $property_schema ) { - if ( array_key_exists( $property, $value ) ) { - $value[ $property ] = $this->cast_empty_objects( $value[ $property ], $property_schema ); - } - } - } - if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { - foreach ( $value as $key => $item ) { - if ( isset( $schema['properties'][ $key ] ) ) { - continue; - } - $value[ $key ] = $this->cast_empty_objects( $item, $schema['additionalProperties'] ); - } - } - - // Empty object-typed arrays must serialize as {} to match the schema. - if ( array() === $value ) { - return (object) array(); - } - } - - return $value; + return _rest_cast_empty_objects_from_schema( $value, $schema ); } /** diff --git a/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php b/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php new file mode 100644 index 0000000000000..e5a2546733b4c --- /dev/null +++ b/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php @@ -0,0 +1,163 @@ +assertSame( '{}', $this->cast_and_encode( array(), array( 'type' => 'object' ) ) ); + } + + public function test_empty_array_encodes_as_array() { + $this->assertSame( '[]', $this->cast_and_encode( array(), array( 'type' => 'array' ) ) ); + } + + public function test_empty_value_with_object_in_type_list_encodes_as_object() { + $this->assertSame( '{}', $this->cast_and_encode( array(), array( 'type' => array( 'object', 'boolean' ) ) ) ); + } + + public function test_non_array_values_are_returned_unchanged() { + $this->assertFalse( _rest_cast_empty_objects_from_schema( false, array( 'type' => array( 'object', 'boolean' ) ) ) ); + $this->assertSame( 'text', _rest_cast_empty_objects_from_schema( 'text', array( 'type' => 'string' ) ) ); + } + + public function test_value_without_schema_is_returned_unchanged() { + $this->assertSame( array(), _rest_cast_empty_objects_from_schema( array(), array() ) ); + } + + public function test_nested_properties_are_cast() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'layout' => array( + 'type' => 'object', + 'properties' => array( + 'styles' => array( 'type' => 'object' ), + ), + ), + 'fields' => array( 'type' => 'array' ), + ), + ); + + $this->assertSame( + '{"layout":{"styles":{}},"fields":[]}', + $this->cast_and_encode( + array( + 'layout' => array( 'styles' => array() ), + 'fields' => array(), + ), + $schema + ) + ); + } + + public function test_object_emptied_of_known_properties_is_still_cast() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'layout' => array( 'type' => 'object' ), + ), + ); + + $this->assertSame( '{}', $this->cast_and_encode( array(), $schema ) ); + } + + public function test_array_items_are_cast() { + $schema = array( + 'type' => 'array', + 'items' => array( 'type' => 'object' ), + ); + + $this->assertSame( '[{},{}]', $this->cast_and_encode( array( array(), array() ), $schema ) ); + } + + public function test_additional_properties_are_cast() { + $schema = array( + 'type' => 'object', + 'properties' => array( + 'known' => array( 'type' => 'array' ), + ), + 'additionalProperties' => array( 'type' => 'object' ), + ); + + $this->assertSame( + '{"known":[],"title":{}}', + $this->cast_and_encode( + array( + 'known' => array(), + 'title' => array(), + ), + $schema + ) + ); + } + + public function test_boolean_additional_properties_are_left_alone() { + $schema = array( + 'type' => 'object', + 'additionalProperties' => true, + ); + + $this->assertSame( '{"extra":[]}', $this->cast_and_encode( array( 'extra' => array() ), $schema ) ); + } + + /** + * @dataProvider data_union_keywords + * + * @param string $keyword The union keyword, `oneOf` or `anyOf`. + */ + public function test_empty_value_is_cast_when_a_union_branch_allows_an_object( $keyword ) { + $schema = array( + $keyword => array( + array( 'type' => 'string' ), + array( 'type' => 'object' ), + ), + ); + + $this->assertSame( '{}', $this->cast_and_encode( array(), $schema ) ); + } + + /** + * @dataProvider data_union_keywords + * + * @param string $keyword The union keyword, `oneOf` or `anyOf`. + */ + public function test_empty_value_is_not_cast_when_no_union_branch_allows_an_object( $keyword ) { + $schema = array( + $keyword => array( + array( 'type' => 'string' ), + array( 'type' => 'array' ), + ), + ); + + $this->assertSame( '[]', $this->cast_and_encode( array(), $schema ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_union_keywords() { + return array( + 'oneOf' => array( 'oneOf' ), + 'anyOf' => array( 'anyOf' ), + ); + } +}