Everything's (potentially) a callable

Published by at 2nd October 2022 7:30 pm

I'm a big fan of callables in PHP in general. They're an extremely powerful way to build applications out of many different reusable components, in a way that combines the best aspects of object-oriented and functional programming.

If you're not familiar with the idea, consider this class:

1<?php
2
3declare(strict_types=1);
4
5namespace App\Jobs;
6
7use Illuminate\Database\ConnectionInterface;
8use Illuminate\Support\Benchmark;
9
10final class Printer
11{
12 public function __invoke(string $value): void
13 {
14 echo $value;
15 }
16}

Note the use of the __invoke() magic method. This gets called if you try to call the object as a function, eg:

1<?php
2
3// Will echo "Hello, World!"
4$printer = new Printer();
5$printer("Hello, World!");

Now, this may not seem like a terribly big deal, but it's only once you start putting them together that their real power becomes apparent. They're essentially "closures on steroids" in that they can do basically anything a closure can, and a lot of stuff they can't:

  • An invokable class passes the callable type hint, so you can generally use it in most of the same places you would a closure. As such, if you have a closure whose functionality needs to be reused elsewhere, it may make sense to refactor it into an invokable class.
  • Because they're classes, if they get large enough for it to be worthwhile, you can refactor part of the functionality into private methods. You can also pull in additional functionality through inheritance or traits if need be, but as this approach makes composition more achievable, you may find you don't often need to do so.
  • You can use constructor injection to pull in any dependencies the class has in a way you can't with closures.

As such, invokables let you chain functionality in the same way you would with closures, but with more capabilities and a higher level of abstraction. For instance, imagine you work on an e-commerce application, where each order is represented by an ORM model called Order, and the job to process an order is implemented in an invokable class called ProcessOrder, which accepts an instance of Order. Now, imagine a third party want to place multiple orders by sending a CSV file over FTP. Because you implemented ProcessOrder as an invokable, all you need to do is get the CSV data, generate an instance of Order from each row, and pass it into ProcessOrder, without changing a single existing line of code. You might do something like this:

1<?php
2
3$data = file_get_contents($remote_url);
4$result = Collection::make($data)
5 ->map(App::make(ConvertToOrder::class))
6 ->each(App::make(ProcessOrder::class));

All ConvertToOrder has to do is take the array data and create the model instance, then pass it on. Orders are processed consistently between those on the e-commerce site and those received via FTP, and the amount of new code required is minimal, limited to the ConvertToOrder class and whatever command class is being triggered (in this case, it might be an Artisan command). As long as you've picked an appropriate name for each class, it's really obvious how this pipeline works and what each step does, without the developer having to even open the class in question.

Using __invoke() as the defined method for doing something when the class only carries out a single action also makes sense as a general convention. If the class name, by itself, defines what the class is meant to do, then a method name is largely superfluous, and you end up making your method something like process(), execute() or handle(). Using __invoke() instead is both more concise and more consistent, because it allows you to use the shorter syntax, without the need to either choose or remember an actual method name.

Assuming you're working with a framework like Laravel, then as long as you're working on a class that's not one of the class types explicitly required by the framework, then you should be fine to use invokables however you like. For instance, if you have something like the ProcessOrder service class above, then its only real dependency on Laravel itself would be that it takes in an ORM instance, plus whatever is pulled in via constructor injection. However, classes generated by the framework itself are more dependent on a particular, predefined structure. That said, there are some parts of the framework that are amenable to using invokables.

Controllers

For a while now, Laravel has supported single action controllers, which implement a single __invoke() method, as in this example:

1<?php
2
3declare(strict_types=1);
4
5namespace App\Http\Controllers;
6
7use App\Models\User;
8
9final class FooController extends Controller
10{
11 /**
12 * Return a view
13 *
14 * @return \Illuminate\Http\Response
15 */
16 public function __invoke()
17 {
18 return view('foo');
19 }
20}

This has the following advantages:

  • Injecting dependencies into a controller's constructor adds to the time taken to process the request, and if you have a controller which handles multiple actions, then you may often be injecting dependencies for a route which aren't required for that route, which can have a noticeable performance impact. By breaking larger controllers down into single action controllers, you can prevent that from happening.
  • Setting up routing for the controller is simpler - it just needs to accept the class name, not the method name.

Using callables as controllers doesn't make sense for every use case. For instance, if you're building an API that exposes CRUD functionality for multiple resource types, it probably makes more sense to use a resource controller which defines all the actions for a given resource type, particularly if they're similar enough that you're extending a base resource controller with common functionality. But for any reasonably complex route, it may make sense to use a single action controller.

