Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Kryptonit3's avatar

Need help writing remove() for global query scope

Well, this was more difficult than I thought.

Here is my scope, the apply works wonderfully.

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

class JobStatusScope implements ScopeInterface
{

    /**
     * Apply scope on the query.
     *
     * @param \Illuminate\Database\Eloquent\Builder  $builder
     * @param \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder
            ->where('active', true)
            ->whereHas('user', function($q) {
                $q->where('account_type', 'company')
                    ->whereNested(function($r) {
                      $r->where('stripe_active', true)
                        ->orWhereNotNull('subscription_ends_at')
                        ->where('subscription_ends_at', '>', Carbon::now())
                        ->orWhereNotNull('trial_ends_at')
                        ->where('trial_ends_at', '>', Carbon::today());
                });
            });
    }

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

        //dd($query->wheres);

        $columns = ['stripe_active', 'subscription_ends_at', 'active', 'trial_ends_at'];

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

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

}

What I tried in my remove() method didn't work.

I have this in a trait file, and attached to my Model

use App\Cable\Traits\Scopes\JobStatusScope;

trait JobStatusTrait {

    public static function bootJobStatusTrait()
    {
        static::addGlobalScope(new JobStatusScope);
    }

    /**
     * Get the query builder without the scope applied.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public static function withAll()
    {
        return with(new static)->newQueryWithoutScope(new JobStatusScope);
    }
}

When I try to run MyModel::withAll()->get() it still returns the same filtered data.

When I dd($query->wheres) in the remove() and call the ->withAll() I get this

array:3 [▼
  0 => array:3 [▼
    "type" => "Null"
    "column" => "jobs.deleted_at"
    "boolean" => "and"
  ]
  1 => array:5 [▼
    "type" => "Basic"
    "column" => "active"
    "operator" => "="
    "value" => true
    "boolean" => "and"
  ]
  2 => array:5 [▼
    "type" => "Basic"
    "column" => Expression {#478 ▼
      #value: "(select count(*) from `users` where `jobs`.`user_id` = `users`.`id` and `account_type` = ? and (`stripe_active` = ? or `subscription_ends_at` is not null and `subscription_ends_at` > ? or `trial_ends_at` is not null and `trial_ends_at` > ?) and `users`.`deleted_at` is null)"
    }
    "operator" => ">="
    "value" => Expression {#476 ▶}
    "boolean" => "and"
  ]
]
0 likes
10 replies
davidwebca's avatar

It is probably because of the bindings. You removed the wheres correctly, but if they have bindings, they are going to screw up the query.

Here is my "Approved" Trait / Global Scope with some particularities. Maybe it'll help you find a solution.

<?php
trait ApprovedTrait {
    public $approved_column = 'approved';

    /**
     * Boot the soft deleting trait for a model.
     *
     * @return void
     */
    public static function bootApprovedTrait()
    {
        $scope = new ApprovedScope;
        static::addGlobalScope($scope);

        /**
         * Need to look for deleting events because of conflict
         * with the SoftDeleting Trait which prevents the use of
         * $instance->delete() when element is not approved
         */
        self::deleting(function($instance) use($scope) {
            if($instance->hasSoftDeletes()){
                if($instance->forceDeleting){
                    $instance->withTrashed()->withNotApproved()->where($instance->getKeyName(), $instance->getKey())->forceDelete();
                }else{
                    $instance->deleted_at = $instance->freshTimestamp();
                    $instance->save();
                }
            }
        });
    }

    public function hasSoftDeletes(){
        $traits = class_uses($this);
        return in_array('SoftDeletingTrait', $traits) || in_array('Illuminate\Database\Eloquent\SoftDeletingTrait', $traits);
    }

    public function approve(){
        $this->approved = true;
        $this->save();
    }

    public function disapprove(){
        $this->approved = false;
        $this->save();
    }

    public static function withNotApproved(){
        $instance = new static;
        $builder = $instance->newQueryWithoutScope(new ApprovedScope);
        return $builder;
    }

    public static function notApproved(){
        $instance = new static;
        $builder = $instance->newQueryWithoutScope(new ApprovedScope);
        $query = $builder->getQuery();
        $query->where($instance->getQualifiedApprovedColumn(), '=', false);
        return $builder;
    }

    /**
     * Get the name of the "approved" column.
     *
     * @return string
     */
    public function getApprovedColumn()
    {
        return defined('static::APPROVED') ? static::APPROVED : 'approved';
    }

    /**
     * Get the fully qualified "approved" column.
     *
     * @return string
     */
    public function getQualifiedApprovedColumn()
    {
        return $this->getTable().'.'.$this->getApprovedColumn();
    }

}
<?php

class ApprovedScope implements \Illuminate\Database\Eloquent\ScopeInterface{
    protected $extensions = ['Approved', 'NotApproved', 'WithNotApproved'];

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

        $builder->where($model->getQualifiedApprovedColumn(), '=', true);

