Reputation: 2549
I have a Sumfony 4.3 command that processes some data and loops through a number of "processors" to do the processing. The code uses a factory (autowired) which then instantiates the command.
use App\Entity\ImportedFile;
use App\Service\Processor\Processor;
class Factory implements FactoryInterface
{
/** @var array */
private $processors;
/** @var TestClausesInterface */
private $testClauses;
private $em;
private $dataSetProvider;
private $ndviFromNasaService;
private $archivalHashService;
private $mailer;
private $projectDir;
public function __construct(
TestClausesInterface $testClauses,
ValidProcessorList $processors,
EntityManagerInterface $em,
DataSetProvider $dataSetProvider,
NDVIFromNasaService $ndviFromNasaService,
ArchivalHashService $archivalHashService,
\Swift_Mailer $mailer,
$projectDir)
{
$this->processors = $processors;
$this->testClauses = $testClauses;
$this->em = $em;
$this->dataSetProvider = $dataSetProvider;
$this->ndviFromNasaService = $ndviFromNasaService;
$this->archivalHashService = $archivalHashService;
$this->mailer = $mailer;
$this->projectDir = $projectDir;
}
public function findProcessorForFile(ImportedFile $file)
{
...
if ($found){
$candidates = $this->recursive_scan( $this->projectDir.'/src/Processor');
foreach ($candidates as $candidate){
if (substr($candidate,0,strlen('Helper')) === 'Helper'){
continue;
}
try {
$candidate = str_replace($this->projectDir.'/src/Processor/', '', $candidate);
$candidate = str_replace('/','\\', $candidate);
$testClassName = '\\App\\Processor\\'.substr( $candidate, 0, -4 );
/* @var Processor $test */
if (!strstr($candidate, 'Helper')) {
$test = new $testClassName($this->testClauses, $this->em, $this->dataSetProvider, $this->ndviFromNasaService, $this->archivalHashService, $this->mailer, $this->projectDir);
}
However I still have to:
I have around 70 subclasses of Processor. All of them use EntityInterface
, but only a couple use SwiftMailer
and the other dependencies.
As I am adding services to be used only by a few Processors, I am looking for a way to autowire these arguments only at the Processor level. Ideally, also without adding service definitions to services.yml
In summary, I would like to be able to add a dependency to any subclass of Processor
, even if it is a parent class of other subclasses and have the dependency automatically injected.
Upvotes: 5
Views: 942
Reputation: 47639
There is much it is not immediately obvious in your code, but the typical way to resolve this is by using a "service locator". Docs.
Let's imagine you have several services implementing the interface Processor
:
The interface:
interface Processor {
public function process($file): void;
}
Couple implementation:
class Foo implements Processor
{
public function __construct(DataSetProvider $dataSet, ArchivalHashService $archivalHash, \Swift_Mailer $swift) {
// initialize properties
}
public function process($file) {
// process implementation
}
public static function getDefaultIndexName(): string
{
return 'candidateFileOne';
}
}
Couple implementations:
class Bar implements Processor
{
public function __construct(\Swift_Mailer $swift, EntityManagerInterface $em) {
// initialize properties
}
public function process($file) {
// process implementation
}
public static function getDefaultIndexName(): string
{
return 'candidateFileTwo';
}
}
Note that each of the processors have completely different dependencies, and can be auto-wired directly, and that each of them has a getDefaultIndexName()
method.
Now we'll "tag" all services implementing the Processor
interface:
# services.yaml
services:
# somewhere below the _defaults and the part where you make all classes in `src` available as services
_instanceof:
App\Processor:
tags:
- { name: "processor_services", default_index_method: 'getDefaultIndexName' }
Attention here: The documentation says that if you define a public static function getDefaultIndexName()
it will be picked by default. But I've found this not to be working at the moment. But if you define the default_index_method
you can wire it to a method of your choice. I'm keeping the getDefaultIndexName
for the time being, but you can pick something of your own choice.
Now, if you need this processes in a console command, for example:
use Symfony\Component\DependencyInjection\ServiceLocator;
class MyConsoleCommand
{
private ServiceLocator $locator;
public function __construct(ServiceLocator $locator)
{
$this->locator = $locator;
}
}
To inject the service locator you would do:
#services.yaml
services:
App\HandlerCollection:
arguments: [!tagged_locator { tag: 'processor_services' } ]
And to fetch any of the processors from the service locator you would do:
$fooProcessor = $this->locator->get('candidateFileOne');
$barProcessor = $this->locator->get('candidateFileTwo');
Summping up, basically what you need is:
getDefaultIndexName()
for each processor, which helps you match files to processors.And you can leave all services auto-wired.
Note: You could use an abstract class instead of an interface, and it would work the same way. I prefer using an interface, but that's up to you.
For completion sake, here is a repo with the above working for Symfony 4.3.
Upvotes: 3