diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d9b7561db2cf..bea43ce76c26 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -4,9 +4,14 @@ use ArgumentCountError; use ArrayAccess; +use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; +use JsonSerializable; use Random\Randomizer; +use Traversable; +use WeakMap; class Arr { @@ -23,6 +28,21 @@ public static function accessible($value) return is_array($value) || $value instanceof ArrayAccess; } + /** + * Determine whether the given value is arrayable. + * + * @param mixed $value + * @return bool + */ + public static function arrayable($value) + { + return is_array($value) + || $value instanceof Arrayable + || $value instanceof Traversable + || $value instanceof Jsonable + || $value instanceof JsonSerializable; + } + /** * Add an element to an array using "dot" notation if it doesn't exist. * @@ -378,6 +398,32 @@ public static function forget(&$array, $keys) } } + /** + * Get the underlying array of items from the given argument. + * + * @template TKey of array-key = array-key + * @template TValue = mixed + * + * @param array|Enumerable|Arrayable|WeakMap|Traversable|Jsonable|JsonSerializable|object $items + * @return ($items is WeakMap ? list : array) + * + * @throws \InvalidArgumentException + */ + public static function from($items) + { + return match (true) { + is_array($items) => $items, + $items instanceof Enumerable => $items->all(), + $items instanceof Arrayable => $items->toArray(), + $items instanceof WeakMap => iterator_to_array($items, false), + $items instanceof Traversable => iterator_to_array($items), + $items instanceof Jsonable => json_decode($items->toJson(), true), + $items instanceof JsonSerializable => (array) $items->jsonSerialize(), + is_object($items) => (array) $items, + default => throw new InvalidArgumentException('Items cannot be represented by a scalar value.'), + }; + } + /** * Get an item from an array using "dot" notation. * diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index d2894529ed6e..c11c9c434d89 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -12,12 +12,9 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\HigherOrderCollectionProxy; -use InvalidArgumentException; use JsonSerializable; -use Traversable; use UnexpectedValueException; use UnitEnum; -use WeakMap; use function Illuminate\Support\enum_value; @@ -1059,17 +1056,9 @@ public function __get($key) */ protected function getArrayableItems($items) { - return match (true) { - is_array($items) => $items, - $items instanceof WeakMap => throw new InvalidArgumentException('Collections can not be created using instances of WeakMap.'), - $items instanceof Enumerable => $items->all(), - $items instanceof Arrayable => $items->toArray(), - $items instanceof Traversable => iterator_to_array($items), - $items instanceof Jsonable => json_decode($items->toJson(), true), - $items instanceof JsonSerializable => (array) $items->jsonSerialize(), - $items instanceof UnitEnum => [$items], - default => (array) $items, - }; + return is_null($items) || is_scalar($items) || $items instanceof UnitEnum + ? Arr::wrap($items) + : Arr::from($items); } /** diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 55844559e711..16c8f0118993 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -77,9 +77,9 @@ function data_get($target, $key, $default = null) $segment = match ($segment) { '\*' => '*', '\{first}' => '{first}', - '{first}' => array_key_first(is_array($target) ? $target : (new Collection($target))->all()), + '{first}' => array_key_first(Arr::from($target)), '\{last}' => '{last}', - '{last}' => array_key_last(is_array($target) ? $target : (new Collection($target))->all()), + '{last}' => array_key_last(Arr::from($target)), default => $segment, }; diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 9ed063241a8f..a84c082343b7 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; trait DatabaseTruncation @@ -120,7 +121,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s $schema = $connection->getSchemaBuilder(); - return static::$allTables[$name] = (new Collection($schema->getTables($schema->getCurrentSchemaListing())))->all(); + return static::$allTables[$name] = Arr::from($schema->getTables($schema->getCurrentSchemaListing())); } /** diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 3bb47011d654..27bdb6bf0189 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1180,7 +1180,7 @@ public static function repeat(string $string, int $times) public static function replaceArray($search, $replace, $subject) { if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } $segments = explode($search, $subject); @@ -1222,15 +1222,15 @@ private static function toStringOr($value, $fallback) public static function replace($search, $replace, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } if ($subject instanceof Traversable) { - $subject = (new Collection($subject))->all(); + $subject = Arr::from($subject); } return $caseSensitive @@ -1363,7 +1363,7 @@ public static function replaceMatches($pattern, $replace, $subject, $limit = -1) public static function remove($search, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } return $caseSensitive diff --git a/tests/Support/Common.php b/tests/Support/Common.php new file mode 100644 index 000000000000..7928b8705624 --- /dev/null +++ b/tests/Support/Common.php @@ -0,0 +1,62 @@ + 'bar']; + } +} + +class TestJsonableObject implements Jsonable +{ + public function toJson($options = 0) + { + return '{"foo":"bar"}'; + } +} + +class TestJsonSerializeObject implements JsonSerializable +{ + public function jsonSerialize(): array + { + return ['foo' => 'bar']; + } +} + +class TestJsonSerializeWithScalarValueObject implements JsonSerializable +{ + public function jsonSerialize(): string + { + return 'foo'; + } +} + +class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable +{ + public $items; + + public function __construct($items = []) + { + $this->items = $items; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + public function jsonSerialize(): array + { + return json_decode(json_encode($this->items), true); + } +} diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index e5d15a06362f..a81e6eb79bee 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -11,6 +11,10 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; +use WeakMap; + +include_once 'Common.php'; +include_once 'Enums.php'; class SupportArrTest extends TestCase { @@ -32,6 +36,25 @@ public function testAccessible(): void $this->assertFalse(Arr::accessible(static fn () => null)); } + public function testArrayable(): void + { + $this->assertTrue(Arr::arrayable([])); + $this->assertTrue(Arr::arrayable(new TestArrayableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonSerializeObject)); + $this->assertTrue(Arr::arrayable(new TestTraversableAndJsonSerializableObject)); + + $this->assertFalse(Arr::arrayable(null)); + $this->assertFalse(Arr::arrayable('abc')); + $this->assertFalse(Arr::arrayable(new stdClass)); + $this->assertFalse(Arr::arrayable((object) ['a' => 1, 'b' => 2])); + $this->assertFalse(Arr::arrayable(123)); + $this->assertFalse(Arr::arrayable(12.34)); + $this->assertFalse(Arr::arrayable(true)); + $this->assertFalse(Arr::arrayable(new \DateTime)); + $this->assertFalse(Arr::arrayable(static fn () => null)); + } + public function testAdd() { $array = Arr::add(['name' => 'Desk'], 'price', 100); @@ -1485,6 +1508,32 @@ public function testForget() $this->assertEquals([2 => [1 => 'products']], $array); } + public function testFrom() + { + $this->assertSame(['foo' => 'bar'], Arr::from(['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from((object) ['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestArrayableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonSerializeObject)); + $this->assertSame(['foo'], Arr::from(new TestJsonSerializeWithScalarValueObject)); + + $this->assertSame(['name' => 'A'], Arr::from(TestEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 1], Arr::from(TestBackedEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 'A'], Arr::from(TestStringBackedEnum::A)); + + $subject = [new stdClass, new stdClass]; + $items = new TestTraversableAndJsonSerializableObject($subject); + $this->assertSame($subject, Arr::from($items)); + + $items = new WeakMap; + $items[$temp = new class {}] = 'bar'; + $this->assertSame(['bar'], Arr::from($items)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Items cannot be represented by a scalar value.'); + Arr::from(123); + } + public function testWrap() { $string = 'a'; diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 7c8104b570ae..513e1fa4187a 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -8,7 +8,6 @@ use CachingIterator; use Exception; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; @@ -18,7 +17,6 @@ use Illuminate\Support\Str; use Illuminate\Support\Stringable; use InvalidArgumentException; -use IteratorAggregate; use JsonSerializable; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -26,10 +24,10 @@ use ReflectionClass; use stdClass; use Symfony\Component\VarDumper\VarDumper; -use Traversable; use UnexpectedValueException; use WeakMap; +include_once 'Common.php'; include_once 'Enums.php'; class SupportCollectionTest extends TestCase @@ -2915,14 +2913,12 @@ public function testConstructMethodFromObject($collection) #[DataProvider('collectionClassProvider')] public function testConstructMethodFromWeakMap($collection) { - $this->expectException('InvalidArgumentException'); - $map = new WeakMap(); $object = new stdClass; $object->foo = 'bar'; $map[$object] = 3; - $data = new $collection($map); + $this->assertEquals([3], $data->all()); } public function testSplice() @@ -5787,30 +5783,6 @@ public function offsetUnset($offset): void } } -class TestArrayableObject implements Arrayable -{ - public function toArray() - { - return ['foo' => 'bar']; - } -} - -class TestJsonableObject implements Jsonable -{ - public function toJson($options = 0) - { - return '{"foo":"bar"}'; - } -} - -class TestJsonSerializeObject implements JsonSerializable -{ - public function jsonSerialize(): array - { - return ['foo' => 'bar']; - } -} - class TestJsonSerializeToStringObject implements JsonSerializable { public function jsonSerialize(): string @@ -5819,34 +5791,6 @@ public function jsonSerialize(): string } } -class TestJsonSerializeWithScalarValueObject implements JsonSerializable -{ - public function jsonSerialize(): string - { - return 'foo'; - } -} - -class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable -{ - public $items; - - public function __construct($items) - { - $this->items = $items; - } - - public function getIterator(): Traversable - { - return new ArrayIterator($this->items); - } - - public function jsonSerialize(): array - { - return json_decode(json_encode($this->items), true); - } -} - class TestCollectionMapIntoObject { public $value;