Jordan Crocker just wrote a post about single-use controllers, a very simple pattern designed to combat the clutter that occurs all too often in the controller layer of typical web applications. Rather than stuffing all actions that refer to the same type of resource into the same controller class, resulting in large and unwieldy classes, a single-use controller only has one single action, which (hopefully) keeps it small and easy to understand.
A stateless class with only one method naturally suggests using the
Function Object pattern, which makes it possible
to call the object itself as if it was a function. PHP supports this pattern by way of the
“magic method” __invoke()
,
but do the frameworks play along nicely?
Jordan demonstrates how to do it in Laravel, and he mentions in passing that he hasn’t tried to do the same in Symfony. Since I work with Symfony on a daily basis, I decided on a whim to give it a go.
The recommended way to declare routes in Symfony is via annotations. Using this method, turning
a controller into a function object is as simple as renaming the action method to __invoke
:
<?php
class HelloController
{
/** @Route("/hello/{name}", name="hello") */
public function __invoke($name)
{
return new Response("Hello $name!");
}
}
For those of us who prefer config files over annotations, it is also not hard to switch to single use controllers. Using YAML for example:
hello:
path: /hello/{name}
defaults: { _controller: app.controller.hello }
For this to work, the controller has to be defined as a service with the name app.
So what do I think? I like the idea of single-use controllers a lot, and I try to employ the pattern
whenever possible. The use of function objects to implement it strikes me more as a gimmick; cool looks aside,
there don’t seem to be any tangible benefits to it, and I’d worry about confusing less experienced
developers. Still, it’s a nice showcase of one of PHP’s lesser-known features – one that is, by the way,
fully supported by PhpStorm’s code assistance. I can’t recall ever using __invoke()
before, but I know
I’ll be looking out for opportunities to use it in the future.
Update: Turns out someone has already built a bundle around this pattern :)