Reputation: 171
I'm using Symfony 4 for a project, and I have a question regarding factories.
Assume that I have a strategy depending on a kind of string.
I'd like to create differente services, each with own dependencies, based on this props and I'd like to create a factory service, so that the interface is simple.
Let me do an example:
class ServiceBar implements Doing{
public function __construct($dep1,$dep2){
}
public function do();
}
class ServiceBaz implements Doing{
public function __construct($dep3,$dep4){
}
public function do();
}
// Factory Class
class MyServiceFactory{
protected $services = [
'bar' => 'app.service.bar',
'baz' => 'app.service.baz'
];
public function __construct(ContainerInterface $sc){
$this->sc = $sc;
}
public function factory($string){
if(!$this->sc->has($this->services[$string])){
throw new Exception("Missing Service");
}
$this->sc->get($this->services[$string])->do();
}
}
// IndexController.php
public function indexAction(Request $request, MyServiceFactory $factory)
{
$factory->factory($request->get('action'));
}
With this implementation, I have my services created with all dependencies, and a factory called from my controller.
Do you have other ideas, of comment about this solution? I have service container injected withn factory constructor; is there other way to create services from a factory? is there something wrong with this approach?
Thanks in advance
Upvotes: 6
Views: 1711
Reputation: 24280
You can use own compiler passes, extensions and service locators. It's a way Symfony allows that, but it requires lot of code.
The simplest approach is to autowire arguments by autowired array.
/**
* @param Doing[] $doings
*/
public function __construct(array $doings)
{
$this->doings = $doings;
}
public function create(string $name): Doing
{
foreach ($this->doings as $doing) {
if ($doing->getName() === name) { // this depends on your design; can be also "is_a" or "instanceof"
return $doing;
}
}
throw new MissingDoingException;
}
This is also called collector pattern.
Upvotes: 0
Reputation: 48865
A Symfony Service Locator can be used to avoid the need to inject the complete container. The locator acts like a container but only has access to a limited number of services.
It takes a bit of magic to configure everything. In your case, you only want the locator to access services implementing the Doing interface.
Start with the locator which will inherit get/has methods like all containers:
use Symfony\Component\DependencyInjection\ServiceLocator;
class DoingLocator extends ServiceLocator
{
protected $services = [
'bar' => 'app.service.bar',
'baz' => 'app.service.baz'
];
public function locate($string) {
return $this->get($this->services[$string]);
}
}
Now comes the magic. You actually could configure this manually in services.yaml per the documentation but it is more fun to do it automatically.
Start by making your Kernel class a compiler pass:
# src/Kernel.php
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class Kernel extends BaseKernel implements CompilerPassInterface
Next, automatically tag all services that implement the Doing interface:
# Kernel.php
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(Doing::class)
->addTag('doing');
}
Finally, add a compiler pass to build your locator service:
# Kernel.php
public function process(ContainerBuilder $container)
{
$doingLocatorIds = [];
foreach ($container->findTaggedServiceIds('doing') as $id => $tags) {
$doingLocatorIds[$id] = new Reference($id);
}
$doingLocator = $container->getDefinition(DoingLocator::class);
$doingLocator->setArguments([$doingLocatorIds]);
}
And presto. You are done. You can now inject your DoingLocator (aka MyServiceFactory) and all should be well.
Upvotes: 1