Lightweight controllers everywhere in Symfony

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:

# 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

usePsr\SimpleCache\CacheInterface;

classController

{

publicfunctionindex(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

namespaceApp;

interfaceControllerInterface

{

}

Step 2, have your controller implement that interface:

1

2

3

4

5

6

7

8

9

10

11

12

namespaceApp\Wherever\You\Want\To\Store\Your\Controller;

useApp\ControllerInterface;

useTwig\Environment;

classMyControllerimplementsControllerInterface

{

publicfunctionindex(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!