Reputation: 1
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
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