ideas
ideas copied to clipboard
Add `except` method to Eloquent query builder
This method will exclude given instance(s) from the query. For example:
// Excluding single model:
User::where('id', '!=', $user->id)->get();
// will be shortened to:
User::except($user)->get();
// Excluding multiple model:
User::whereNotIn('id', [$userA->id, $userB->id])->get();
// will be shortened to:
User::except([$userA, $userB])->get();
except
can compare model's primaryKey
property with getKey()
of given instances.
It can also accept primary keys as integers:
// Excluding single id:
User::whereNot('id', '!=', 5)->get();
// will be shortened to:
User::except(5)->get();
// Excluding multiple ids:
User::whereNotIn('id', [5, 7])->get();
// will be shortened to:
User::except([5, 7])->get();
This method can result in a slightly better coding experience which is I think is Laravel's goal.
I quite like this, but I think it's best to stick to SQL like function names. Although it's more verbose, it's clearer and fits the paradigm better.
I think it's OK since we have fluent methods like latest
, oldest
or even find
and aliases like take
, skip
.
public function scopeExcept(Builder $query, User $user)
{
return $builder->where('id', '!=', $user->id);
}
Now it is type hinted. Easy to maintain. And not magically hidden in the query builder.
None of your mentioned methods are the same as what you are suggesting.
latest -> just a query helper, no arguments olders -> same as latest find -> takes an ID not a model. This method actually causes issues with support, people often use it as part of the query builder and wonder why things don't work. take and skip -> these are just nicer names for part of the query?
I think the way to make this better would be to remove the restriction that the method arguments have to be the instance of an eloquent model. It could therefore be documented that except() always expects the arguments to be the primary keys of the model which should be the instance of an int.
Why? Because if it requires an instance of a model, and we are on an update route like update/user/id
. Calling ->except()
on some task inside that route will require me to query the user model first with the current id and then pass it to the except method which will do another query. To me this is highly inefficient as it will take 2 queries to do a job that would have otherwise been done by one query and provided better performance.
I would therefore understand why someone would avoid these except methods for performance reason. The better business case for a PR like this is if it also enables the end users to write slightly
better and faster code than what is already provided by the sql like methods.
Thank you @ratatatKE, I've updated the idea.
Now it is type hinted. Easy to maintain. And not magically hidden in the query builder.
It is unfortunately still hidden behind a bunch of magic. More so than the query builder I'd say.
You could, however, achieve this quite nicely with a macro, but I suspect a PR could be beneficial. If I was just pulling something off the top of my head I'd say a method like this in Illuminate\Database\Eloquent\Builder
.
public function ignore()
{
$models = get_func_args();
$currentClass = get_class($this->getModel());
$ids = array_map(function (Model $model) {
return $model->getKey();
}, array_filter($models, function ($model) use($currentClass) {
return $model instanceof Model && get_class($model) === $currentClass;
});
if (count($ids) > 1) {
$this->whereNotIn($this->getModel()->getKeyName(), $ids);
} else {
$this->whereNot($this->getModel()->getKeyName(), array_values($ids)[0]);
}
return $this;
}
I chose ignore
over except
because it's clearer.
You could, however, achieve this quite nicely with a macro, but I suspect a PR could be beneficial. If I was just pulling something off the top of my head I'd say a method like this in
Illuminate\Database\Eloquent\Builder
.public function ignore() { $models = get_func_args(); $currentClass = get_class($this->getModel()); $ids = array_map(function (Model $model) { return $model->getKey(); }, array_filter($models, function ($model) use($currentClass) { return $model instanceof Model && get_class($model) === $currentClass; }); if (count($ids) > 1) { $this->whereNotIn($this->getModel()->getKeyName(), $ids); } else { $this->whereNot($this->getModel()->getKeyName(), array_values($ids)[0]); } return $this; }
I chose
ignore
overexcept
because it's clearer.
In that same light... some people would argue that exclude
is much more clearer?
We need to do something like this
Model::where('firstname', 'like', '%$firstname%')
->exceptWhere('lastname, 'like', '%$lastname%');
https://stackoverflow.com/questions/65124775/except-query-in-laravel/65124821#65124821
This worked
Model::where('firstname', 'LIKE', "%$firstname%")
->whereNotExists(function ($q) use ($lastname) {
$q->where('lastname', 'like', "%$lastname%");
})->get();
We need to do something like this
Model::where('firstname', 'like', '%$firstname%') ->exceptWhere('lastname, 'like', '%$lastname%');
https://stackoverflow.com/questions/65124775/except-query-in-laravel/65124821#65124821
This worked
Model::where('firstname', 'LIKE', "%$firstname%") ->whereNotExists(function ($q) use ($lastname) { $q->where('lastname', 'like', "%$lastname%"); })->get();
I like this one