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

1000ml's avatar

How to pass additional Closure to query scopes?

Hey guys,

Let's say I have Products, a Category tree with categories and descendants, and a scope:

Product::ofCategoryAndDescendats(1)->get();

This scope gets all Products that are related to Category 1 and all its child categories. In some parts in my code I'd like to put constraints on those child categories.

Get all Products in Category with id 1 and in all categories that are child of Category with id 1, but category and its child must be "active". Passing a closure with all my constraints to the scope would be perfect, but I can't find documentation on how to apply that Closure to the query builder inside my scope:

Product::ofCategoryAndDescendants(1, function($query){
			$query->where('category_active',true);
})->get();


Product::scopeOfCategoryAndDescendants($query, $categoryId, Closure $closure){
			return $query->where('category_id',$categoryId)->->whereDescendants(????);
	
			// I must apply the $closure here in order to apply additional Wheres.
}

Right now I'm just duplicating all my scopes :( E.g.:

Product::ofCategoriesAndDescendants(1)->get();
Product::ofActiveCategoriesAndDescendants(1)->get();

Thank you for you help!

0 likes
6 replies
rodrigo.pedra's avatar

Use the ->when() method

public function scopeOfCategoryAndDescendants($builder, $categoryId, ?\Closure $constraints = null)
{
    return $builder
        ->where('category_id', $categoryId)
        ->when($constraints, fn ($query, $closure) => $query->where($closure));
}

The closure will then receive a query builder instance that they should use for adding constraints, for example:

Product::ofCategoryAndDescendants(1, function ($builder) {
    $builder->where('category_active', true);
});

reference: https://laravel.com/docs/9.x/queries#conditional-clauses

1 like
1000ml's avatar

@rodrigo.pedra Well, thank you. I'm just an idiot. I didn't think I could just write ->where($closure)

1 like
rodrigo.pedra's avatar

@1000ml no worries1 Sometimes we only think of something after we see it the first time. Have a nice day =)

1 like
kokoshneta's avatar
Level 27

Based on your use case alone, I would not pass the constraints to the existing scope – that’s polluting the functionality of the scope. The ofCategoryOrDescendants (as I would name it for clarity) scope should do just that: filter by records which are related to a specific category or its descendants.

If you want to further filter by active categories only, that’s a different scope – and not necessarily one that only needs to be used together with the first one. In your model, I would do this:

class Product extends Model {
	public function scopeOfActiveCategory($builder) {
		$builder->where('category_active', true);
	}

	public function scopeOfCategoryOrDescendants($builder, $categoryId) {
		$builder->where('category_id', $categoryId)
			->orWhereIn('category_id', function($builder, $categoryId) {
				$builder->select('id')
					->from('categories')
					->where('parent_id', $categoryId)
				;
			})
		;
	}
}

And then chain the scopes in your controller:

Product::ofCategoryOrDescendant(1)->ofActiveCategory()->get();

This makes the code easier to read in that each scope has a clear and understandable name, and anyone reading it can easily figure out that we’re fetching all products which belong to an active category which is either 1 or a descendant of it. It also has the advantage of making the scopes more reusable: if you want to fetch all products from all active categories, just do Product::ofActiveScope()->get(). You wouldn’t be able to do that if you’d lumped the two together into one scope.

1 like
1000ml's avatar

@kokoshneta Nice point, thank you.

So: I get products that belongs to a Category or its descendants

AND

I get products that belongs to an active Category.

This is cool. Thanks a lot.

kokoshneta's avatar

@1000ml If you’ve got the issue solved now, you can use the “Set Best Answer” button to mark the question as solved and get it off the Unsolved list.

Please or to participate in this conversation.