Can you make a scope in laravel that calls various

2019-05-13 06:34发布

I have a model in Laravel that has various scopes defined. I want to use all of them in a lot of places so rather than chaining them together I'd rather just be able to call one scope that calls all of the other scopes like so:

function scopeValid($query, $user_id) {
    $query = $this->scopeDateValid($query);
    $query = $this->scopeMaxUsesValid($query);
    $query = $this->scopeCustomerMaxUsesValid($query, $user_id);
    return $query;
}

This doesn't seem to work though, is there a way to achieve this?

4条回答
Viruses.
2楼-- · 2019-05-13 07:17

Original answer

Query scopes are called statically.

$users = Model::dateValid()->get()

There is no $this when making static calls. Try replacing $this->scopeDateValid with self::scopeDateValid

Revised answer

There probably was something else wrong with your code since $this is in fact a Model instance when scopes are called. You should be able to either call the class scope methods directly with the $query parameter (like you did) or use another chain of scope method resolution as proposed by ceejayoz.

Personally, I don't see much of an advantage in going through the whole query scope resolution process when you know you want to call the scope methods on your class, but either way works.

Analysis

Let's walk through the call stack for executing query scopes:

#0 [internal function]: App\User->scopeValid(Object(Illuminate\Database\Eloquent\Builder))
#1 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php(829): call_user_func_array(Array, Array)
#2 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php(940): Illuminate\Database\Eloquent\Builder->callScope('scopeOff', Array)
#3 [internal function]: Illuminate\Database\Eloquent\Builder->__call('valid', Array)
#4 [internal function]: Illuminate\Database\Eloquent\Builder->valid()
#5 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(3482): call_user_func_array(Array, Array)
#6 [internal function]: Illuminate\Database\Eloquent\Model->__call('valid', Array)
#7 [internal function]: App\User->valid()
#8 /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php(3496): call_user_func_array(Array, Array)
#9 /app/Http/Controllers/UserController.php(22): Illuminate\Database\Eloquent\Model::__callStatic('valid', Array)
#10 /app/Http/Controllers/UserController.php(22): App\User::valid()

#10 The User::scopeValid() call

#8 __callStatic() handler for Model

From the PHP docs on Method overloading:

public static mixed __callStatic ( string $name , array $arguments )

__callStatic() is triggered when invoking inaccessible methods in a static context.

Annotated code of Model.php's __callStatic() method (lines 3492-3497):

public static function __callStatic($method, $parameters)
{
    // Uses PHP's late static binding to create a new instance of the
    // model class (User in this case)
    $instance = new static;

    // Call the $method (valid()) on $instance (empty User) with $parameters
    return call_user_func_array([$instance, $method], $parameters);
}

#7 User->valid() (which doesn't exist)

#5 __call handler for Model

Again, from the PHP docs on Method overloading:

public mixed __call ( string $name , array $arguments )

__call() is triggered when invoking inaccessible methods in an object context.

Annotated code of Model.php's __call() method (lines 3474-3483):

public function __call($method, $parameters)
{
    // increment() and decrement() methods are called on the Model
    // instance apparently. I don't know what they do.
    if (in_array($method, ['increment', 'decrement'])) {
        return call_user_func_array([$this, $method], $parameters);
    }

    // Create a new \Illuminate\Database\Eloquent\Builder query builder
    // initialized with this model (User)
    $query = $this->newQuery();

    // Call the $method (valid()) on $query with $parameters
    return call_user_func_array([$query, $method], $parameters);
}

#2 __call handler for the query Builder

Annotated code of Builder.php's __call() method (lines 933-946):

public function __call($method, $parameters)
{
    if (isset($this->macros[$method])) {
        // Handle query builder macros (I don't know about them)
        array_unshift($parameters, $this);

        return call_user_func_array($this->macros[$method], $parameters);
    } elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
        // Now we're getting somewhere! Builds the 'scopeValid' string from
        // the original 'valid()' method call. If that method exists on the
        // model, use it as a scope.
        return $this->callScope($scope, $parameters);
    }

    // Other stuff for fallback
    $result = call_user_func_array([$this->query, $method], $parameters);

    return in_array($method, $this->passthru) ? $result : $this;
}

#1 callScope() method of the query Builder

Annotated code of Builder.php's __call() method (lines 825-830):

protected function callScope($scope, $parameters)
{
    // Add $this (the query) as the first parameter
    array_unshift($parameters, $this);

    // Call the query $scope method (scopeValid) in the context of an
    // empty User model instance with the $parameters.
    return call_user_func_array([$this->model, $scope], $parameters) ?: $this;
}
查看更多
We Are One
3楼-- · 2019-05-13 07:17

It should work as long as you pass a valid Query object. Maybe typehinting in your function signature will tell you what's wrong? Edit: bernie caught it

Slightly off-topic, here is what I like to do to make my code even more readable :)

static function scopeValid($query, $user_id) {
    return $query->scopeDateValid()
                ->scopeMaxUsesValid()
                ->scopeCustomerMaxUsesValid($user_id);
}
查看更多
甜甜的少女心
4楼-- · 2019-05-13 07:19

Another solution for if, for example: You have to call a Date Range on multiple scopes

<?php 
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use DB;
class UserModel extends Model
{
    public function scopeCreatedBetween(Builder $query, \DateTime $start, \DateTime $end) : Builder
    {
        return $query->whereBetween('created_date',[$start->format('Y-m-d H:i:s'),$end->format('Y-m-d H:i:s')]);

    }

    public function scopeBilledBetween(Builder $query, \DateTime $start, \DateTime $end) : Builder
    {
        return $query->whereBetween('billed_date',[$start->format('Y-m-d H:i:s'),$end->format('Y-m-d H:i:s')]);
    }

    public function scopeMonthToDate(Builder $query, string ...$scopes) : Builder
    {
        return $this->applyDateRangeScopes(
            $query,
            $scopes,
            new \DateTime('first day of this month'),
            \DateTime::createFromFormat('Y-m-d',date('Y-m-d'))->sub(new \DateInterval('P1D'))
        );
    }

    /**
     * Applies the scopes used for our date ranges
     * @param  Builder $query 
     * @param  array  $scopes 
     * @return Builder
     */
    private function applyDateRangeScopes(Builder $query,array $scopes, \DateTime $from, \DateTime $to) : Builder
    {
        // If there are no scopes to apply just return the query
        if(!(bool)$scopes) return $query;
        // So we don't count each iteration
        $scopeCount = count((array)$scopes);

        for ($i=0; $i < $scopeCount; $i++) { 
            // Method does NOT exist
            if( !method_exists($this,$scopes[$i]) ) continue;
            // Apply the scope
            $query = $this->{$scopes[$i]}($query,$from,$to);
        }
        return $query;
    }

} 

Usage:

namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\UserModel;

class User extends Controller
{
    public function someAction(UserModel $user)
    {
        $user::scopeMonthToDate('scopeCreatedBetween','scopeCreatedBetween');
    }
}
查看更多
相关推荐>>
5楼-- · 2019-05-13 07:26

As shown in the docs, you'll want to do your scoping against $query, not $this, and use the scope's magic function rather than calling the internal implementation:

public function scopeTesting($query) {
    return $query->testingTwo();
}

public function scopeTestingTwo($query) {
    return $query->where('testing', true);
}

As a demonstration, you can see here calling the testing() scope applies the logic in the testingTwo() scope:

>>> App\User::testing()->toSql();
=> "select * from "users" where "testing" = ?"
>>> 

So, for your code, this should do the trick:

function scopeValid($query, $user_id) {
    $query = $query->dateValid();
    $query = $query->maxUsesValid();
    $query = $query->customerMaxUsesValid($user_id);

    return $query;

    // or just return $query->dateValid()
    //                      ->maxUsesValid()
    //                      ->customerMaxUsesValid($user_id);
}
查看更多
登录 后发表回答