diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 68fb3cecc100..23059c37eab1 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -4,6 +4,8 @@ use ArrayAccess; use ArrayIterator; +use Illuminate\Collections\ItemNotFoundException; +use Illuminate\Collections\MultipleItemsFoundException; use Illuminate\Support\Traits\EnumeratesValues; use Illuminate\Support\Traits\Macroable; use stdClass; @@ -1050,6 +1052,37 @@ public function splitIn($numberOfGroups) return $this->chunk(ceil($this->count() / $numberOfGroups)); } + /** + * Get the first item in the collection, but only if exactly + * item exists. Otherwise, throw an exception. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return mixed + * + * @throws \Illuminate\Collections\ItemNotFoundException + * @throws \Illuminate\Collections\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + $items = $this->when($filter)->filter($filter); + + if ($items->isEmpty()) { + throw new ItemNotFoundException; + } + + if ($items->count() > 1) { + throw new MultipleItemsFoundException; + } + + return $items->first(); + } + /** * Chunk the collection into chunks of the given size. * diff --git a/src/Illuminate/Collections/ItemNotFoundException.php b/src/Illuminate/Collections/ItemNotFoundException.php new file mode 100644 index 000000000000..8f9c17f0eb74 --- /dev/null +++ b/src/Illuminate/Collections/ItemNotFoundException.php @@ -0,0 +1,9 @@ +passthru('split', func_get_args()); } + /** + * Get the first item in the collection, but only if exactly + * item exists. Otherwise, throw an exception. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return mixed + * + * @throws \Illuminate\Collections\ItemNotFoundException + * @throws \Illuminate\Collections\MultipleItemsFoundException + */ + public function sole($key = null, $operator = null, $value = null) + { + $filter = func_num_args() > 1 + ? $this->operatorForWhere(...func_get_args()) + : $key; + + return $this + ->when($filter) + ->filter($filter) + ->take(2) + ->collect() + ->sole(); + } + /** * Chunk the collection into chunks of the given size. * diff --git a/src/Illuminate/Collections/MultipleItemsFoundException.php b/src/Illuminate/Collections/MultipleItemsFoundException.php new file mode 100644 index 000000000000..e1ead76393ab --- /dev/null +++ b/src/Illuminate/Collections/MultipleItemsFoundException.php @@ -0,0 +1,9 @@ +assertSame('default', $result); } + /** + * @dataProvider collectionClassProvider + */ + public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection) + { + $collection = new $collection([ + ['name' => 'foo'], + ['name' => 'bar'], + ]); + + $this->assertSame(['name' => 'foo'], $collection->where('name', 'foo')->sole()); + $this->assertSame(['name' => 'foo'], $collection->sole('name', '=', 'foo')); + $this->assertSame(['name' => 'foo'], $collection->sole('name', 'foo')); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSoleThrowsExceptionIfNoItemsExists($collection) + { + $this->expectException(ItemNotFoundException::class); + + $collection = new $collection([ + ['name' => 'foo'], + ['name' => 'bar'], + ]); + + $collection->where('name', 'INVALID')->sole(); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSoleThrowsExceptionIfMoreThanOneItemExists($collection) + { + $this->expectException(MultipleItemsFoundException::class); + + $collection = new $collection([ + ['name' => 'foo'], + ['name' => 'foo'], + ['name' => 'bar'], + ]); + + $collection->where('name', 'foo')->sole(); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSoleReturnsFirstItemInCollectionIfOnlyOneExistsWithCallback($collection) + { + $data = new $collection(['foo', 'bar', 'baz']); + $result = $data->sole(function ($value) { + return $value === 'bar'; + }); + $this->assertSame('bar', $result); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSoleThrowsExceptionIfNoItemsExistsWithCallback($collection) + { + $this->expectException(ItemNotFoundException::class); + + $data = new $collection(['foo', 'bar', 'baz']); + + $data->sole(function ($value) { + return $value === 'invalid'; + }); + } + + /** + * @dataProvider collectionClassProvider + */ + public function testSoleThrowsExceptionIfMoreThanOneItemExistsWithCallback($collection) + { + $this->expectException(MultipleItemsFoundException::class); + + $data = new $collection(['foo', 'bar', 'bar']); + + $data->sole(function ($value) { + return $value === 'bar'; + }); + } + /** * @dataProvider collectionClassProvider */ diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index c523038c450c..dca52b769e07 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Support; +use Illuminate\Collections\MultipleItemsFoundException; use Illuminate\Support\LazyCollection; use PHPUnit\Framework\TestCase; use stdClass; @@ -977,6 +978,33 @@ public function testSomeIsLazy() }); } + public function testSoleIsLazy() + { + $this->assertEnumerates(2, function ($collection) { + try { + $collection->sole(); + } catch (MultipleItemsFoundException $e) { + // + } + }); + + $this->assertEnumeratesOnce(function ($collection) { + $collection->sole(function ($item) { + return $item === 1; + }); + }); + + $this->assertEnumerates(4, function ($collection) { + try { + $collection->sole(function ($item) { + return $item % 2 === 0; + }); + } catch (MultipleItemsFoundException $e) { + // + } + }); + } + public function testSortIsLazy() { $this->assertDoesNotEnumerate(function ($collection) {