Home > other >  Passing dynamic arguments to service factory in Symfony
Passing dynamic arguments to service factory in Symfony

Time:12-13

I'm integrating Symfony into an older application having its own dependency container based on PSR-11. Been searching for a solution to merge that DI container to the one Symfony uses, but found nothing. To just make it work, I came with one "hacky" solution which I don't like.

I've created this class. It creates an instance of an old DI container inside of it:

class OldAppServiceFactory
{
    private ContainerInterface $container;

    public function __construct()
    {
        $this->container = OldContainerFactory::create();
    }

    public function factory(string $className)
    {
        return $this->container->get($className);
    }
}

and added proper entries to services.yaml:

    oldapp.service_factory:
        class: Next\Service\LeonContainer\LeonServiceFactory

    OldApp\Repository\Repository1:
        factory: ['@oldapp.service_factory', 'factory']
        arguments:
            - 'OldApp\Repository\Repository1'

    OldApp\Repository\Repository2:
        factory: ['@oldapp.service_factory', 'factory']
        arguments:
            - 'OldApp\Repository\Repository2'

    OldApp\configuration\ConfigurationProviderInterface:
        factory: ['@oldapp.service_factory', 'factory']
        arguments:
            - 'OldApp\configuration\ConfigurationProviderInterface'

With above hack, putting those classes in service class constructors works. Unfortunately it looks bad and it'll be pain to extend it with more of those repositories (especially when having 50 of them). Is it possible to achieve something like this in services.yaml?

    OldApp\Repository\:
        factory: ['@oldapp.service_factory', 'factory']
        arguments:
            - << PASS FQCN HERE >>

This would leave me with only one entry in services.yaml for a single namespace of the old application.

But, maybe there is other solution for my problem? Been trying with configuring Kernel.php and prepareContainer(...) method, but I also ended with nothing as the old dependencies are in one PHP file returning an array:

return array [
    RepositoryMetadataCache::class => static fn () => RepositoryMetadataCache::createFromCacheFile(),
    EntityCollection::class => autowire(EntityCollection::class),
    'Model\Repository\*' => static function (ContainerInterface $container, RequestedEntry $entry) { ... }
];

CodePudding user response:

You could probably accomplish this easily with a custom compiler pass.

First tag all the old repository classes by loading the directory where they exist:

OldApp\Repository\:
    resource: '../src/OldApp/Repository/*'
    autowire: false
    autoconfigure: false
    tags: ['oldapp_repository']

(I think that you may need to also exclude src/OldApp from the default automatic service loading. E.g.:

App\:
    resource: '../src/*'
    exclude: '../src/{OldApp/Repository,DependencyInjection,Entity,Tests,Kernel.php}'

... but I'm not 100% sure, test this one).

Then create a compiler pass to go through the tags and define a factory for each one:

class OldAppRepositoryCompilerPass implements CompilerPassInterface
{

    public function process(ContainerBuilder $container): void
    {
        $taggedServices = $container->findTaggedServiceIds('oldapp_repository');

        foreach ($taggedServices as $serviceId => $tags) {

            $definition = $container->getDefinition($serviceId);
            $definition
                    ->setFactory([new Reference('@oldapp.service_factory'), 'factory'])
                    ->addArgument($serviceId);
        }

    }
}

And in your Application Kernel build() method add the compiler pass:

// src/Kernel.php
namespace App;

use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...

class Kernel extends BaseKernel
{
    // ...

    protected function build(ContainerBuilder $container): void
    {
        $container->addCompilerPass(new OldAppRepositoryCompilerPass());
    }
}

Can't test this right at this minute, but this should get you going in the right direction. For additional details check the docs:

You can check this example repo where the above is implemented and working. On this repo the OldApp namespace is outside of App and src, so no need to exclude it from automatic service loading.

  • Related