Middleware

Middleware is something else that only really has one public method and could therefore be implemented as an invokable, at least in theory. Some frameworks, such as Laminas, explicitly support it. The Laravel documentation doesn't appear to mention it, but after some experimentation I've discovered that it's possible to use callables as middleware in Laravel. For instance, take this callable middleware class I wrote:

1<?php
2
3declare(strict_types=1);
4
5namespace App\Http\Middleware;
6
7use Illuminate\Http\Request;
8use Illuminate\Http\Response;
9
10final class CallableDemo
11{
12 public function __invoke(Request $request, $next): Response
13 {
14 $response = $next($request);
15 $response->header('X-Clacks-Overhead', 'GNU Terry Pratchett');
16 return $response;
17 }
18}

This is a relatively simple middleware class which adds the X-Clacks-Overhead header to the response. At least in a recent version of Laravel 9, the following method of adding middleware in the router works:

1<?php
2
3...
4use App\Http\Middleware\CallableDemo;
5
6...
7
8Route::resource('foo', FooController::class)
9->middleware(CallableDemo::class);

As does declaring it as global middleware in app\Http\Kernel.php:

1<?php
2
3namespace App\Http;
4...
5
6class Kernel extends HttpKernel
7{
8 protected $middleware = [
9 ...
10 \App\Http\Middleware\CallableDemo::class,
11 ];

Or in the web group:

1<?php
2
3...
4 /**
5 * The application's route middleware groups.
6 *
7 * @var array<string, array<int, class-string|string>>
8 */
9 protected $middlewareGroups = [
10 'web' => [
11 ...
12 \App\Http\Middleware\CallableDemo::class,
13 ],

Or as route middleware:

1<?php
2 protected $routeMiddleware = [
3 ...
4 'gnu' => \App\Http\Middleware\CallableDemo::class,
5 ];

This example doesn't cover middleware that accepts dependencies from the container, however. So what if we amend our middleware class to accept a raw database connection as a constructor dependency and use that in the middleware body to add a header giving the total number of users, as in this example?

1<?php
2
3declare(strict_types=1);
4
5namespace App\Http\Middleware;
6
7use Illuminate\Database\ConnectionInterface;
8use Illuminate\Http\Request;
9use Illuminate\Http\Response;
10
11final class CallableDemo
12{
13 public function __construct(private ConnectionInterface $db)
14 {
15 }
16
17 public function __invoke(Request $request, $next): Response
18 {
19 $response = $next($request);
20 $response->header('X-Clacks-Overhead', 'GNU Terry Pratchett');
21 $response->header('X-Total-Users', $this->db->table('users')->count());
22 return $response;
23 }
24}

Yes, looks like this works fine too.

Queue jobs

Job classes are something else that do only one thing, and thus it potentially makes sense to use an invokable class for them. Consider this job class which uses the Benchmark helper to benchmark a query and dump the results to the screen:

1<?php
2
3declare(strict_types=1);
4
5namespace App\Jobs;
6
7use Illuminate\Database\ConnectionInterface;
8use Illuminate\Support\Benchmark;
9
10final class ThingDoer
11{
12 public function __construct(private ConnectionInterface $db)
13 {
14 }
15
16 public function __invoke()
17 {
18 Benchmark::dd(fn() => $this->db->table('users')->get());
19 }
20}

This example will work if you're using the sync queue connection type, but likely not with any others, because the intent is to run a query and dump it out. Obviously, that isn't really the main use case of job classes, but it's fine for demonstrating the principle of using invokable classes for tasks that do one thing only.

If we try the following in a route closure:

1Route::get('/', function () {
2 dispatch(ThingDoer::class);

We see the error get_class(): Argument #1 ($object) must be of type object, string given. So instead, we need to fetch ThingDoer from the container and pass it to dispatch():

1Route::get('/', function (ThingDoer $doer) {
2 dispatch($doer);

In a controller, it may well make more sense to do this via method injection.

Event listeners

Event listeners are yet another example of something that only really does one thing. This invokable listener is based on the one used to send notifications in Laravel Bootcamp:

1<?php
2
3namespace App\Listeners;
4
5use App\Events\ChirpCreated;
6use App\Models\User;
7use App\Notifications\NewChirp;
8use Illuminate\Contracts\Queue\ShouldQueue;
9use Illuminate\Queue\InteractsWithQueue;
10
11class SendChirpCreatedNotifications implements ShouldQueue
12{
13 /**
14 * Create the event listener.
15 *
16 * @return void
17 */
18 public function __construct()
19 {
20 //
21 }
22
23 /**
24 * Handle the event.
25 *
26 * @param \App\Events\ChirpCreated $event
27 * @return void
28 */
29 public function __invoke(ChirpCreated $event)
30 {
31 foreach (User::cursor() as $user) {
32 $user->notify(new NewChirp($event->chirp));
33 }
34 }
35}

This works in exactly the same way as a standard listener class. If you map it to an event class in the usual way, it works entirely as expected. However, as with some of the other examples, it's not clear if it supports constructor injection, so it's best to check. If we amend the listener as follows:

1<?php
2
3namespace App\Listeners;
4
5use App\Events\ChirpCreated;
6use App\Models\User;
7use App\Notifications\NewChirp;
8use Illuminate\Contracts\Queue\ShouldQueue;
9use Illuminate\Database\ConnectionInterface;
10use Illuminate\Queue\InteractsWithQueue;
11
12class SendChirpCreatedNotifications implements ShouldQueue
13{
14 /**
15 * Create the event listener.
16 *
17 * @return void
18 */
19 public function __construct(private ConnectionInterface $db)
20 {
21 }
22
23 /**
24 * Handle the event.
25 *
26 * @param \App\Events\ChirpCreated $event
27 * @return void
28 */
29 public function __invoke(ChirpCreated $event)
30 {
31 foreach ($this->db->table('users')->cursor() as $user) {
32 dd($user);
33 }
34 }
35}

Now, if we trigger the ChirpCreated event, it breaks at the right point and spits out the user data as expected, thus demonstrating that it works.

Limitations

There are some limitations of using invokable classes which you should bear in mind when deciding whether to make a class an invokable or not. For instance, accessing an invokable as as property of another class can be awkward - take this controller class:

1<?php
2
3namespace AppServiceProvider
4
5use App\Jobs\ThingDoer;
6
7final class FooController extends Controller
8{
9 public function __construct(private ThingDoer $thingDoer)
10 {
11 }
12
13 /**
14 * Return a view
15 *
16 * @return \Illuminate\Http\Response
17 */
18 public function __invoke()
19 {
20 // Call $this->thingDoer...
21 return view('foo');
22 }
23}

We can't call $this->thingDoer() because $this refers to the instance of FooController, and so we're referring to a non-existing method of FooController called thingDoer rather than the property $this->thingDoer. There are a couple of ways to do it. You can call __invoke() explicitly, which isn't very elegant:

1<?php
2
3final class FooController extends Controller
4{
5 ...
6 /**
7 * Return a view
8 *
9 * @return \Illuminate\Http\Response
10 */
11 public function __invoke()
12 {
13 $this->thingDoer->__invoke();
14 return view('foo');
15 }
16}

Or you can use call_user_func():

1<?php
2
3final class FooController extends Controller
4{
5 ...
6 /**
7 * Return a view
8 *
9 * @return \Illuminate\Http\Response
10 */
11 public function __invoke()
12 {
13 call_user_func($this->thingDoer);
14 return view('foo');
15 }
16}

Or, my personal favourite approach:

1<?php
2
3final class FooController extends Controller
4{
5 ...
6 /**
7 * Return a view
8 *
9 * @return \Illuminate\Http\Response
10 */
11 public function __invoke()
12 {
13 ($this->thingDoer)();
14 return view('foo');
15 }
16}

In the context of Laravel controllers, you also have the option to use method injection:

1<?php
2
3final class FooController extends Controller
4{
5 ...
6 /**
7 * Return a view
8 *
9 * @return \Illuminate\Http\Response
10 */
11 public function __invoke(ThingDoer $thingDoer)
12 {
13 $thingDoer();
14 return view('foo');
15 }
16}

Summary

Using invokable classes for any part of your application that does one thing only, and could potentially be reused, makes a lot of sense. It allows for more elegant code, giving you the advantages of functional programming without losing the benefits of OOP, and allows you to break your application down into a selection of easily reusable parts. While I've not been in a position to try it before, I also suspect that it makes decorating components simpler, to the point that certain tasks like logging and caching can be done with a single closure or invokable. Next time you write a class to do something, give serious thought to the idea of whether it should be an invokable - the answer may be "yes" more often than you think, and it'll often help make your code simpler and more reusable.