Skip to content

[DependencyInjection] Autowire tagged iterators using doc blocks #46253

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

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented May 4, 2022

Q A
Branch? 6.1
Bug fix? no
New feature? yes
Deprecations? no
Tickets n/a
License MIT
Doc PR todo

This patch allows the Dependency Injection component to autowire basic tagged iterators:

namespace App;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag]
interface RoomInterface
{
    public function getBeds(): int;
}

class Hospital
{
    /** @param iterable<RoomInterface> $rooms */
    public function __construct(private iterable $rooms) {}
}

(edited: simplified example removing the need for a YAML config)

Tadam! This just works 🎉

This follows discussions about @GregoireHebert's article "Inject a tagged iterator in a more natural way".

This patch has the following limitations:

  • phpdocumentor/reflection-docblock must be installed
  • Advanced cases aren't supported (union types, multiple tags...), use the new #[TaggedIterator] to handle these cases

@stof
Copy link
Member

stof commented May 4, 2022

In all static analyzers, Room[] means array<Room>. But TaggedIterator does not inject an array. So it would be better to build that based on a phpdoc of iterable<Room> instead.

@dunglas
Copy link
Member Author

dunglas commented May 4, 2022

@stof indeed it's much better. Patch and PR description updated to use iterable<RoomInterface>.

@alanpoulain
Copy link
Contributor

I don't see the value of having this configuration:

# config/services.yaml
services:
    _instanceof:
        App\RoomInterface:
            tags: ['app.room']

Since there is no tag information in the doc block, tagging the interface feels weird, at least to me.

@dunglas
Copy link
Member Author

dunglas commented May 4, 2022

@alanpoulain this is an existing feature: https://symfony.com/doc/current/service_container/tags.html#define-services-with-a-custom-tag

This patch just adds some syntactic sugar on top of this feature, to remove some verbose and useless configuration or attributes in the common case.

I discussed with @nicolas-grekas off-GitHub on a way to remove the config block you pointed out, but this will be for a follow-up PR (and may have edge cases).

@Tobion
Copy link
Contributor

Tobion commented May 5, 2022

I don't think this is a good idea as it removes the relation between the tag name and the iterator as already pointed out by @alanpoulain . If you search for the tag name, you won't find anything. That's really bad developer experience.

@dunglas
Copy link
Member Author

dunglas commented May 5, 2022

@Tobion @alanpoulain what we need is just a new convention: use the interface name if you want to inject all services implementing it, as we use the class name if there is only one instance of it registered as a service:

# config/services.yaml
services:
    _instanceof:
        App\RoomInterface:
-            tags: ['app.room']
+            tags: ['App\RoomInterface']

This would fix this DX issue isn't it?

I'm working on a follow-up PR to simplify/remove the need for this configuration snippet.

@dunglas
Copy link
Member Author

dunglas commented May 5, 2022

Actually, it's already supported:

namespace App;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag]
interface RoomInterface
{
    public function getBeds(): int;
}

class Hospital
{
    /** @param iterable<RoomInterface> $rooms */
    public function __construct(private iterable $rooms) {}
}

That's all you need.

@chalasr
Copy link
Member

chalasr commented May 5, 2022

It's quite common that the service taking the tagged iterator as dependency implements the same interface as the services included in the iterator, so that each method iterates over these services until it finds the right one and delegates the call to it. e.g:

class ChainRoomProvider implements RoomProviderInterface
{
    /** @param iterable<RoomProviderInterface> $roomProviders */
    public function __construct(private iterable $roomProviders) {}
    
    public function findRoom(): Room {
        foreach ($this->roomProviders as $roomProvider) {
            if ($roomProvider->supports($someRuntimeCriteria)) {
                return $this->roomProvider->findRoom(); 
            }
        }
    }
}

That wouldn't work with the proposed approach since the delegating implementation would end up being part of the iterator, right?

@dunglas
Copy link
Member Author

dunglas commented May 5, 2022

This is exactly the same situation as currently. This works as long as you don't enable autoconfiguration on ChainRoomProvider (and it's the same regardless of the config format you choose).

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second though, I fear this might have a too big impact on the compile-time of the container, since this is now going to parse all doc-blocks of all autowired methods. We should double check this aspect before considering this change.

Note that adding #[TaggedIterator(FooInterface::class)] next to the @paramalready works so this is just a way to write less declarative code next to an argument.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented May 10, 2022

This works as long as you don't enable autoconfiguration on ChainRoomProvider

Feature idea: allow doing this with an attribute? #[SkipAutoconfigure(...string[] $superTypes = [])]

@chalasr
Copy link
Member

chalasr commented Jul 2, 2022

Trying to help moving forward here: Given the limitations, the potential perf cost and the fact we already have an attribute for that (and using this feature implies to use an attribut anyway) I'm not convinced this is worth it.

@nicolas-grekas
Copy link
Member

Closing as discussed. Thanks for proposing.

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

Successfully merging this pull request may close these issues.

8 participants