Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel negate local scope

i'm using laravel 5.2 for my project I just implemented a local scope (let's use laravel docs example for simplicity - https://laravel.com/docs/master/eloquent#local-scopes )

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

}

I call:

$users = App\User::popular()->get();

And it works like a charm

Is it possibile to call a negation of a scope?

$users = App\User::Notpopular()->get();

Or I have to manually implement the negation of the said scope? If so is there a better way to do it?

Thank you in advance!

like image 292
eldblz Avatar asked Jan 06 '16 23:01

eldblz


4 Answers

I am using this piece of code for "negating" scopes.

public function scopePublished($query, $negate = false) {
    return ($negate ? $query->where('published', false) : $query->where('published', true));
}

Now if I simply want published records I call $post->published()... I want all non-published records I call $post->published(true)...


I might to switch to following, because its simpler to understand:

public function scopePublished($query, $override = true) {
    return $query->where('published', $override);
}

published records: $post->published()...
unpublished records: $post->published(false)...

like image 103
Kyslik Avatar answered Nov 13 '22 03:11

Kyslik


It's not possible (technically it is, but it would require much more code) to create a local scope being negation of another local scope without implementing another scope method. It is however possible to reuse the negated criteria from one scope in another scope.

The following code should do the trick:

public function scopeNotPopular($query) {
  $query->whereNotIn($this->getKeyName(), function($q) {
    $q->select($this->getKeyName())->from($this->getTable());
    $this->scopePopular($q);
  });
}

This code uses whereNotIn constraint to apply negated constraint from another scope. It will allow you to reuse constraint logic instead of implementing original and negated versions in 2 different places. While this amount of code seems like quite an overhead with the example scope from the docs, it makes more sense with more complex constraints.

Keep in mind that while it allows code reuse, the SQL query that will be applied might be sub-optimal from performance point of view. It uses a subselect to identify rows that should be excluded from the result set.

like image 21
jedrzej.kurylo Avatar answered Nov 13 '22 03:11

jedrzej.kurylo


I think you need implement manually, because the Laravel won't "negate" automagically.

What you can do is use Dynamic Query Scopes: https://laravel.com/docs/5.0/eloquent#query-scopes

So, in Dynamic Query Scopes, you can pass additional parameters, and one of them could be if must get most popular people, or the less popular.

like image 1
Vitor Villar Avatar answered Nov 13 '22 02:11

Vitor Villar


Here is another approach that is similar to the accepted answer yet I feel can be better for performance since it doesn't use the subquery. In my use case the scope I was negating had multiple complex conditions which I was looking to avoid replicating.

    public function scopeNotPopular($query)
    {
        // whereNot doesn't exist in Laravel so doing similar logic here to avoid replicating popular logic
        //    whereRaw('TRUE') insures the "AND NOT" on the later WHERE will not be trimmed
        //    end result: WHERE true AND NOT ([popular logic])
        return $query->whereRaw('TRUE')
            ->where(function ($q) {
                $q->popular();
            }, null, null, 'AND NOT');
    }
like image 1
Zack Huston Avatar answered Nov 13 '22 02:11

Zack Huston