3
votes

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

2
As a general rule, injecting the container is frowned upon because it gives you global access to all services. However, for cases where you don't know the specific service in advance, injecting the container is acceptable. Symfony has recently introduced Service Locators which act like containers but only have access to specific services such as all services that implement Doing. It's a useful refinement if you care about such things.Cerad

2 Answers

1
votes

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.

0
votes

You can use own compiler passes, extensions and service locators. It's a way Symfony allows that, but it requires lot of code.

Autowired Array

The simplest approach is to autowire arguments by autowired array.

  • no container dependency
  • no extension
  • no bundle registration
  • 1 compiler pass

Example

/**
 * @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.

How to Integrate