Skip to content

Autowire ServiceLocator with iterable of tagged services #39924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
zmitic opened this issue Jan 21, 2021 · 8 comments
Closed

Autowire ServiceLocator with iterable of tagged services #39924

zmitic opened this issue Jan 21, 2021 · 8 comments

Comments

@zmitic
Copy link

zmitic commented Jan 21, 2021

Description

Use attributes to have autowiring between ServiceLocator and tagged services, by key that can be either FQCN (default) or allow users to define it. Upgrade of implementation: #29203

Example of FQCN index, along with psalm annotation use:

class MyService
{
    public function __construct(
        #[Tagged(name: 'form.type')] private ServiceLocator $tagged
    ) {}
    
    /**
    * @param class-string<FormTypeInterface> $className
    */
    public function get(string $className): FormTypeInterface
    {
        return $this->tagged->get($className);
    }
}

$formType = $myService->get(EntityType::class);

If users want different index (like #29203), optional parameter would be the name of static method used:

interface ExporterInterface
{
    public static function getName(): string;
}

class CSVExporter implements ExporterInterface
{   
    public static function getName(): string
    {
        return 'csv_exporter';
    }
}

class MyService
{
    public function __construct(
        #[Tagged(name: 'app.exporter', indexMethod: 'getName')] private ServiceLocator $tagged
    ) {}
    
    public function get(string $name): ExporterInterface
    {
        return $this->tagged->get($name);
    }
}

$exporter = $myService->get('csv_exporter'); // this would be instance of CSVExporter

If of any relevance, stub for ServiceLocator can be this:

/** 
 * @template T
 */
class ServiceLocator
{
    /** @return T */
    public function get($id){}
}
@zmitic
Copy link
Author

zmitic commented Jan 21, 2021

Best case would be to allow interface usage instead of string-based tag names:

class MyService
{
    public function __construct(
        #[TaggedInterface(FormTypeInterface::class)] private ServiceLocator $tagged
    ) {}
    
    /**
    * @param class-string<FormTypeInterface> $className
    */
    public function get(string $className): FormTypeInterface
    {
        return $this->tagged->get($className);
    }
}

$formType = $myService->get(EntityType::class);

A feature like this could be useful for a day we get generics; tools like rector might provide automigration:

public function __construct(private ServiceLocator<FormTypeInterface> $tagged)

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Jan 21, 2021

#[Tagged(name: 'form.type')]

This looks like just one step after #39897 - could happen in 5.3 :)

#[TaggedInterface(FormTypeInterface::class)]

I would clearly advise against this. It's a shortcut that forgets that tags are absolutely required as intermediaries to create collections of services in a decoupled way.

But fortunately, together with #39804, you might end up being able to provide a very similar DX.

@zmitic
Copy link
Author

zmitic commented Jan 21, 2021

I would clearly advise against this.

That is OK. My tagged services have same name as interface i.e.

$container->registerForAutoconfiguration(ExporterInterface::class)->addTag(ExporterInterface::class);

so I thought to maybe even remove this one line. But yes, it might be too much, sorry.

@nicolas-grekas
Copy link
Member

PR welcome @zmitic

@zmitic
Copy link
Author

zmitic commented Jun 21, 2021

@nicolas-grekas For #[TaggedInterface]?

If so; would I be allowed to create another class instead of ServiceLocator, and add stubs like in the first comment?

@nicolas-grekas
Copy link
Member

For #[Tagged(name: 'form.type')] (see above about #[TaggedInterface])

@zmitic
Copy link
Author

zmitic commented Jul 29, 2021

@nicolas-grekas

I am sorry I didn't have time for this before.

Before I start, just to confirm:

  • unless indexAttribute is defined, keys should be class names instead of integers; correct?

I.e. instead of this way to get class-name based index:

Copy&paste from real project
class MyMainService
{
    public function __construct(
        #[TaggedLocator(tag: ViewFactoryInterface::class, indexAttribute: 'default')] 
        private ServiceLocator $tagged,
    )
    {
    }
}

abstract class AbstractViewFactory implements ViewFactoryInterface
{
    public static function getDefaultName(): string
    {
        return self::class;
    }
}

it should work this way when indexAttribute is null. Is this correct?

@zmitic
Copy link
Author

zmitic commented Nov 5, 2021

With latest @fractalzombie patch, everything works perfectly.

Thank you all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants