phplaravellaravel-5

Can you make a scope that calls various other scopes?


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? This causes this problem:

Call to a member function where() on null

Solution

  • 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;
    }