-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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
[8.x] Added sole() and soleWhere() methods for Collections #37034
[8.x] Added sole() and soleWhere() methods for Collections #37034
Conversation
Ping @JosephSilber |
I was never a fan of the |
* @throws ItemNotFoundException | ||
* @throws MultipleItemsFoundException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We always use fully qualified class names in DocBlocks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh okay, sorry I wasn't aware of that. I'll get them changed to be fully qualified
public function sole(callable $callback = null); | ||
|
||
/** | ||
* Get the first item by the given key value pair, but only if | ||
* exactly one item matches the criteria. Otherwise, throw | ||
* an exception. | ||
* | ||
* @param string $key | ||
* @param mixed $operator | ||
* @param mixed $value | ||
* @return mixed | ||
*/ | ||
public function soleWhere($key, $operator = null, $value = null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't add methods to an interface mid-cycle.
Remove these from the interface. After it's merged, submit a separate PR to master
to add them to the interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense thinking about it. I'll take this out and make a separate PR if this one gets merged
* @throws ItemNotFoundException | ||
* @throws MultipleItemsFoundException | ||
*/ | ||
public function sole(callable $callback = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation is way more complex than it needs to be.
Something like this (not tested) should suffice:
public function sole(callable $callback = null)
{
return $this
->when($callback)
->filter($callback)
->take(2)
->collect()
->sole();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do agree that it's more complex. I was looking at some of the existing code for things like the first()
method and just assumed that it had been done like that for a specific reason. I've not really used LazyCollections too much so I wasn't 100% sure on the best way to implement it. Thanks for the advice on this one
/** | ||
* @dataProvider collectionClassProvider | ||
*/ | ||
public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
public function soleWhere($key, $operator = null, $value = null) | ||
{ | ||
return $this->sole($this->operatorForWhere(...func_get_args())); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since, unlike the first
method, sole
doesn't take a $default
parameter , there's no need for a separate soleWhere
method. Constraints can be passed into the sole
method directly, like we do in contains
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. So do you suggest removed the soleWhere
method completely?
*/ | ||
public function sole(callable $callback = null) | ||
{ | ||
$items = $this->filter($callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code as is will filter out null
s when no $callback
has been provided.
$items = $this->filter($callback); | |
$items = $this->when($callback)->filter($callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh I must have missed that, cheers
It's a pity the exceptions aren't shared (maybe down the line in 9.x). Then this: framework/src/Illuminate/Database/Concerns/BuildsQueries.php Lines 256 to 266 in 277c2fb
could be changed to simply: return $this->take(2)->get($columns)->sole(); |
Thanks for the feedback on all of this @JosephSilber! I'll make all of the changes for it now 😄 And I do agree that it'd be cool to have the shared functionality like in your suggestion. |
I've made the changes that you suggested @JosephSilber and from what I can see, they all seem to working. One thing thing I'm not too sure about though is the new test I've added. I have a feeling that I might not have done it right; apologies if that's the case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking better.
Seems like you forgot to add operator support to sole
.
public function testSoleIsLazy() | ||
{ | ||
$data = $this->make([['a' => 1], ['a' => 2], ['a' => 3]]); | ||
|
||
$this->assertEnumeratesCollection($data, 3, function ($collection) { | ||
$collection->sole(function ($item) { | ||
return $item['a'] === 2; | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public function testSoleIsLazy() | |
{ | |
$data = $this->make([['a' => 1], ['a' => 2], ['a' => 3]]); | |
$this->assertEnumeratesCollection($data, 3, function ($collection) { | |
$collection->sole(function ($item) { | |
return $item['a'] === 2; | |
}); | |
}); | |
} | |
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) { | |
// | |
} | |
}); | |
} |
Be sure to also import MultipleItemsFoundException
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion again, I've just pushed up the new test changes :)
If it's not added now, we won't be able to add it until 9.x, since it would be a breaking change in the function signature. |
@JosephSilber In your opinion, do you think it's something that should be added now? I can try and add it if so |
Would be nice. If you can't figure it out, let me know, and I'll try to find a few minutes to do it for you 👍 |
Sweet, I'll take a look at it now then. Thanks again for all the help on this one, I really appreciate it and it's giving me a chance to learn a bit more at the same time 😄 |
god, this is a lot of extra code to avoid saying if($collection->count() === 1){
return $collection->first();
} |
@JosephSilber I think I've added the operator support, but please let me know if I've missed anything |
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())); | ||
} |
There was a problem hiding this comment.
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:
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(); | |
} |
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())); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here:
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(); | |
} |
@JosephSilber Ah yeah, the ternary makes it look much cleaner, thanks! 😄 |
It looks like 2 of the test checks failed, but I don't think I can rerun them myself? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
$items = $this->when($filter)->filter($filter); | ||
|
||
if ($items->isEmpty()) { | ||
throw new ItemNotFoundException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be nice if the query builder etc would throw the exact same exceptions when calling sole()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR apparently introduced a new |
@driesvints Sorry, my bad here! When I made the exceptions, I must have assumed at the time that the namespace was the same as the file path. I should have double-checked this before pushing 😔 |
@ash-jc-allen don't worry about it. I've sent in a PR to fix this: #38449 |
Hey! This PR should hopefully add new
sole()
andsoleWhere()
methods to Collections and LazyCollections. I've taken inspiration from the similarsole()
method for the database queries and think that these could be pretty handy additions.The
sole()
method should work in a similar way to thefirst()
method does but does not require a default value as a second parameter. Likewise, thesoleWhere()
method should work in a similar way to thefirstWhere()
method.If either of the methods are executed and there are no items to be returned, an
Illuminate\Collections\ItemNotFoundException
will be thrown. If the either of the methods are executed are there is more than one item to be returned, anIlluminate\Collections\MultipleItemsFoundException
will be thrown.Here's a couple of quick examples of how they could be used:
sole():
soleWhere():
If there's anything that might need changing on these, please let me know 😄