-1

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?

jurchiks
  • 1,354
  • 4
  • 25
  • 55
  • Lookup setter/property injection in the docs. However base controllers generally end up being less than useful. Do all controllers really need all the functionality? Doubtful. An alternative approach is to move your base functionality into individual traits. Then each controller can just specify the traits it actually needs. – Cerad Aug 16 '23 at 16:17
  • @Cerad I did, and property injection seems to require the properties to be public, which this obviously doesn't need to be. Setter injection is kinda meh and also requires a public method. Traits won't work because they would require these dependencies but have no constructor. And yes, the majority of those methods will be used by all the controllers (they're used for consistent response format). Basically all I need is an `#[Inject]` attribute. – jurchiks Aug 16 '23 at 16:21
  • The constructor is usually a public method as well. – Cerad Aug 16 '23 at 16:54
  • @Cerad I don't think you understood what I'm trying to get rid of here. – jurchiks Aug 16 '23 at 17:00
  • Sounds like you should refactor those controllers. Usually, they should not contain that much logic. Also, you could inject the services to the actions instead – Nico Haase Aug 16 '23 at 17:43
  • @jurchiks Pretty sure I understand. You want to reduce constructor injection but reject the use of public methods/properties. Look up the way controllers use a service locator to inject it's dozen or so standard services. Easy enough to add your dependencies. – Cerad Aug 16 '23 at 18:05
  • Injecting the services in every action results in way more clutter, especially if I have a dependency that's necessary for every action. Bad solution. As for the other comment - the answer I got came way earlier and with actually useful code. – jurchiks Aug 16 '23 at 18:30

1 Answers1

1

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(...)
    }

}
Fabien Papet
  • 2,244
  • 2
  • 25
  • 52
  • Well, I guess it's better than nothing, although it's not the solution I wished for. `#[Inject]` would have been so much nicer... – jurchiks Aug 16 '23 at 18:27