Skip to content

[12.x] Introduce Arr::from() #55715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/Illuminate/Collections/Arr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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.
*
Expand Down Expand Up @@ -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<TKey, TValue>|Enumerable<TKey, TValue>|Arrayable<TKey, TValue>|WeakMap<object, TValue>|Traversable<TKey, TValue>|Jsonable|JsonSerializable|object $items
* @return ($items is WeakMap ? list<TValue> : array<TKey, TValue>)
*
* @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.
*
Expand Down
17 changes: 3 additions & 14 deletions src/Illuminate/Collections/Traits/EnumeratesValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Illuminate/Collections/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
3 changes: 2 additions & 1 deletion src/Illuminate/Foundation/Testing/DatabaseTruncation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()));
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/Illuminate/Support/Str.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/Support/Common.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Illuminate\Tests\Support;

use ArrayIterator;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use IteratorAggregate;
use JsonSerializable;
use Traversable;

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 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);
}
}
49 changes: 49 additions & 0 deletions tests/Support/SupportArrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand Down Expand Up @@ -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';
Expand Down
Loading