Dependency Injection inside a service utility class using php-di

493 Views Asked by At

In my Slim v4, application, I am setting up the container definitions as below.

$containerBuilder = new ContainerBuilder();
$containerBuilder->useAttributes(true);

$containerBuilder->addDefinitions([
    MailerInterface::class => function (ContainerInterface $container) {
        $dsn = sprintf(
            '%s://%s:%s@%s:%s',
            "smtp",
            "username",
            "password",
            "hostname",
            "587"
        );

        return new Mailer(Transport::fromDsn($dsn));
    }
]);

I can use the MailerInterface in my controllers without any issue.

<?php
abstract class BaseController
{
    protected MailerInterface $mailer;

    /* DI works in constructor as part of the controller */
    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }
}

This works perfectly fine as part of the controller. But I am looking for creating a service which can be used anywhere in the application and not only in the controllers.

My intended usage of the service class is as follows. I don't want my users to deal with dependencies creation and hence the empty constructor.

$m = new MailerService();
$m->sendEmail(($email));

My service class is as below as I am trying to inject the dependency as per doc in https://php-di.org/doc/attributes.html.

final class MailerService
{
    private MailerInterface $mailer;

    public function __construct(#[Inject('MailerInterface')] MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sendEmail(Email $email): void
    {
        $this->mailer->send($email);
    }
}

What is missing in my implementation? Any help is appreciated.

1

There are 1 best solutions below

2
IMSoP On

Dependency injection containers don't change how normal PHP code works; if you write new MailerService(); that still means "run the constructor with no arguments".

What dependency injection containers do is define how to create an instance when you ask for one from the container. (Generally, they also return the same instance each time you ask for one, rather than repeating the constructor call.)

In other words, when you write this:

$m = $container->get(MailerService::class);

Then the container runs this:

$dsn = sprintf(
    '%s://%s:%s@%s:%s',
    "smtp",
    "username",
    "password",
    "hostname",
    "587"
);
$mailer = new Mailer(Transport::fromDsn($dsn));
$m = new MailerService($mailer);

So what you need to do is replace code using the new operator. That means either:

  • Make the container itself available, and ask for the instance explicitly as I did in that example. This generally makes dependencies harder to keep track of, but can be useful in a controller which is tightly coupled to the framework already, and uses a lot of different services. A common rule of thumb is that your code should have an "application layer" which doesn't directly access the container.
  • Make your MailerService a constructor parameter in all the controllers and other services that need it. Repeat for all your services. Then the framework will ask for the controller, and the container will work out what objects need to be created, in what order, to fill all its parameters.