The usual way of defining a controller in a default Symfony installation is by placing it in the src/Controller
directory, but there might be situations where you just don’t want to do that. For instance, if you’re using some sort of domain-driven design and want to organize your project according to your domain needs rather than according to the default structure, you might want to place controllers in src/UI/Http/Web/Controller
just like in this excellent CQRS-Event sourcing boilerplate. Well, Symfony does not stop you from doing that in any way. In fact, any service in Symfony can be a controller, yet there are a few gotchas.
The default configuration
Let’s take a look at the default services configuration for a symfony/website-skeleton
project in config/services.yaml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
parameters: locale: 'en' services: # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: resource: '../src/*' exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class App\Controller\: resource: '../src/Controller' tags: ['controller.service_arguments'] |
As you can see, there is a special configuration for allowing you to fetch container services in controller actions, which allows you do have this:
1 2 3 4 5 6 7 8 9 |
use Psr\SimpleCache\CacheInterface; class Controller { public function index(CacheInterface $cache) { // do something } } |
If you place this in the default src/Controller
directory, this controller action will now have access to a PSR-16 cache service, even if it does not extend the base AbstractController
class. But what if, as we mentioned earlier, want to have the controller in a different directory? There are a few options to do this.
Hardcode the path in the config file
This is the easy one. Just replace ../src/Controller
with ../src/UI/Http/Web/Controller
(from the example above), and something like App\UI\Http\Web\Controller
to replace the configuration key. This is fine and works well, but it’s still hardcoded.
Extend the base controller class
Symfony will automatically detect any class which extends Symfony\Bundle\FrameworkBundle\Controller\AbstractController
, and assume (correctly) that the class is indeed a controller. Service injection in actions will therefore work fine.
As a side note, inheriting from the default AbstractController
also means that you can delete from the configuration file the special definition for controllers, as recognizing the abstract class is something that Symfony does automatically without any extra configuration needed.
Do away with hardcoded directories and base classes
If you’re like me, both solutions above are not completely satisfactory:
- The first one means that you have to store controllers in that one directory, but your domain directory structure might dictate that you place controllers in different subdomain folders.
- The second one means that you’re forced to use a rather bloated base class. Often all you want is the shortcut for
$this->render()
, but the buy-in for that is pretty huge.
Lately I’ve been experimenting with a solution which allows me to remove the need for a base class, and still have automatic controller detection regardless of where I store the classes. The trick behind this is that Symfony does not really need a base class to detect controllers, it just needs some sort of marker. And what’s better than an interface for this?
Step 1, let’s create an empty marker interface. In this example I use the App
namespace, but it can be stored anywhere:
1 2 3 4 5 |
namespace App; interface ControllerInterface { } |
Step 2, have your controller implement that interface:
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace App\Wherever\You\Want\To\Store\Your\Controller; use App\ControllerInterface; use Twig\Environment; class MyController implements ControllerInterface { public function index(Environment $twig) { // do something } } |
Step 3, add this to your services definition:
1 2 3 4 5 |
services: # ... _instanceof: App\ControllerInterface: tags: ['controller.service_arguments'] |
This will tell Symfony that any class implementing this interface should be treated as a controller, which means you can easily request services in your actions, and have super lightweight controllers that can be unit tested with incredible ease!