diceline-chartmagnifierquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

Extracting closures to their own classes

Laravel has a lot of functions that accept a closure as a callback. But sometimes the callback function is just too large to be done inline - so our becomes very unreadable.

Let's take a look at an example of using such a function:

class MyScope
{
    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            function (Builder $query) {
                //really long function
            }
        );
    }
}

If you are used to writing Javascript you might first attempt something like this in order to refactor:

class MyScope
{

    public function callback(Builder $query)
    {
       //really long method
    }

    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            $this->callback
        );
    }
}

Here we extracted the callback to a method and we attempt to pass it as a parameter instead of writing the closure inline.

Unfortunately, this would not work and we would get the following error:

Undefined property: App\Scopes\MyScope::$callback

One valid approach would be to have our method return the function we need instead:

class MyScope
{

    public function callback()
    {
       return function (Builder $query) {
          //really long function
       }

    }

    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            $this->callback()
        );
    }
}

This would do just fine, but we can take this one step further by making use of a powerful PHP feature - invokable classes:

class InvokableClass
{
    //this method will automatically be called and
    //passed the query parameter when the time comes
    public function __invoke($query)
    {
        //really long method
    }
}

This approach is very beneficial because it also allows us to break up the long method into smaller, more understandable pieces (because an invokable class is still a class and we can take full advantage of it) so we are not just sweeping the unreadable code under the rug.

We can now use our freshly defined invokable class in the following way:

class MyScope
{
    public function apply(Builder $query, Model $model)
    {
        return $query->whereHas(
            'relationship',
            \Closure::fromCallable(new InvokableClass())
        );
    }
}

Please notice that we called Closure::fromCallable() first - this is necessary because we have to convert our class to a closure to avoid getting an error such as:

Argument 2 passed to Illuminate\Database\Eloquent\Builder::whereHas() must be an instance of Closure or null, instance of App\InvokableClass given