Laravel/Eloquent BelongsToMany and globalScopes

108 views Asked by At

In short - I would like to define global scope that would filter related instances by the value of the field in pivot table. I have defined the next relationship:

class Client extends Model {
    function promotions():BelongsToMany{
        return $this->belongsToMany(Promotion::class)
                    ->using(ClientPromotionPivot::class);
    }
}
class Promotion extends Model {
        // ...
}
class ClientPromotionPivotextends Pivot {
        protected $casts = [
                'active' => 'boolean'
        ];
}

My idea is that by default, everywhare where I access $client->promotions - only active ones would appear:

$active_promotions = $client->promotions;

BUT, sometimes I need to access them all:

$all_promotions = $client->promotions()
            ->withoutGlobalScope('active_promotions') // or something similar
            ->get();

Also I have many more similar relationships in many models where the Pivot table has the field active to be filtered on. The problem is it seems that global scopes doesn't know anything about the pivot table.

P.S. : Please do not offer creating two separated relations - like all_promotions and active_promotions - I would like to avoid this. Also I don't want to write something similar to $client->promotions()->wherePivot('active') everywhere I access promotions.

To me it looks like a missing feature in Eloquent/Laravel. I found this Issue published recently, but there is no solution to it: https://github.com/laravel/framework/issues/48617

What I tried:

  1. Defining two different relationships (This is my current solution, but I don't like it too much):
    function all_promotions():BelongsToMany{
        return $this->belongsToMany(Promotion::class)
                    ->using(ClientPromotionPivot::class);
    }
    function active_promotions():BelongsToMany{
        return $this->all_promotions->wherePivot('active');
    }
    
  2. Defining conditional relationship:
    function promotions($active=true):BelongsToMany{
        $relationship = $this->belongsToMany(Promotion::class)
                    ->using(ClientPromotionPivot::class);
        if ($active) $relationship->wherePivot('active');
        return $relationship;
    }
    
  3. Creating a global scope and passing it the relationship object:
    class ActivePivot implements Scope {
    
        public $belongs_to_many_query;
        // we need to pass the relationship to constructor to be able to use **qualifyPivotColumn** method
        public function __construct(BelongsToMany $belongs_to_many_query) {
            $this->belongs_to_many_query = $belongs_to_many_query;
        }
    
        public function apply(Builder $builder, Model $model) {
            return $builder->where(
                $this->belongs_to_many_query->qualifyPivotColumn('active'),
                '=',
                'true'
            );
        }
    }
    
    And then using it like so:
    class Client extends Model {
        function promotions():BelongsToMany{
            $relationship = $this->belongsToMany(Promotion::class)
                        ->using(ClientPromotionPivot::class);
            // Here I'm passing the relationship to the constructor of the global scope
            $relationship->withGlobalScope('active_pivot', new ActivePivot($relationship));
            return $relationship;
        }
    }
    
    And this way I can efectively do both:
    $active_promotions = $client->promotions;
    // and
    $all_promotions = $client->promotions()->withoutGlobalScope('active_pivot');
    

All three solutions work... but look ugly and not very flexible (imagine if i'd like to apply multiple global scopes/filters). Intuitively it seems to me that there should be a better approach to it.

0

There are 0 answers