Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel: how to "disable" a global scope in order to include "inactive" objects into query?

I am having trouble with global scopes, especially the removal of the scope.

In my User model, i have a ActivatedUsersTrait, that introduces a global scope to only query for Users with the column "activated" set to true (The User is "activated" after email verification).

So far everything works fine, when i query for User::all(), i only get Users with activated=true.

My problem now is, how to include the non-activated Users into my query, like SoftDeletingTrait does via withTrashed()? This is only relevant in my ActivationController, where i need to get the User, set activated=true and save them back to db.

I've created a withInactive() method in my ActiveUsersTrait, based on the method i found in SoftDeletingTrait, but when i run a query on User::withInactive->get(), the non-activated Users won't show up in the results.

Here's my ActiveUsersTrait:

use PB\Scopes\ActiveUsersScope;

trait ActiveUsersTrait {

    public static function bootActiveUsersTrait()
    {
        static::addGlobalScope(new ActiveUsersScope);
    }

    public static function withInactive()
    {
        // dd(new static);
        return (new static)->newQueryWithoutScope(new ActiveUsersScope);
    }

    public function getActivatedColumn()
    {
        return 'activated';
    }

    public function getQualifiedActivatedColumn()
    {
        return $this->getTable().'.'.$this->getActivatedColumn();
    }

}

and my ActiveUsersScope:

use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;

class ActiveUsersScope implements ScopeInterface {

    public function apply(Builder $builder)
    {
        $model = $builder->getModel();

        $builder->where($model->getQualifiedActivatedColumn(), true);

    }

    public function remove(Builder $builder)
    {
        $column = $builder->getModel()->getQualifiedActivatedColumn();

        $query = $builder->getQuery();

        foreach ((array) $query->wheres as $key => $where)
        {
            if ($this->isActiveUsersConstraint($where, $column))
            {
                unset($query->wheres[$key]);

                $query->wheres = array_values($query->wheres);
            }
        }
    }

    protected function isActiveUsersConstraint(array $where, $column)
    {
        return $where['type'] == 'Basic' && $where['column'] == $column;
    }
}

Any help is highly appreciated!

Thanks in advance! -Joseph

like image 395
jsphpl Avatar asked Aug 14 '14 15:08

jsphpl


2 Answers

Eloquent queries now have a removeGlobalScopes() method.

See: https://laravel.com/docs/5.3/eloquent#query-scopes (under the Removing Global Scopes subheading).

From the docs:

// Remove one scope
User::withoutGlobalScope(AgeScope::class)->get();

// Remove all of the global scopes...
User::withoutGlobalScopes()->get();

// Remove some of the global scopes...
User::withoutGlobalScopes([
    FirstScope::class, SecondScope::class
])->get();
like image 159
voidstate Avatar answered Oct 15 '22 11:10

voidstate


The SoftDeletingTrait where cleanup is simpler because it doesn't involve any bindings (it's a "Null" where, not a "Basic" where). The issue you're encountering is that the binding for [ n => true ] is still there, even when you manually remove the where.

I'm thinking about making a PR because I encountered the same issue myself, and there isn't a great way to keep track of which wheres and which bindings go together.

If you are only using a simple query, you can keep track of the index of the binding more or less like so:

use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;

class ActiveUsersScope implements ScopeInterface {

    /**
     * The index in which we added a where clause
     * @var int
     */
    private $where_index;

    /**
     * The index in which we added a where binding
     * @var int
     */
    private $binding_index;

    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    public function apply(Builder $builder)
    {
        $model = $builder->getModel();

        $builder->where($model->getQualifiedActivatedColumn(), true);

        $this->where_index = count($query->wheres) - 1;

        $this->binding_index = count($query->getRawBindings()['where']) - 1;
    }

    /**
     * Remove the scope from the given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    public function remove(Builder $builder)
    {
        $query = $builder->getQuery();

        unset($query->wheres[$this->where_index]);
        $where_bindings = $query->getRawBindings()['where'];
        unset($where_bindings[$this->binding_index]);
        $query->setBindings(array_values($where_bindings));
        $query->wheres = array_values($query->wheres);
    }
}

Note how we're storing the indices where the where clause and bindings were added, rather than looping through and checking if we found the right one. This almost feels like a better design—we added the where clause and binding, so we should know where it is without having to loop through all where clauses. Of course, it will all go haywire if something else (like ::withTrashed) is also messing with the where array. Unfortunately, the where bindings and where clauses are just flat arrays, so we can't exactly listen on changes to them. A more object-oriented approach with better automatic management of the dependency between clauses and their binding(s) would be preferred.

Obviously this approach could benefit from some prettier code and validation that array keys exists, etc. But this should get you started. Since the global scopes aren't singletons (they get applied whenever newQuery() is invoked) this approach should be valid without that extra validation.

Hope this helps under the heading of "good enough for now"!

like image 31
DefiniteIntegral Avatar answered Oct 15 '22 12:10

DefiniteIntegral