diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index ecae45083a6ca..9641cd8229ae3 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5256,6 +5256,7 @@ function sanitize_option( $option, $value ) { * This is similar to `array_walk_recursive()` but acts upon objects too. * * @since 4.4.0 + * @since 7.2.0 Added support for ArrayObject entries. * * @param mixed $value The array, object, or scalar. * @param callable $callback The function to map onto $value. @@ -5271,6 +5272,13 @@ function map_deep( $value, $callback ) { $value[ $index ] = map_deep( $item, $callback ); } } elseif ( is_object( $value ) ) { + // get_object_vars() does not include ArrayObject entries on PHP 7.4 and later. + if ( $value instanceof ArrayObject && ! ( $value->getFlags() & ArrayObject::STD_PROP_LIST ) ) { + foreach ( $value->getArrayCopy() as $index => $item ) { + $value[ $index ] = map_deep( $item, $callback ); + } + } + $object_vars = get_object_vars( $value ); foreach ( $object_vars as $property_name => $property_value ) { $value->$property_name = map_deep( $property_value, $callback ); diff --git a/tests/phpunit/tests/formatting/mapDeep.php b/tests/phpunit/tests/formatting/mapDeep.php index c4856e885bf60..5277ab82c1637 100644 --- a/tests/phpunit/tests/formatting/mapDeep.php +++ b/tests/phpunit/tests/formatting/mapDeep.php @@ -172,6 +172,120 @@ public function test_map_deep_should_map_array_elements_passed_by_reference() { ); } + /** + * @ticket 53160 + * @dataProvider data_array_object_flags + * + * @param int $flags ArrayObject flags. + * @param string $expected_value Expected value of the stored entry. + */ + public function test_map_deep_should_map_array_object_entries_and_public_properties( $flags, $expected_value ) { + $value = new class( array( 'entry' => 'x' ), $flags ) extends ArrayObject { + public $property = 'a'; + }; + + $result = map_deep( $value, array( $this, 'append_baba' ) ); + + $this->assertSame( $value, $result, 'The original object should be returned.' ); + $this->assertSame( $flags, $result->getFlags(), 'The flags should not change.' ); + $this->assertSame( 'ababa', $result->property, 'Public properties should still be mapped.' ); + $this->assertSame( array( 'entry' => $expected_value ), $result->getArrayCopy() ); + } + + /** + * Data provider for ArrayObject flag combinations. + * + * @return array[] + */ + public static function data_array_object_flags() { + return array( + 'default' => array( 0, 'xbaba' ), + 'array entries as properties' => array( ArrayObject::ARRAY_AS_PROPS, 'xbaba' ), + 'public properties only' => array( ArrayObject::STD_PROP_LIST, 'x' ), + 'both flags' => array( ArrayObject::STD_PROP_LIST | ArrayObject::ARRAY_AS_PROPS, 'x' ), + ); + } + + /** + * @ticket 53160 + */ + public function test_map_deep_should_map_nested_array_objects() { + $inner = new ArrayObject( array( 'x' ) ); + $outer = new ArrayObject( + array( + 'array' => array( 'a' ), + 'object' => (object) array( 'inner' => $inner ), + ) + ); + + $result = map_deep( array( 'outer' => $outer ), array( $this, 'append_baba' ) ); + + $this->assertSame( $outer, $result['outer'] ); + $this->assertSame( array( 'ababa' ), $outer['array'] ); + $this->assertSame( $inner, $outer['object']->inner ); + $this->assertSame( array( 'xbaba' ), $inner->getArrayCopy() ); + } + + /** + * Object-backed storage is deprecated as of PHP 8.5. + * + * @ticket 53160 + * @requires PHP < 8.5 + */ + public function test_map_deep_should_map_array_object_backed_by_an_object() { + $storage = (object) array( 'entry' => 'x' ); + $value = new ArrayObject( $storage ); + + $this->assertSame( $value, map_deep( $value, array( $this, 'append_baba' ) ) ); + $this->assertSame( 'xbaba', $storage->entry ); + } + + /** + * @ticket 53160 + */ + public function test_map_deep_should_not_call_callback_for_empty_array_object() { + $value = new ArrayObject(); + + $result = map_deep( + $value, + function () { + $this->fail( 'The callback should not be called for an empty ArrayObject.' ); + } + ); + + $this->assertSame( $value, $result ); + $this->assertSame( array(), $result->getArrayCopy() ); + } + + /** + * @ticket 53160 + * @dataProvider data_array_object_callers + * + * @param callable $callback Function using map_deep(). + * @param string $input Input value. + * @param string $expected Expected mapped value. + */ + public function test_map_deep_callers_should_map_array_object_entries( $callback, $input, $expected ) { + $value = new ArrayObject( array( 'entry' => $input ) ); + + $this->assertSame( $value, $callback( $value ) ); + $this->assertSame( array( 'entry' => $expected ), $value->getArrayCopy() ); + } + + /** + * Data provider for functions using map_deep(). + * + * @return array[] + */ + public static function data_array_object_callers() { + return array( + 'strip slashes' => array( 'stripslashes_deep', "a\\'b", "a'b" ), + 'encode URL' => array( 'urlencode_deep', 'a b', 'a+b' ), + 'encode raw URL' => array( 'rawurlencode_deep', 'a b', 'a%20b' ), + 'decode URL' => array( 'urldecode_deep', 'a+b', 'a b' ), + ); + } + public function append_baba( $value ) { return $value . 'baba'; }