Skip to content

Commit 64c440f

Browse files
daniserSergey Danilchenko
andauthored
[12.x] Introduce Arr::from() (#55715)
* [12.x] Introduce Arr::from() * Arr::from should not wrap scalar values * Arr::arrayable helper function * Arr::from can be used with raw objects except enums; handle UnitEnum instances as scalars * Add tests * Replace implicit `getArrayableItems` usage with `Arr::from` * Handle enums as regular objects in `Arr::from`; wrap enums in `getArrayableItems` * Remove useless `Arr::arrayable` checks * Revert to `instanceof Traversable` checks --------- Co-authored-by: Sergey Danilchenko <s.danilchenko@ttbooking.ru>
1 parent 98ae8ed commit 64c440f

File tree

8 files changed

+171
-80
lines changed

8 files changed

+171
-80
lines changed

src/Illuminate/Collections/Arr.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
use ArgumentCountError;
66
use ArrayAccess;
7+
use Illuminate\Contracts\Support\Arrayable;
8+
use Illuminate\Contracts\Support\Jsonable;
79
use Illuminate\Support\Traits\Macroable;
810
use InvalidArgumentException;
11+
use JsonSerializable;
912
use Random\Randomizer;
13+
use Traversable;
14+
use WeakMap;
1015

1116
class Arr
1217
{
@@ -23,6 +28,21 @@ public static function accessible($value)
2328
return is_array($value) || $value instanceof ArrayAccess;
2429
}
2530

31+
/**
32+
* Determine whether the given value is arrayable.
33+
*
34+
* @param mixed $value
35+
* @return bool
36+
*/
37+
public static function arrayable($value)
38+
{
39+
return is_array($value)
40+
|| $value instanceof Arrayable
41+
|| $value instanceof Traversable
42+
|| $value instanceof Jsonable
43+
|| $value instanceof JsonSerializable;
44+
}
45+
2646
/**
2747
* Add an element to an array using "dot" notation if it doesn't exist.
2848
*
@@ -378,6 +398,32 @@ public static function forget(&$array, $keys)
378398
}
379399
}
380400

401+
/**
402+
* Get the underlying array of items from the given argument.
403+
*
404+
* @template TKey of array-key = array-key
405+
* @template TValue = mixed
406+
*
407+
* @param array<TKey, TValue>|Enumerable<TKey, TValue>|Arrayable<TKey, TValue>|WeakMap<object, TValue>|Traversable<TKey, TValue>|Jsonable|JsonSerializable|object $items
408+
* @return ($items is WeakMap ? list<TValue> : array<TKey, TValue>)
409+
*
410+
* @throws \InvalidArgumentException
411+
*/
412+
public static function from($items)
413+
{
414+
return match (true) {
415+
is_array($items) => $items,
416+
$items instanceof Enumerable => $items->all(),
417+
$items instanceof Arrayable => $items->toArray(),
418+
$items instanceof WeakMap => iterator_to_array($items, false),
419+
$items instanceof Traversable => iterator_to_array($items),
420+
$items instanceof Jsonable => json_decode($items->toJson(), true),
421+
$items instanceof JsonSerializable => (array) $items->jsonSerialize(),
422+
is_object($items) => (array) $items,
423+
default => throw new InvalidArgumentException('Items cannot be represented by a scalar value.'),
424+
};
425+
}
426+
381427
/**
382428
* Get an item from an array using "dot" notation.
383429
*

src/Illuminate/Collections/Traits/EnumeratesValues.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@
1212
use Illuminate\Support\Collection;
1313
use Illuminate\Support\Enumerable;
1414
use Illuminate\Support\HigherOrderCollectionProxy;
15-
use InvalidArgumentException;
1615
use JsonSerializable;
17-
use Traversable;
1816
use UnexpectedValueException;
1917
use UnitEnum;
20-
use WeakMap;
2118

2219
use function Illuminate\Support\enum_value;
2320

@@ -1059,17 +1056,9 @@ public function __get($key)
10591056
*/
10601057
protected function getArrayableItems($items)
10611058
{
1062-
return match (true) {
1063-
is_array($items) => $items,
1064-
$items instanceof WeakMap => throw new InvalidArgumentException('Collections can not be created using instances of WeakMap.'),
1065-
$items instanceof Enumerable => $items->all(),
1066-
$items instanceof Arrayable => $items->toArray(),
1067-
$items instanceof Traversable => iterator_to_array($items),
1068-
$items instanceof Jsonable => json_decode($items->toJson(), true),
1069-
$items instanceof JsonSerializable => (array) $items->jsonSerialize(),
1070-
$items instanceof UnitEnum => [$items],
1071-
default => (array) $items,
1072-
};
1059+
return is_null($items) || is_scalar($items) || $items instanceof UnitEnum
1060+
? Arr::wrap($items)
1061+
: Arr::from($items);
10731062
}
10741063

10751064
/**

src/Illuminate/Collections/helpers.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ function data_get($target, $key, $default = null)
7777
$segment = match ($segment) {
7878
'\*' => '*',
7979
'\{first}' => '{first}',
80-
'{first}' => array_key_first(is_array($target) ? $target : (new Collection($target))->all()),
80+
'{first}' => array_key_first(Arr::from($target)),
8181
'\{last}' => '{last}',
82-
'{last}' => array_key_last(is_array($target) ? $target : (new Collection($target))->all()),
82+
'{last}' => array_key_last(Arr::from($target)),
8383
default => $segment,
8484
};
8585

src/Illuminate/Foundation/Testing/DatabaseTruncation.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Contracts\Console\Kernel;
66
use Illuminate\Database\ConnectionInterface;
77
use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands;
8+
use Illuminate\Support\Arr;
89
use Illuminate\Support\Collection;
910

1011
trait DatabaseTruncation
@@ -120,7 +121,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s
120121

121122
$schema = $connection->getSchemaBuilder();
122123

123-
return static::$allTables[$name] = (new Collection($schema->getTables($schema->getCurrentSchemaListing())))->all();
124+
return static::$allTables[$name] = Arr::from($schema->getTables($schema->getCurrentSchemaListing()));
124125
}
125126

126127
/**

src/Illuminate/Support/Str.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,7 @@ public static function repeat(string $string, int $times)
11801180
public static function replaceArray($search, $replace, $subject)
11811181
{
11821182
if ($replace instanceof Traversable) {
1183-
$replace = (new Collection($replace))->all();
1183+
$replace = Arr::from($replace);
11841184
}
11851185

11861186
$segments = explode($search, $subject);
@@ -1222,15 +1222,15 @@ private static function toStringOr($value, $fallback)
12221222
public static function replace($search, $replace, $subject, $caseSensitive = true)
12231223
{
12241224
if ($search instanceof Traversable) {
1225-
$search = (new Collection($search))->all();
1225+
$search = Arr::from($search);
12261226
}
12271227

12281228
if ($replace instanceof Traversable) {
1229-
$replace = (new Collection($replace))->all();
1229+
$replace = Arr::from($replace);
12301230
}
12311231

12321232
if ($subject instanceof Traversable) {
1233-
$subject = (new Collection($subject))->all();
1233+
$subject = Arr::from($subject);
12341234
}
12351235

12361236
return $caseSensitive
@@ -1363,7 +1363,7 @@ public static function replaceMatches($pattern, $replace, $subject, $limit = -1)
13631363
public static function remove($search, $subject, $caseSensitive = true)
13641364
{
13651365
if ($search instanceof Traversable) {
1366-
$search = (new Collection($search))->all();
1366+
$search = Arr::from($search);
13671367
}
13681368

13691369
return $caseSensitive

tests/Support/Common.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Support;
4+
5+
use ArrayIterator;
6+
use Illuminate\Contracts\Support\Arrayable;
7+
use Illuminate\Contracts\Support\Jsonable;
8+
use IteratorAggregate;
9+
use JsonSerializable;
10+
use Traversable;
11+
12+
class TestArrayableObject implements Arrayable
13+
{
14+
public function toArray()
15+
{
16+
return ['foo' => 'bar'];
17+
}
18+
}
19+
20+
class TestJsonableObject implements Jsonable
21+
{
22+
public function toJson($options = 0)
23+
{
24+
return '{"foo":"bar"}';
25+
}
26+
}
27+
28+
class TestJsonSerializeObject implements JsonSerializable
29+
{
30+
public function jsonSerialize(): array
31+
{
32+
return ['foo' => 'bar'];
33+
}
34+
}
35+
36+
class TestJsonSerializeWithScalarValueObject implements JsonSerializable
37+
{
38+
public function jsonSerialize(): string
39+
{
40+
return 'foo';
41+
}
42+
}
43+
44+
class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable
45+
{
46+
public $items;
47+
48+
public function __construct($items = [])
49+
{
50+
$this->items = $items;
51+
}
52+
53+
public function getIterator(): Traversable
54+
{
55+
return new ArrayIterator($this->items);
56+
}
57+
58+
public function jsonSerialize(): array
59+
{
60+
return json_decode(json_encode($this->items), true);
61+
}
62+
}

tests/Support/SupportArrTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
use InvalidArgumentException;
1212
use PHPUnit\Framework\TestCase;
1313
use stdClass;
14+
use WeakMap;
15+
16+
include_once 'Common.php';
17+
include_once 'Enums.php';
1418

1519
class SupportArrTest extends TestCase
1620
{
@@ -32,6 +36,25 @@ public function testAccessible(): void
3236
$this->assertFalse(Arr::accessible(static fn () => null));
3337
}
3438

39+
public function testArrayable(): void
40+
{
41+
$this->assertTrue(Arr::arrayable([]));
42+
$this->assertTrue(Arr::arrayable(new TestArrayableObject));
43+
$this->assertTrue(Arr::arrayable(new TestJsonableObject));
44+
$this->assertTrue(Arr::arrayable(new TestJsonSerializeObject));
45+
$this->assertTrue(Arr::arrayable(new TestTraversableAndJsonSerializableObject));
46+
47+
$this->assertFalse(Arr::arrayable(null));
48+
$this->assertFalse(Arr::arrayable('abc'));
49+
$this->assertFalse(Arr::arrayable(new stdClass));
50+
$this->assertFalse(Arr::arrayable((object) ['a' => 1, 'b' => 2]));
51+
$this->assertFalse(Arr::arrayable(123));
52+
$this->assertFalse(Arr::arrayable(12.34));
53+
$this->assertFalse(Arr::arrayable(true));
54+
$this->assertFalse(Arr::arrayable(new \DateTime));
55+
$this->assertFalse(Arr::arrayable(static fn () => null));
56+
}
57+
3558
public function testAdd()
3659
{
3760
$array = Arr::add(['name' => 'Desk'], 'price', 100);
@@ -1485,6 +1508,32 @@ public function testForget()
14851508
$this->assertEquals([2 => [1 => 'products']], $array);
14861509
}
14871510

1511+
public function testFrom()
1512+
{
1513+
$this->assertSame(['foo' => 'bar'], Arr::from(['foo' => 'bar']));
1514+
$this->assertSame(['foo' => 'bar'], Arr::from((object) ['foo' => 'bar']));
1515+
$this->assertSame(['foo' => 'bar'], Arr::from(new TestArrayableObject));
1516+
$this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonableObject));
1517+
$this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonSerializeObject));
1518+
$this->assertSame(['foo'], Arr::from(new TestJsonSerializeWithScalarValueObject));
1519+
1520+
$this->assertSame(['name' => 'A'], Arr::from(TestEnum::A));
1521+
$this->assertSame(['name' => 'A', 'value' => 1], Arr::from(TestBackedEnum::A));
1522+
$this->assertSame(['name' => 'A', 'value' => 'A'], Arr::from(TestStringBackedEnum::A));
1523+
1524+
$subject = [new stdClass, new stdClass];
1525+
$items = new TestTraversableAndJsonSerializableObject($subject);
1526+
$this->assertSame($subject, Arr::from($items));
1527+
1528+
$items = new WeakMap;
1529+
$items[$temp = new class {}] = 'bar';
1530+
$this->assertSame(['bar'], Arr::from($items));
1531+
1532+
$this->expectException(InvalidArgumentException::class);
1533+
$this->expectExceptionMessage('Items cannot be represented by a scalar value.');
1534+
Arr::from(123);
1535+
}
1536+
14881537
public function testWrap()
14891538
{
14901539
$string = 'a';

0 commit comments

Comments
 (0)