ideas icon indicating copy to clipboard operation
ideas copied to clipboard

Add `except` method to Eloquent query builder

Open AmirrezaNasiri opened this issue 4 years ago • 10 comments

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.

AmirrezaNasiri avatar Mar 05 '20 02:03 AmirrezaNasiri

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.

ryangjchandler avatar Mar 05 '20 22:03 ryangjchandler

I think it's OK since we have fluent methods like latest, oldest or even find and aliases like take, skip.

AmirrezaNasiri avatar Mar 06 '20 09:03 AmirrezaNasiri

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?

robclancy avatar Mar 13 '20 18:03 robclancy

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.

ratatatKE avatar Mar 14 '20 17:03 ratatatKE

Thank you @ratatatKE, I've updated the idea.

AmirrezaNasiri avatar Mar 14 '20 18:03 AmirrezaNasiri

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.

ollieread avatar Mar 15 '20 17:03 ollieread

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.

ollieread avatar Mar 15 '20 17:03 ollieread

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.

In that same light... some people would argue that exclude is much more clearer?

ratatatKE avatar Mar 19 '20 17:03 ratatatKE

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();

xlcrr avatar Dec 03 '20 11:12 xlcrr

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

Luieka224 avatar Jun 07 '21 11:06 Luieka224