Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Added sole() and soleWhere() methods for Collections #37034

Merged
merged 15 commits into from Apr 20, 2021
Merged
33 changes: 33 additions & 0 deletions src/Illuminate/Collections/Collection.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
if (func_num_args() <= 1) {
$items = $this->when($key)->filter($key);

if ($items->isEmpty()) {
throw new ItemNotFoundException;
}

if ($items->count() > 1) {
throw new MultipleItemsFoundException;
}

return $items->first();
}

return $this->sole($this->operatorForWhere(...func_get_args()));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ternary would be a little clearer, and allows dedenting the whole method:

Suggested change
public function sole($key = null, $operator = null, $value = null)
{
if (func_num_args() <= 1) {
$items = $this->when($key)->filter($key);
if ($items->isEmpty()) {
throw new ItemNotFoundException;
}
if ($items->count() > 1) {
throw new MultipleItemsFoundException;
}
return $items->first();
}
return $this->sole($this->operatorForWhere(...func_get_args()));
}
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.
*
Expand Down
9 changes: 9 additions & 0 deletions src/Illuminate/Collections/ItemNotFoundException.php
@@ -0,0 +1,9 @@
<?php

namespace Illuminate\Collections;

use RuntimeException;

class ItemNotFoundException extends RuntimeException
{
}
26 changes: 26 additions & 0 deletions src/Illuminate/Collections/LazyCollection.php
Expand Up @@ -1010,6 +1010,32 @@ public function split($numberOfGroups)
return $this->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)
{
if (func_num_args() <= 1) {
return $this
->when($key)
->filter($key)
->take(2)
->collect()
->sole();
}

return $this->sole($this->operatorForWhere(...func_get_args()));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here:

Suggested change
public function sole($key = null, $operator = null, $value = null)
{
if (func_num_args() <= 1) {
return $this
->when($key)
->filter($key)
->take(2)
->collect()
->sole();
}
return $this->sole($this->operatorForWhere(...func_get_args()));
}
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.
*
Expand Down
9 changes: 9 additions & 0 deletions src/Illuminate/Collections/MultipleItemsFoundException.php
@@ -0,0 +1,9 @@
<?php

namespace Illuminate\Collections;

use RuntimeException;

class MultipleItemsFoundException extends RuntimeException
{
}
88 changes: 88 additions & 0 deletions tests/Support/SupportCollectionTest.php
Expand Up @@ -7,6 +7,8 @@
use ArrayObject;
use CachingIterator;
use Exception;
use Illuminate\Collections\ItemNotFoundException;
use Illuminate\Collections\MultipleItemsFoundException;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -66,6 +68,92 @@ public function testFirstWithDefaultAndWithoutCallback($collection)
$this->assertSame('default', $result);
}

/**
* @dataProvider collectionClassProvider
*/
public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add tests in SupportLazyCollectionIsLazyTest.php ensuring this method is lazy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure, I didn't realise those tests were there, so I'll add some to it

{
$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
*/
Expand Down
28 changes: 28 additions & 0 deletions tests/Support/SupportLazyCollectionIsLazyTest.php
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Tests\Support;

use Illuminate\Collections\MultipleItemsFoundException;
use Illuminate\Support\LazyCollection;
use PHPUnit\Framework\TestCase;
use stdClass;
Expand Down Expand Up @@ -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) {
Expand Down