Slim 4 + PHP-DI: Getting New Instance From Container

Instead of retrieving the same instance of a particular object from the container on each resolution, suppose I want to retrieve a new, unique instance of the object while taking advantage of autowiring.

I am aware that PHP-DI offers a make() method for this purpose, but I want to be able to take advantage of autowiring so that I don’t have to inject the container into my controllers.

My use case here is a PHPMailer object that I want to be able to typehint in my controllers’ constructors and have a fresh instance to work with therein. I know this is more a PHP-DI question than a slim one, but any ideas on how to approach this?

Hi @ediv

I’ve had the same issue with PHP-DI.

In PHP-DI all your definitions are shared by default. Unfortunately PHP-DI does’t support the definition of factories (non shared objects). And yes, the make method makes no sense in this context (constructor injection and autowiring), because the container should not be injected.

The phpleage/container offers support for non-shared objects.

But PHP-DI is much faster (10x to 12x) and is better supported by the Slim community.

I would recommend to inject a Factory object.

Example

<?php

// ...

class MyService
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerFactory $loggerFactory)
    {
        $this->logger = $loggerFactory
            ->addFileHandler('my_service.log')
            ->createInstance('my_service');
    }
}

Hey @odan, gotcha. That’s sort of a bummer I was hoping there was just some bit I missed in the PHP-DI docs on how to get the container to resolve items in this fashion.

So in this case you would inject the factory into the service, and then inject the service into your Controller (or Action class as the case may be) as necessary?

Yes, exactly.

But I would only use a factory where it makes sense (and not everywhere). By default, I would let PHI-DI inject shared (singelton) objects.

1 Like

Still struggling with this one, feels like I’m missing something conceptually.

Based on prior discussion, I’ve created a MailerService class which creates a new PHPMailer instance through an injected MailerFactory (see below). However, it still appears that if the MailerService is used in multiple places throughout the app, it’s still using the same PHPMailer object.

Now, I’m still relying on autowiring when injecting my MailerFactory into the MailerService, and the MailerService into wherever it ends up being used. Do I need to completely sidestep the autowiring process and manually inject the MailerService to prevent its PHPMailer instance from being stored in and reused in the container?


class MailerFactory
{
    private $settings;

    public function __construct($settings)
    {
        $this->settings = $settings;
    }

    public function getMailerInstance()
    {
        $mailer = new \PHPMailer;
        $mailer->CharSet = $this->settings['char_set'];
        $mailer->WordWrap = $this->settings['word_wrap'];
        $mailer->isHTML($this->settings['is_html']);  
        $mailer->Host = $this->settings['host'];
        $mailer->Port = $this->settings['port'];
        $mailer->Mailer = $this->settings['mailer'];     

        return $mailer;
    }
}

class MailerService
{
    
    private $mailer;

    public function __construct (MailerFactory $mailerFactory)
    {
        $this->mailer = $mailerFactory->getMailerInstance();
    }

    // MailerService Methods ...
}

// meanwhile ..

class SomeActionController
{
    private $mailer;
    
    public function __construct(MailerService $mailer)
    {
        $this->mailer = $mailer;
    }

    public function doSomethingThenSendEmail()
    {
        // .. do some stuff, then:

        $this->mailer->setSubject('This is a subject');
        $this->mailer->addFrom('sender@sender.com');
        // .. etc 
        $this->mailer->send();
    }
}

The above works, but if the MailerService is utilized previously anywhere else previously in the app, its PHPMailer Object is the same one as was used previously which leads to the possibility of sending an email with some old unwanted data or settings associated with it.

Update: I could be crazy, I had been comparing the spl_object_hash() of the PHPMailer objects to test for sameness and I guess its possible that I was getting the same hash as the object was created and destroyed in place, so to speak.

In any case, I’ve just retained a reference to the MailerFactory in my MailerService class and created a method there to get a fresh instance from it when necessary. Still appears to be the same object, but all properties are defaulted so it works for my use case.

Just one note: Your controller does to much and breaks MVC. The service should contain the business logic and the controller only invokes the service method. The heavy work is getting done by the service.
Sending an email is just an implementation detail and should be moved to the service.

The Action does only these things:

  • collects input from the HTTP request (if needed)
  • invokes the service (domain layer) with those inputs (if required) and retains the result
  • builds an HTTP response (typically with the Domain invocation results).

All other logic, including all forms of input validation, error handling, and so on, are therefore pushed out of the Action and into the service (for domain logic concerns) or the response renderer (for presentation logic concerns).

I would refactor it like this in this example:

(I renamed MailerService to UserCreator to give the task a more specific name)

class UserCreator
{
    
    private $mailer;

    public function __construct (MailerFactory $mailerFactory)
    {
        $this->mailer = $mailerFactory->getMailerInstance();
    }

    // The service should contain only one method

   public function registerUser( /* parameters */ )
    {
        // .. do some stuff, then:
        $this->mailer->setSubject('This is a subject');
        $this->mailer->addFrom('sender@sender.com');
        // .. etc 
        $this->mailer->send();

        return true;
    }
}

class SomeActionController
{
    private $userCreator;
    
    public function __construct(UserCreator $userCreator)
    {
        $this->userCreator= $userCreator;
    }

    public function doSomethingThenSendEmail(ServerRequest $request, Response $response)
    {
        // Invoke the domain (service)
        $result = $this->userCreator->registerUser( /* parameters */);
       
        // render response (json or html)
        return $response->withJson(['success' => $result]);
    }
}