@simonhamp This is exactly what I was looking for couple of weeks back.
I posted this problem on StackOverflow as well but didn't get any answer.
stackoverflow.com/questions/41654506/laravel-5-2-…
I'll give it a try.
Thanks a lot :)
I admit I was too lazy to read it all at 03.15am, but just before I dig in (and it will also be a reminder to read it tomorrow if I have a notification :)), what is your benefit from protected static over the regular public?
When you need to call this kind of shared protected methods from the same class boot method, for example to be reused in a global scope an error occurs like this :
Non-static method App\Product::withCalculatedPricingScope() should not be called statically
but you can avoid it with this useful trick
if ( ! function_exists('outer_scope')) {
class guestClass23c7d2389d6839b0dcd53a8230d04b35{};
/**
* Easily changes closure scope to an extraneous
*
* @param $closure
* @param null $thisPointer
* @param string $scope
* @return mixed
*/
function outer_scope($closure,$thisPointer=null,$scope=\guestClass23c7d2389d6839b0dcd53a8230d04b35::class){
return $closure->bindTo($thisPointer,$scope);
}
}
// Reuse as global scope
class Product {
// ....
protected static function boot()
{
parent::boot(); // TODO: Change the autogenerated stub
self::addGlobalScope(outer_scope(function($query){
Product::withCalculatedPricingScope($query);
}));
}
// ....
// Our actual scope, which will continue to work in exactly the same way as before
public scopeWithCalculatedPricing( $query, $include_tax = false ) {
$this->withCalculatedPricingScope( $query, $include_tax );
}
protected function withCalculatedPricingScope( $query, $include_tax = false ) {
$tax_multiplier = $include_tax ? 1.2 : 1;
$query->selectRaw( 'products.*, ( products.price * ' . $tax_multiplier . ' ) as total_price' )
->where( 'stock', '>', 0 );
}
}
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?
I've been using scopes inside with clauses etc for a very long time, I don't think this is needed. I believe it is not working for you simply because you are not returning the $query object from the scope, it should be:
public function scopeWithCalculatedPricing( $query, $include_tax = false ) { $tax_multiplier = $include_tax ? 1.2 : 1; return $query->selectRaw( 'products.*, ( products.price * ' . $tax_multiplier . ' ) as total_price' ) ->where( 'stock', '>', 0 ); }Please note the added return. If you can see if you refer to the laravel docs on the subject: https://laravel.com/docs/5.4/eloquent#local-scopes