Skip to content

Commit ec7a532

Browse files
committed
Merge branch 'feature/collection-sole' into 8.x
2 parents 26a7353 + 3ea827c commit ec7a532

File tree

6 files changed

+191
-0
lines changed

6 files changed

+191
-0
lines changed

src/Illuminate/Collections/Collection.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use ArrayAccess;
66
use ArrayIterator;
7+
use Illuminate\Collections\ItemNotFoundException;
8+
use Illuminate\Collections\MultipleItemsFoundException;
79
use Illuminate\Support\Traits\EnumeratesValues;
810
use Illuminate\Support\Traits\Macroable;
911
use stdClass;
@@ -1050,6 +1052,36 @@ public function splitIn($numberOfGroups)
10501052
return $this->chunk(ceil($this->count() / $numberOfGroups));
10511053
}
10521054

1055+
/**
1056+
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
1057+
*
1058+
* @param mixed $key
1059+
* @param mixed $operator
1060+
* @param mixed $value
1061+
* @return mixed
1062+
*
1063+
* @throws \Illuminate\Collections\ItemNotFoundException
1064+
* @throws \Illuminate\Collections\MultipleItemsFoundException
1065+
*/
1066+
public function sole($key = null, $operator = null, $value = null)
1067+
{
1068+
$filter = func_num_args() > 1
1069+
? $this->operatorForWhere(...func_get_args())
1070+
: $key;
1071+
1072+
$items = $this->when($filter)->filter($filter);
1073+
1074+
if ($items->isEmpty()) {
1075+
throw new ItemNotFoundException;
1076+
}
1077+
1078+
if ($items->count() > 1) {
1079+
throw new MultipleItemsFoundException;
1080+
}
1081+
1082+
return $items->first();
1083+
}
1084+
10531085
/**
10541086
* Chunk the collection into chunks of the given size.
10551087
*
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Illuminate\Collections;
4+
5+
use RuntimeException;
6+
7+
class ItemNotFoundException extends RuntimeException
8+
{
9+
}

src/Illuminate/Collections/LazyCollection.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,31 @@ public function split($numberOfGroups)
10101010
return $this->passthru('split', func_get_args());
10111011
}
10121012

1013+
/**
1014+
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
1015+
*
1016+
* @param mixed $key
1017+
* @param mixed $operator
1018+
* @param mixed $value
1019+
* @return mixed
1020+
*
1021+
* @throws \Illuminate\Collections\ItemNotFoundException
1022+
* @throws \Illuminate\Collections\MultipleItemsFoundException
1023+
*/
1024+
public function sole($key = null, $operator = null, $value = null)
1025+
{
1026+
$filter = func_num_args() > 1
1027+
? $this->operatorForWhere(...func_get_args())
1028+
: $key;
1029+
1030+
return $this
1031+
->when($filter)
1032+
->filter($filter)
1033+
->take(2)
1034+
->collect()
1035+
->sole();
1036+
}
1037+
10131038
/**
10141039
* Chunk the collection into chunks of the given size.
10151040
*
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Illuminate\Collections;
4+
5+
use RuntimeException;
6+
7+
class MultipleItemsFoundException extends RuntimeException
8+
{
9+
}

tests/Support/SupportCollectionTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use ArrayObject;
88
use CachingIterator;
99
use Exception;
10+
use Illuminate\Collections\ItemNotFoundException;
11+
use Illuminate\Collections\MultipleItemsFoundException;
1012
use Illuminate\Contracts\Support\Arrayable;
1113
use Illuminate\Contracts\Support\Jsonable;
1214
use Illuminate\Support\Collection;
@@ -66,6 +68,92 @@ public function testFirstWithDefaultAndWithoutCallback($collection)
6668
$this->assertSame('default', $result);
6769
}
6870

71+
/**
72+
* @dataProvider collectionClassProvider
73+
*/
74+
public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection)
75+
{
76+
$collection = new $collection([
77+
['name' => 'foo'],
78+
['name' => 'bar'],
79+
]);
80+
81+
$this->assertSame(['name' => 'foo'], $collection->where('name', 'foo')->sole());
82+
$this->assertSame(['name' => 'foo'], $collection->sole('name', '=', 'foo'));
83+
$this->assertSame(['name' => 'foo'], $collection->sole('name', 'foo'));
84+
}
85+
86+
/**
87+
* @dataProvider collectionClassProvider
88+
*/
89+
public function testSoleThrowsExceptionIfNoItemsExists($collection)
90+
{
91+
$this->expectException(ItemNotFoundException::class);
92+
93+
$collection = new $collection([
94+
['name' => 'foo'],
95+
['name' => 'bar'],
96+
]);
97+
98+
$collection->where('name', 'INVALID')->sole();
99+
}
100+
101+
/**
102+
* @dataProvider collectionClassProvider
103+
*/
104+
public function testSoleThrowsExceptionIfMoreThanOneItemExists($collection)
105+
{
106+
$this->expectException(MultipleItemsFoundException::class);
107+
108+
$collection = new $collection([
109+
['name' => 'foo'],
110+
['name' => 'foo'],
111+
['name' => 'bar'],
112+
]);
113+
114+
$collection->where('name', 'foo')->sole();
115+
}
116+
117+
/**
118+
* @dataProvider collectionClassProvider
119+
*/
120+
public function testSoleReturnsFirstItemInCollectionIfOnlyOneExistsWithCallback($collection)
121+
{
122+
$data = new $collection(['foo', 'bar', 'baz']);
123+
$result = $data->sole(function ($value) {
124+
return $value === 'bar';
125+
});
126+
$this->assertSame('bar', $result);
127+
}
128+
129+
/**
130+
* @dataProvider collectionClassProvider
131+
*/
132+
public function testSoleThrowsExceptionIfNoItemsExistsWithCallback($collection)
133+
{
134+
$this->expectException(ItemNotFoundException::class);
135+
136+
$data = new $collection(['foo', 'bar', 'baz']);
137+
138+
$data->sole(function ($value) {
139+
return $value === 'invalid';
140+
});
141+
}
142+
143+
/**
144+
* @dataProvider collectionClassProvider
145+
*/
146+
public function testSoleThrowsExceptionIfMoreThanOneItemExistsWithCallback($collection)
147+
{
148+
$this->expectException(MultipleItemsFoundException::class);
149+
150+
$data = new $collection(['foo', 'bar', 'bar']);
151+
152+
$data->sole(function ($value) {
153+
return $value === 'bar';
154+
});
155+
}
156+
69157
/**
70158
* @dataProvider collectionClassProvider
71159
*/

tests/Support/SupportLazyCollectionIsLazyTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Tests\Support;
44

5+
use Illuminate\Collections\MultipleItemsFoundException;
56
use Illuminate\Support\LazyCollection;
67
use PHPUnit\Framework\TestCase;
78
use stdClass;
@@ -977,6 +978,33 @@ public function testSomeIsLazy()
977978
});
978979
}
979980

981+
public function testSoleIsLazy()
982+
{
983+
$this->assertEnumerates(2, function ($collection) {
984+
try {
985+
$collection->sole();
986+
} catch (MultipleItemsFoundException $e) {
987+
//
988+
}
989+
});
990+
991+
$this->assertEnumeratesOnce(function ($collection) {
992+
$collection->sole(function ($item) {
993+
return $item === 1;
994+
});
995+
});
996+
997+
$this->assertEnumerates(4, function ($collection) {
998+
try {
999+
$collection->sole(function ($item) {
1000+
return $item % 2 === 0;
1001+
});
1002+
} catch (MultipleItemsFoundException $e) {
1003+
//
1004+
}
1005+
});
1006+
}
1007+
9801008
public function testSortIsLazy()
9811009
{
9821010
$this->assertDoesNotEnumerate(function ($collection) {

0 commit comments

Comments
 (0)