I've been thinking about this problem for a few years and I'd like to propose another solution. (and perhaps my problem is not literally the same).
What if we used seperate invokable classes instead of query scopes on the Model class?
In this example:
class Bee extends Eloquent {
public function scopeAlive($query) {
$query->where('is_alive', true);
}
}
class Hive extends Eloquent {
}
// get all hives with live bees
Hive::query()->whereIn('hives.id', Bee::query()->alive()->pluck('hive_id')->all())->get()
// this works, but produces 2 SQL queries
// we must fulfill our unnatural desires to micro-optimize this prototype
// of a bee hive health tracking SASS
Hive::query()->whereIn('hives.id', function($query) {
$query->from('bees')
//->alive() // scope not found!!
// Bee::scopeAlive($query) // this one crashes, too!
->select('bees.id')
->where('bees.is_alive', true); // oh no, I had to copy and paste the contents of Bee->scopeAlive!
// a real world scenario that I actually care about would be when scopeAlive is a more complicated sub-operation
// but you can see that scopes are not composable or flexible
})->get()
// introducing the invokable class!
class BeesAlive () {
public function __invoke($query) {
$query->where('bees.is_alive', true);
}
}
// Now you can write
Bee::query()->where(new BeesAlive)->paginate();
Hive::query()->whereIn('hives.id', function($query) {
$query->from('bees')
->select('bees.hive_id')
->where(new BeesAlive);
})->get();
// Now to add fluff to it:
class BeesAlive () {
protected $foreignKey
public function __construct($foreignKey) {
$this->foreignKey = $foreignKey;
}
public function __invoke($query) {
if ($this->foreignKey) {
$query->from('bees')
->select($this->foreignKey);
}
$query->where('bees.is_alive', true);
}
}
// now we can do this:
Bee::query()->where(new BeesAlive)->paginate();
Hive::query()->whereIn('hives.id', new BeesAlive('bees.hive_id'))->get();
// woah! one query! that's at least 10 milliseconds!
// Now we can gold plate our controller:
// in controller:
Hive::query()->where(new HiveFilters($request->all()))->get()
// literally anywhere else
class HiveFilters {
protected $arr;
protected $foreignKey;
public function __construct(array $arr, $foreignKey) {
$this->arr = $arr;
$this->foreignKey = $foreignKey;
}
public function __invoke($query) {
if ($this->foreignKey) {
$query->from('hives')
->select($this->foreignKey);
}
foreach($this->arr as $key => $value) {
if ($key === 'bees' && is_array($value)) {
// pretend like we defined this class:
$query->whereIn('hives.id', new BeesFilter($value, $foreignKey = 'bees.hive_id'));
// if bees.is_alive was specified in the search for hives,
// we'll only get hives that have live bees
// not only that, but you can now use the word "composable"
// an inappropriate number of times to your lead dev
} else {
// more logic, map to other filters
}
}
}
}
Thoughts?