Laravel:如何“禁用”;全局作用域,以便包含“非活动的”;对象进行查询


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

我在使用全局作用域时遇到了麻烦,特别是在删除作用域时。

在我的User模型中,我有一个ActivatedUsersTrait,它引入了一个全局作用域,只查询列"activated"设置为true的用户(用户在电子邮件验证后被"激活")。

到目前为止一切都很好,当我查询User::all()时,我只得到激活=true的用户。

我现在的问题是,如何包括非激活的用户到我的查询,像SoftDeletingTrait通过withTrashed()?这只与我的ActivationController相关,我需要获得用户,设置activated=true并将它们保存回db。

我在我的ActiveUsersTrait中创建了一个withInactive()方法,基于我在SoftDeletingTrait中发现的方法,但是当我在User::withInactive->get()上运行查询时,未激活的用户不会出现在结果中。

这是我的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();
    }
}

和我的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;
    }
}

任何帮助都非常感谢!

提前感谢!约瑟夫。

雄辩查询现在有一个removeGlobalScopes()方法。

参见:https://laravel.com/docs/5.3/eloquent#query-scopes(在移除全局作用域的副标题下)。

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

SoftDeletingTrait的清理更简单,因为它不涉及任何绑定(它是一个"Null"的地方,而不是一个"Basic"的地方)。您遇到的问题是,[n => true]的绑定仍然存在,即使您手动删除了where.

我正在考虑做一个PR,因为我自己也遇到了同样的问题,并且没有一个很好的方法来跟踪哪个位置和哪个绑定在一起。

如果您只使用一个简单的查询,您可以或多或少地像这样跟踪绑定的索引:

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

注意我们是如何将索引存储在添加了where子句和绑定的地方,而不是遍历并检查是否找到了正确的索引。这似乎是一种更好的设计——我们添加了where子句和绑定,所以我们应该知道它在哪里,而不必遍历所有where子句。当然,如果其他东西(比如::withTrashed)也扰乱了where数组,它就会变得混乱。不幸的是,where绑定和where子句只是平面数组,因此我们不能准确地侦听它们的变化。更倾向于采用一种面向对象的方法,更好地自动管理子句及其绑定之间的依赖关系。

显然,这种方法可以受益于一些更漂亮的代码和数组键存在的验证等。但这应该让你开始。由于全局作用域不是单例的(它们在newQuery()被调用时被应用),这种方法应该是有效的,而不需要额外的验证。

希望在"现在足够好"的标题下有所帮助!

在遇到同样的问题后发现了这个,我有一个更有说服力的解决方案。只需将你的"remove"方法替换为:

/**
 * 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();
    $column = $builder->getModel()->getQualifiedActivatedColumn();
    foreach ((array) $query->wheres as $key => $where)
    {
        if ($this->isActiveUsersConstraint($where, $column))
        {
            // Here SoftDeletingScope simply removes the where
            // but since we use Basic where (not Null type)
            // we need to get rid of the binding as well
            $this->removeWhere($query, $key);
            $this->removeBinding($query, $key);
        }
    }
}
/**
 * Remove scope constraint from the query.
 *
 * @param  'Illuminate'Database'Eloquent'Builder  $builder
 * @param  int  $key
 * @return void
 */
protected function removeWhere($query, $key)
{
    unset($query->wheres[$key]);
    $query->wheres = array_values($query->wheres);
}
/**
 * Remove scope constraint from the query.
 *
 * @param  'Illuminate'Database'Eloquent'Builder  $builder
 * @param  int  $key
 * @return void
 */
protected function removeBinding($query, $key)
{
    $bindings = $query->getRawBindings()['where'];
    unset($bindings[$key]);
    $query->setBindings(array_values($bindings));
}

看看newqueue函数(Eloquent/Model.php Lvl 4.2):

newQuery{
//'initialized' 
$b = $this->newQueryWithoutScopes();  //Get a 'clean' query. Note the plural.  
//and applies scopes
return $this->applyGlobalScopes($b);  //now builder is 'dirty'
}

所以,这建议了一个解决方案:

function someButNotAllScopes(){
$b = $this->newQueryWithoutScopes();
$unwanted = new MyUnwantedScope();
//get all scopes, but skip the one(s) you dont want
foreach($this->getGlobalScopes as $s){
 if ($s instanceof $unwanted){continue;}
  $s->apply($b, $this)
}
return $b;
}

你也可以用作用域做一些聪明的事情。用'applyMe'方法实现OnOffInterface。这个方法可以"打开"/关闭作用域的apply方法。在上面的函数中,您可以获得不需要的作用域并将其'关闭':

$scope = $this->getGlobalScope(new Unwanted());
$scope->applyme(false); //will turn off the apply method
return $this->newQuery();  //now each scope checks if it is 'off'

简单地说,您可以像这样应用不需要绑定:

$builder->where($column, new 'Illuminate'Database'Query'Expression(1));

$builder->where($column, 'DB::raw(1));