Everything's (potentially) a callable
Published by Matthew Daly 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<?php23declare(strict_types=1);45namespace App\Jobs;67use Illuminate\Database\ConnectionInterface;8use Illuminate\Support\Benchmark;910final class Printer11{12 public function __invoke(string $value): void13 {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<?php23// 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<?php23$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<?php23declare(strict_types=1);45namespace App\Http\Controllers;67use App\Models\User;89final class FooController extends Controller10{11 /**12 * Return a view13 *14 * @return \Illuminate\Http\Response15 */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<?php23declare(strict_types=1);45namespace App\Http\Middleware;67use Illuminate\Http\Request;8use Illuminate\Http\Response;910final class CallableDemo11{12 public function __invoke(Request $request, $next): Response13 {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<?php23...4use App\Http\Middleware\CallableDemo;56...78Route::resource('foo', FooController::class)9->middleware(CallableDemo::class);
As does declaring it as global middleware in app\Http\Kernel.php
:
1<?php23namespace App\Http;4...56class Kernel extends HttpKernel7{8 protected $middleware = [9 ...10 \App\Http\Middleware\CallableDemo::class,11 ];
Or in the web
group:
1<?php23...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<?php2 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<?php23declare(strict_types=1);45namespace App\Http\Middleware;67use Illuminate\Database\ConnectionInterface;8use Illuminate\Http\Request;9use Illuminate\Http\Response;1011final class CallableDemo12{13 public function __construct(private ConnectionInterface $db)14 {15 }1617 public function __invoke(Request $request, $next): Response18 {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<?php23declare(strict_types=1);45namespace App\Jobs;67use Illuminate\Database\ConnectionInterface;8use Illuminate\Support\Benchmark;910final class ThingDoer11{12 public function __construct(private ConnectionInterface $db)13 {14 }1516 public function __invoke()17 {18 Benchmark::dd(fn() => $this->db->table('users')->get());19 }20}
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<?php23namespace App\Listeners;45use App\Events\ChirpCreated;6use App\Models\User;7use App\Notifications\NewChirp;8use Illuminate\Contracts\Queue\ShouldQueue;9use Illuminate\Queue\InteractsWithQueue;1011class SendChirpCreatedNotifications implements ShouldQueue12{13 /**14 * Create the event listener.15 *16 * @return void17 */18 public function __construct()19 {20 //21 }2223 /**24 * Handle the event.25 *26 * @param \App\Events\ChirpCreated $event27 * @return void28 */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<?php23namespace App\Listeners;45use App\Events\ChirpCreated;6use App\Models\User;7use App\Notifications\NewChirp;8use Illuminate\Contracts\Queue\ShouldQueue;9use Illuminate\Database\ConnectionInterface;10use Illuminate\Queue\InteractsWithQueue;1112class SendChirpCreatedNotifications implements ShouldQueue13{14 /**15 * Create the event listener.16 *17 * @return void18 */19 public function __construct(private ConnectionInterface $db)20 {21 }2223 /**24 * Handle the event.25 *26 * @param \App\Events\ChirpCreated $event27 * @return void28 */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<?php23namespace AppServiceProvider45use App\Jobs\ThingDoer;67final class FooController extends Controller8{9 public function __construct(private ThingDoer $thingDoer)10 {11 }1213 /**14 * Return a view15 *16 * @return \Illuminate\Http\Response17 */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<?php23final class FooController extends Controller4{5 ...6 /**7 * Return a view8 *9 * @return \Illuminate\Http\Response10 */11 public function __invoke()12 {13 $this->thingDoer->__invoke();14 return view('foo');15 }16}
Or you can use call_user_func()
:
1<?php23final class FooController extends Controller4{5 ...6 /**7 * Return a view8 *9 * @return \Illuminate\Http\Response10 */11 public function __invoke()12 {13 call_user_func($this->thingDoer);14 return view('foo');15 }16}
Or, my personal favourite approach:
1<?php23final class FooController extends Controller4{5 ...6 /**7 * Return a view8 *9 * @return \Illuminate\Http\Response10 */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<?php23final class FooController extends Controller4{5 ...6 /**7 * Return a view8 *9 * @return \Illuminate\Http\Response10 */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.