Julien
Julien

Reputation: 1

How to get Symfony 4 dependency injection working with two different use case scenarios?

We're trying to find the best way to implement dependency injection in a Symfony project with a quite specific problematic.

At user level, our application rely on an "Account" doctrine entity which is loaded with the help of the HTTP_HOST global against a domain property (multi-domain application). Going on the domain example.domain.tld will load the matching entity and settings.

At the devops level, we also need to do batch work with CLI scripts on many accounts at the same time.

The question we are facing is how to write services that will be compatible with both needs?

Let's illustrate this with a simplified example. For the user level we have this and everything works great:

Controller/FileController.php

    public function new(Request $request, FileManager $fileManager): Response
    {
        ...
        $fileManager->addFile($file);
        ...

    }

Service/FileManager.php

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

Service/AccountFactory.php

    public function __construct(RequestStack $requestStack, AccountRepository $accountRepository)
    {
        $this->requestStack = $requestStack;
        $this->accountRepository = $accountRepository;
    }

    public function createAccount()
    {
        $httpHost = $this->requestStack->getCurrentRequest()->server->get('HTTP_HOST');
        $account = $this->accountRepository->findOneBy(['domain' => $httpHost]);

        if (!$account) {
            throw $this->createNotFoundException(sprintf('No matching account for given host %s', $httpHost));
        }

        return $account;
    }

Now if we wanted to write the following console command, it would fail because the FileManager is only accepting an AccountFactory and not the Account Entity.

$accounts = $accountRepository->findAll();
foreach ($accounts as $account) {
    $fileManager = new FileManager($account);
    $fileManager->addFile($file);
}

We could tweak in the AccountFactory but this would feel wrong... In reality this is even worse because the Account dependency is deeper in services.

Does anyone have an idea how to make this properly ?

Upvotes: 0

Views: 393

Answers (1)

Marcel Kohls
Marcel Kohls

Reputation: 1861

As a good practice, you should create an interface for the FileManager and set this FileManagerInterface as your dependency injection (instead of FileManager). Then, you can have different classes that follow the same interface rules but just have a different constructor.

With this approach you can implement something like:

Service/FileManager.php

interface FileManagerInterface
{
    // declare the methods that must be implemented
    public function FileManagerFunctionA();
    public function FileManagerFunctionB(ParamType $paramX):ReturnType;
}

FileManagerInterface.php

class FileManagerBase implements FileManagerInterface
{
    // implement the methods defined on the interface
    public function FileManagerFunctionA()
    {
        //... code
    }

    public function FileManagerFunctionB(ParamType $paramX):ReturnType
    {
        //... code
    }
}

FileManagerForFactory.php

class FileManagerForFactory implements FileManagerInterface
{
    // implement the specific constructor for this implementation
    public function __construct(AccountFactory $account)
    {
        // your code here using the account factory object
    }
    // additional code that is needed for this implementation and that is not on the base class
}

FileManagerAnother.php

class FileManagerForFactory implements FileManagerInterface
{
    // implement the specific constructor for this implementation
    public function __construct(AccountInterface $account)
    {
        // your code here using the account object
    }
    // additional code that is needed for this implementation and that is not on the base class
}

Ans last but not least:

Controller/FileController.php

public function new(Request $request, FileManagerInterface $fileManager): Response
{
    // ... code using the file manager interface
}

Another approach that also looks correct is, assuming that FileManager depends on an AccountInstance to work, changes could be made to your FileManager dependency to have the AccountInstance as a dependency instead of the Factory. Just Because in fact, the FileManager does not need the factory, it needs the result that the factory generates, so, automatically it is not FileManager's responsibility to carry the entire Factory.

With this approach you will only have to change your declarations like:

Service/FileManager.php

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

Service/AccountFactory.php

public function createAccount():AccountInterface
{
    // ... your code
}

Upvotes: 1

Related Questions