        $this->extend($builder);
    }

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

        $query = $builder->getQuery();
        $bindings = $query->getBindings();

        $bindingKey = 0;

        foreach ((array) $query->wheres as $key => $where)
        {
            if ($this->isApprovedConstraint($where, $column))
            {
                $this->removeWhere($query, $key);
 
                // 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->removeBinding($query, $bindingKey);
            }
 
            // Check if where is either NULL or NOT NULL type,
            // if that's the case, don't increment the key
            // since there is no binding for these types
            if ( ! in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++;
        }
    }

    /**
     * Remove scope constraint from the query.
     * 
     * @param  \Illuminate\Database\Query\Builder  $builder
     * @param  int  $key
     * @return void
     */
    protected function removeWhere(\Illuminate\Database\Query\Builder &$query, $key)
    {
        unset($query->wheres[$key]);

        $query->wheres = count($query->wheres)>0?array_values($query->wheres):$query->wheres;
    }
 
    /**
     * Remove scope constraint from the query.
     * 
     * @param  \Illuminate\Database\Query\Builder  $builder
     * @param  int  $key
     * @return void
     */
    protected function removeBinding(\Illuminate\Database\Query\Builder &$query, $key)
    {
        $bindings = $query->getRawBindings()['where'];
 
        unset($bindings[$key]);
 
        $query->setBindings($bindings);
    }

    /**
     * Extend the query builder with the needed functions.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    public function extend(\Illuminate\Database\Eloquent\Builder $builder)
    {
        foreach ($this->extensions as $extension)
        {
            $this->{"add{$extension}"}($builder);
        }
    }

    /**
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addWithNotApproved(\Illuminate\Database\Eloquent\Builder $builder)
    {
        $builder->macro('withNotApproved', function(\Illuminate\Database\Eloquent\Builder $builder)
        {
            $this->remove($builder);

            return $builder;
        });
    }

    /**
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addApproved(\Illuminate\Database\Eloquent\Builder $builder)
    {
        $builder->macro('approved', function(\Illuminate\Database\Eloquent\Builder $builder)
        {
            $this->apply($builder);

            return $builder;
        });
    }

    /**
     * Add the only-trashed extension to the builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return void
     */
    protected function addNotApproved(\Illuminate\Database\Eloquent\Builder $builder)
    {
        $builder->macro('notApproved', function(\Illuminate\Database\Eloquent\Builder $builder)
        {
            $this->remove($builder);

            $builder->getQuery()->where($builder->getModel()->getQualifiedApprovedColumn(), '=', false);

            return $builder;
        });
    }

    public function isApprovedConstraint($where, $column){
        return ($where['type'] == 'Basic' && $where['column'] == $column && $where['value'] == true);
    }
}
1 like
Kryptonit3's avatar

@david_treblig my issue is I need to reference a related table with whereHas. My bindings will NEVER change. How would I go about removing them properly with my example? Thanks for your example, I am sure it will help in the future when I am not trying to reference a related table.

I really wish this feature (global query scopes) was more "Eloquent". Like calling return with(new static)->newQueryWithoutScope(new JobStatusScope); just removed the apply() function completely instead of making us have to break down the query manually.

Kryptonit3's avatar

for some reason my remove() function needs to look like this to work

    public function remove(Builder $builder, Model $model)
    {
        $query = $builder->getQuery();

        foreach ((array) $query->wheres as $key => $where)
        {
            if ($where['column'] instanceof \Illuminate\Database\Query\Expression)
            {
                unset($query->wheres[$key]);

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

            }
        }
        foreach ((array) $query->wheres as $key => $where)
        {
            if ($where['column'] == 'active')
            {
                unset($query->wheres[$key]);

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

            }
        }
        $builder->setBindings([]);
    }

It is working now when I call withAll(). Strange.

I hope that @TaylorOtwell makes this feature (global query scope) more "Eloquent" in the future.

pmall's avatar

You mentioned in the other thread that your scope must be applied to every request. Why do you ever want to remove it ?

In fact I never understood why we could remove a global scope. It is by definition global so applied on every query. If not, why not just creating a regular scope ?

Kryptonit3's avatar

@pmall - My site allows companies to post job listings. The company has a stripe subscription. My company is a User. a User->hasMany('Job'). Their listings will show automatically across the site as long as their stripe subscription is active (hence the global scope). But even when their subscription expires, I still want them, or me (site staff for that matter) to be able to see/manage the jobs in their/our dashboard even though the jobs will not be visible on the front end of the site. The moment their subscription lapses the jobs no longer get displayed with $job->all(). But I still need to be able to manage them with $job->withAll()->get();. Hope that makes sense.

This global scope on the Job model applies the same logic to the job listing owners as doing a $user->subscribed() check as the laravel/cashier package does to see if the user is still subscribed.

pmall's avatar
pmall
Best Answer
Level 56

But even when their subscription expires, I still want them, or me (site staff for that matter) to be able to see/manage the jobs in their/our dashboard even though the jobs will not be visible on the front end of the site.

So this is not a global scope. It is just a regular scope.

1 like

Please or to participate in this conversation.