Search code examples
phpsymfonydependency-injectionsymfony6php-8.2

symfony 6 dependency injection across parent -> child controllers


I have a base controller class that has some utility methods that all child controllers will use. As of now it has 3 dependencies, but might have more in the future. As a result, I now have a bit of a problem with, IMO, excessive DI instructions whenever I want to add a dependency to a child controller.

abstract class BaseController extends AbstractController
{
    public function __construct(
        protected readonly SerializerInterface $serializer,
        protected readonly ValidatorInterface  $validator,
        private readonly ResponseGenerator     $responseGenerator,
    ) {
    }
    ...

}

class ChildController extends BaseController
{
    // All the parent class injections are required in all child classes.
    public function __construct(
        SerializerInterface             $serializer,
        ValidatorInterface              $validator,
        ResponseGenerator               $responseGenerator,
        private readonly SomeRepository $someRepository,
        ... insert any other child controller-specific dependencies here.
    ) {
        parent::__construct($serializer, $validator, $responseGenerator);
    }
    ...
}

I tried using $this->container->get('serializer') in the base controller, but that doesn't work, because AbstractController::$container is defined by injection but has no constructor, so calling parent::__construct() is not possible. Besides, it wouldn't give me validator, so even if it did work, it would only solve part of the problem.

I tried looking for an attribute that I could use, e.g.

abstract class BaseController extends AbstractController
{
    #[Inject]
    protected readonly SerializerInterface $serializer;

    #[Inject]
    protected readonly ValidatorInterface $validator;

But didn't find anything like that (PHP-DI has it, but not Symfony).

Is there a way to somehow get rid of the duplicate dependencies in child controllers?


Solution

  • What you need is called service subscribers

    Controllers in Symfony when they extend AbstractController are service subscribers, which means they have a small container injected which contains few common services like twig, the serializer, the form builder, etc.

    If you want have some "common" services that your child controllers will use, you can extend the list by overriding the getSubscribedServices() in your parent controller. Or if your controller does not extends the default provided by Symfony, all you need to do is implement your own:

    If your controller is a service (which is already is I guess), Symfony will use setter injection to inject the container inside your controller.

    The code will look like this:

    <?php
    
    use Symfony\Contracts\Service\ServiceSubscriberInterface;
    
    
    class ParentController implement ServiceSubscriberInterface {
        protected ContainerInterface $container;
        public function setContainer(ContainerInterface) { $this->container = $container; } 
    
        public static function getSubscribedServices() {
             // This is static, so Symfony can "see" the needed services without instanciating the controller.
             // define some common services here, an example is inside the Symfony AbstractController
        }
    }
    
    class ChildController extends ParentController {
        // use custom DI for children.
    
        public function indexAction {
            // you can fetch services with $this->container->get(...)
        }
    
    }