Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony 4 Service Dependency Injection - Constructor vs Method

So I've got a Symfony controller, and I'm injecting needed services into my methods via params.

One of the params (MySqlGroupDAO $groupDAO) is used by all methods of this controller class.

Currently, I'm passing the 'common' param as my last param in each method like this:

/**
 * @Route("/{id}", methods={"POST"})
 * @IsGranted("EDIT_GROUP", subject="parentGroup")
 */
public function addGroup(Request $request, MySqlGroupDAO $groupDAO)  {
    $group = new Group();
    //code to init group from request

    $groupDAO->addGroup($group);

    return new Response("Adding $groupName");
}

Doing it this way allows me to eliminate my __construct method. However, I'm not sure this is the best way to go about it. Since it's common across all methods would it be better to re-add my constructor and do something like this?:

private $groupDAO;

public function __construct(
    Config $config,
    ValidatorInterface $validator,
    TranslatorInterface $translator,
    RequestStack $requestStack
) {
    parent::__construct($config, $validator, $translator, $requestStack);
    $this->groupDAO = new MySqlGroupDAO($config);
}


/**
 * @Route("/{id}", methods={"POST"})
 * @IsGranted("EDIT_GROUP", subject="parentGroup")
 */
public function addGroup(Request $request)  {
    $group = new Group();
    //code to init group from request

    $this->groupDAO->addGroup($group);

    return new Response("Adding $groupName");
}

In doing so, I'm eliminating about half a dozen params across all my methods (in this particular class). But I'm adding back in my constructor, which requires me to add a class param, and inject several additional params in my constructor since it extends another class.

Is there an advantage to doing it one way vs the other?

Thanks.

like image 986
mcmurphy Avatar asked Oct 10 '18 02:10

mcmurphy


People also ask

Which dependency injection method is better constructor based or setter based?

If we use both constructor and setter injection, IOC container will use the setter injection. Changes: We can easily change the value by setter injection. It doesn't create a new bean instance always like constructor. So setter injection is flexible than constructor injection.

Why constructor based dependency injection is better?

Constructor injection helps in creating immutable objects because a constructor's signature is the only possible way to create objects. Once we create a bean, we cannot alter its dependencies anymore.

Which type of dependency injection is better?

Dependencies passed into the constructor should be useful to the class in a general way, with its use spanning multiple methods in the class. If a dependency is used in only one spot, method injection (covered below) might be a better choice.

Is it difficult to inject dependency by constructor?

Frameworks that apply the Constrained Construction anti-pattern can make using Constructor Injection difficult. The main disadvantage to Constructor Injection is that if the class you're building is called by your current application framework, you might need to customize that framework to support it.


1 Answers

Reasons to use DI in a controller's route methods:

  • Less susceptible to changes of the parent::__construct method. Using DI on the constructor means you have to adapt your code whenever this changes. Also notice that some Symfony bundles may assume that controllers have a particular signature and it might make things more complicated for you if it does not.
  • If at least one of the routes does not use the service, by using these fine-grained DIs we avoid instantiating a service when this is not necessary (which can be costly if it has its own DIs that weren't already used somewhere else). This can be mostly offset by using lazy services though.

Reasons to use DI in the contructor:

  • In services other than controllers (and in methods other than route methods of controllers, if any), you can't use autowiring. If you want to inject your dependencies using a method's argument, you'll have to manually pass that dependency with each call. This means in turn that whichever service calls that method should itself have a DI on the required service. The problem is therefore shifted, but it can't be shifted infinitely that way, and at some point you're going to want to use some autowiring on a parent.

Alternative to using DI in the constructor:

You can also use setter injection and configure your service this way. This is functionally pretty similar to using DI in the constructor, but it bypasses the major drawback of generating a different signature from the parent and being more work to maintain if the parent constructor changes.

You can also make this easier to use for services you often inject. Make the services that need this DI implement an interface and configure them with _instanceof. Symfony does that with its ContainerAwareInterface and even has a facilitator in the form of ContainerAwareTrait to declare the setter and the property. In your case, if several services require the MySqlGroupDAO service, you could define a MySqlGroupDAOAwareTrait and MySqlGroupDAOAwareInterface, add a MySqlGroupDAOAwareInterface entry in your services.yaml's _instanceof section, and use the trait and implement the interface in services that need the DI.


Edit Nov. 2021:

This answer is still being read by new people, so I thought I'd complete it with something that was added with Symfony 5.2 (and it requires PHP 8): using PHP attributes for dependency injection.

This allows Setter Injection without touching services.yaml, and it allows public property injection. Here's the doc's example for both of these:

use Symfony\Contracts\Service\Attribute\Required;

class SomeService
{
    // Example of public property injection
    #[Required]
    public Bar $bar;

    // Example of setter injection without editing services.yaml
    #[Required]
    public function setFoo(Foo $foo): void
    {
        // ...
    }
}

Notice that the property and the setter method used above need to be public (which might or might not be okay with you).

Read more at https://symfony.com/blog/new-in-symfony-5-2-php-8-attributes.

like image 110
NicolasB Avatar answered Sep 30 '22 10:09

NicolasB