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:

``` php
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:

``` php
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:

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

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

```php
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:

```php
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:

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

Denisa Halmaghi
15 Nov 2021
« Back to post