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

bionary's avatar

Job Queues and Query Scopes

I have a fairly mature Laravel app that helps me manage my business. Recently I refactored the codebase to allow for multiple: projects and users.

	//models/Project
	public function users(){
		return $this->belongsToMany(User::Class);//pivot table relationship
	}
	//models/User
	public function projects(){
		return $this->belongsToMany(Project::Class);//pivot table relationship
	}

As such I placed a project_id field on most of my tables with the applicable foreign index. I then applied the following scope to each of those tables...

class ProjectScope implements Scope
{
	/**
	 * Apply the scope to a given Eloquent query builder.
	 *
	 * @param  \Illuminate\Database\Eloquent\Builder  $builder
	 * @param  \Illuminate\Database\Eloquent\Model  $model
	 * @return void
	 */
	public function apply(Builder $builder, Model $model)
	{
		$builder
			->where($model->getTable().'.project_id', 
			auth()->user()->settings['project_id'] 
);
	}
}

Things worked great when performing operations as a logged in user.

Once running job queues the scoping gets all messed up as expected because a queued job has no awareness of: auth()->user()->settings['project_id']

I'm not sure what is the best way to scope queries to each project_id.

The only thing I can think of was to make a new table that would keep track of the current project_id "in focus" and to pass that id in with each job. The query scope could check the DB field if the project_id === null. Even as I type this it sounds like a horrible solution.

I'm sure there is some smart Laravel way to handle this...

What would you do to ensure all queued jobs stay scoped to each project?

Thanks.

0 likes
6 replies
kevinbui's avatar

What about adding an optional parameter for the query scope:

public function apply(Builder $builder, Model $model, Project $project = null)
{
		$builder
		    ->where($model->getTable().'.project_id', 
		    $project?->id  ??  auth()->user()->settings['project_id'] 
        );
}

Then you can inject the project ids to your jobs.

1 like
bionary's avatar

@kevinbui I looked at the docs and the interface for Scopes. I don't think parameters other than $builder, $model are allowed.

interface: public function apply(Builder $builder, Model $model);

kevinbui's avatar

@bionary Hmm, according to this thread and this thread, it is not possible to add parameters to a global scope.

Maybe creating trait that contains a local scope and apply to all your models:

trait ProjectScope {
    public function scopeProject($query, $projectId = null)
    {
        $builder
		    ->where($this->getTable().'.project_id',
		        $project?->id  ??  auth()->user()->settings['project_id']
        );
    }
}
bionary's avatar

Thank you for your suggestion @kevinbui, I suppose I could do that but considering the size of the codebase that is fairly painful. I would have to manually apply that local query scope to each and every model. I have ~40+ tables and a couple years of code! I'm trying to avoid having to put Mymodel::project()->where(...)... in hundreds of places.

I would have guessed that my dilemma is not an anomaly, I'm surprised this is not more of a common problem. I mean project_ids, queue workers? seems like it should be a common situation for any standard SAAS app.

I'm surprised there is not some nifty Laravel way to manage this.

kevinbui's avatar

@bionary I understand. This workaround might work for you, though I don't really like it. What about passing the authenticated user to the job and then temporarily authenticate him in the job:

YourJob::dispatch(auth()->user());

class YourJob implements ShouldQueue
{
    public $user;

    function __construct(User $user)
    {
        $this->user = $user;
    }

    public function handle()
    {
        Auth::login($user);

        // Access the query scope.

        Auth::logout();
    }
}
1 like
Jaan's avatar

@bionary Hi,

I know I am a year late but I had the same problem and found a work around that worked for me and might work for you.

if ( array_walk($builder->getQuery()->wheres, function ($where, $key) use ($model) { if (str_contains($where['column'], $model->getTable()) && $where['operator'] === '=') { return true; } }) ) { return; }

The wheres property on the builder instance seems to be all the where clauses being applied.

So what is being done here is it's checking to see if the builder instance's query is looking for a specific model. It's checking the database table name concated with '.id' with the query string & seeing if the builder is looking for a specific instance of a model.

1 like

Please or to participate in this conversation.