Skip to content

[DependencyInjection] Add Definition::addExcludedTag() and ContainerBuilder::findExcludedServiceIds() for auto-discovering value-objects #59704

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

Merged
merged 1 commit into from
Feb 11, 2025

Conversation

GromNaN
Copy link
Member

@GromNaN GromNaN commented Feb 5, 2025

Q A
Branch? 7.3
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

We could not use the method findTaggedServiceIds in #59401 (comment), same for api-platform/core#6943.

As "using the container loading tools to do resource discovery quite seamlessly" seems to be a good idea, this changes make it easier.

I'm not closed to alternative ideas if we want to go further with this use-case.

Usage

Let's create a AppModel attribute class and use it on any class of the project.

In the extension class:

$this->registerAttributeForAutoconfiguration(AppModel::class, static function (ChildDefinition $definition) {
    $definition->addExcludedTag('app.model');
});

In a compiler pass:

$classes = [];
foreach($containerBuilder->findExcludedServiceIds('app.model') as $id => $tags) {
    $classes[] = $containerBuilder->getDefinition($id)->getClass();
}

$containerBuilder->setParameter('.app.model_classes', $classes);

And this parameter can be injected into a service, or directly update a service definition to inject this list of classes. The attribute parameters can be injected into the tag, and retrieved in the compiler pass, for more advanced configuration.

@carsonbot carsonbot added this to the 7.3 milestone Feb 5, 2025
@GromNaN GromNaN changed the title [DependencyInjection] Accept excluded tags in findTaggedServiceIds, with a new parameter [DependencyInjection] Accept excluded services in findTaggedServiceIds, with a new parameter Feb 5, 2025
@GromNaN GromNaN changed the title [DependencyInjection] Accept excluded services in findTaggedServiceIds, with a new parameter [DependencyInjection] Return excluded services in findTaggedServiceIds, with a new parameter Feb 5, 2025
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.

I'm not sure I like to have a method with "services" in its name return non-services things

this made me wonder if we should add another method instead like this?

findTaggedValueObjects($name, $autoExclude = true)

@GromNaN GromNaN changed the title [DependencyInjection] Return excluded services in findTaggedServiceIds, with a new parameter [DependencyInjection] Add ContainerBuilder::findTaggedValueClasses() for auto-discovering value-object classes Feb 5, 2025
@GromNaN
Copy link
Member Author

GromNaN commented Feb 5, 2025

I added the method, that's a lot better and it allows to add the additional feature of automatically set the container.excluded tag. But isn't it late in the CompilerPass workflow?

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.

But isn't it late in the CompilerPass workflow?

This depends on the stage the compiler pass runs, right?
Nothing related to this PR IIUC.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Feb 6, 2025

But isn't it late in the CompilerPass workflow?

I think I've got your point. A complimentary idea might be to add Definition::tagAsValueObject(), to be used in registerAttributeForAutoconfiguration().

Tentatively:

/**
 * @return $this
 */
public function tagAsValueObject(string $name, array $attributes = []): static
{
    return $this->addTag($name, $attributes)->addTag('container.excluded', ['source' => \sprintf('by tag "%s"', $name));
}

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.

Closer to the sweet spot :)

@GromNaN
Copy link
Member Author

GromNaN commented Feb 6, 2025

Perfect, we have the full feature like this:

  • Definition::tagAsValueObject() called inside a registerAttributeForAutoconfiguration
  • ContainerBuilder::findTaggedValueObjects() called inside a compiler pass

Now I wonder if we could skip de tag and delay the registerAttributeForAutoconfiguration callback to any of the compile time. With something like registerAttributeForValueObject()

function registerAttributeForValueObject(Entity::class, static function (ContainerBuilder $container, array $classes) {
    $container->setParameter('doctrine.entity_classes', $classes);
});

@nicolas-grekas nicolas-grekas changed the title [DependencyInjection] Add ContainerBuilder::findTaggedValueClasses() for auto-discovering value-object classes [DependencyInjection] Add Definition::addValueObjectTag() and ContainerBuilder::findTaggedValueObjects() for auto-discovering value-objects Feb 6, 2025
@chalasr
Copy link
Member

chalasr commented Feb 7, 2025

I get and support the use case this tries to solve, but I don't like the fact we are adding the concept of value objects to the DIC. Services vs non-services is all the container knows so far which makes sense.

Also good part of the objects mentioned in the linked use cases do not match the definition of a value object actually: neither entity/models nor API resources are VO, both have identifiers.

@GromNaN
Copy link
Member Author

GromNaN commented Feb 7, 2025

That's class definitions that contribute to the build of the DIC; even if this classes are not used for services.

What do you think if we rename addValueObjectTag to addClassTag, and findTaggedValueObjects to findTaggedClasses? Or maybe addExcludedClassTag to be explicit that we also exclude the class from the DIC.

Then we could have a !tagged_classes in Yaml to automatically inject class names into a service, in the same format as findTaggedClasses.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Feb 7, 2025

I don't like addClassTag / findTaggedClasses - it doesn't tell anything about those not being services. Also this returns identifiers, not classes (even if most of the time we equal both).

If VO doesn't match, then what remains is that we want to exclude those definitions. What about:

addExcludingTag(string $name, array $attributes = [])

findExcludedServiceIds(string $name) (and we remove the autoexclude argument now that the name contains the term "exclude" - or keep it, and throw an exception when its false and a matching definition doesn't have the container.excluded tag?)

@GromNaN
Copy link
Member Author

GromNaN commented Feb 7, 2025

Since it's the class names we're interested in here, we can group the result tags by class name and not by serviceId. This way, there's no need to call $containerBuilder->getDefinition($serviceId)->getClass() in the loop in the compiler pass.

@nicolas-grekas
Copy link
Member

Class names are only one possible use case. Maybe the main one, but I don't see why we should optimize the design for this use case.

@GromNaN
Copy link
Member Author

GromNaN commented Feb 7, 2025

We are creating a way to read attributes on classes and make this metadata available to inject in services. There is no point in having multiple definitions for the same class as attributes are defined on the class itself (we can merge them).

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Feb 7, 2025

Unless there is one. My point is that the design and the code are perfectly able to handle more than just lists of classes, and I don't see any reasons (but our current narrow vision) to restrict the capabilities of what we already have...

@chalasr
Copy link
Member

chalasr commented Feb 7, 2025

That's class definitions that contribute to the build of the DIC; even if this classes are not used for services.

A DIC is all about services. The concept of excluded services has been added to Symfony's DIC mostly for DX purpose following the introduction of autowiring/autodiscovery, and it is still mostly hidden today if we look at the component's public API.

We are creating a way to read attributes on classes

Fact is that DI's low-level API is decoupled from classes through supporting any string as service identifier everywhere and allowing callables/factories for service construction, which is nice and fits PSR-11 perfectly also.

If VO doesn't match, then what remains is that we want to exclude those definitions

I agree. Trying to find a generic term for grouping some classes through the container then making them excluded looks like a dead end as there are many possible reasons for a class not to be registered as service and certainly ones we don't even imagine. Excluded services however is a concept we already have so let's build on it. Then Nicolas' proposal looks sensible.

nicolas-grekas added a commit that referenced this pull request Feb 10, 2025
…constructor when autodiscovering (nicolas-grekas)

This PR was merged into the 7.3 branch.

Discussion
----------

[DependencyInjection] Don't skip classes with private constructor when autodiscovering

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | Fix #48392
| License       | MIT

With value objects auto-discovery becoming more mainstream (see #59704), it's time to fix registering classes with private constructors.

Those are skipped today but with support for `#[Autoconfigure(constructor: 'createInstance')]` as introduced in #49665, this doesn't make sense anymore.

Best reviewed [ignoring whitespace](https://github.com/symfony/symfony/pull/59712/files?w=1).

Commits
-------

99830f6 [DependencyInjection] Don't skip classes with private constructor when autodiscovering
@GromNaN
Copy link
Member Author

GromNaN commented Feb 11, 2025

I updated the methods with more generic names: Definition::addExcludeTag() and ContainerBuilder::findExcludedServiceIds($tagName).

findExcludedServiceIds throws an exception when a service as the searched tag but not container.excluded.

@GromNaN GromNaN force-pushed the tags-excluded branch 2 times, most recently from 6cedb37 to f2259e1 Compare February 11, 2025 10:55
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.

LGTM, just minor wording things.

@GromNaN GromNaN changed the title [DependencyInjection] Add Definition::addValueObjectTag() and ContainerBuilder::findTaggedValueObjects() for auto-discovering value-objects [DependencyInjection] Add Definition::addExcludedTag() and ContainerBuilder::findExcludedServiceIds() for auto-discovering value-objects Feb 11, 2025
…erBuilder::findExcludedServiceIds()` for auto-discovering value-objects
@nicolas-grekas
Copy link
Member

Thank you @GromNaN.

@nicolas-grekas nicolas-grekas merged commit 09ba274 into symfony:7.3 Feb 11, 2025
9 of 11 checks passed
@GromNaN GromNaN deleted the tags-excluded branch February 11, 2025 13:20
nicolas-grekas added a commit that referenced this pull request Mar 20, 2025
…tag" (kbond)

This PR was merged into the 7.3 branch.

Discussion
----------

[DependencyInjection] Rename "exclude tag" to "resource tag"

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Issues        | Follow up to #59704
| License       | MIT

I find `$definition->addExcludedTag('app.model')` and `$containerBuilder->findExcludedServiceIds('app.model')` unintuitive. If feels like your excluding something from the DI container but in fact, your registering it, but in a "different way". The fact that it's an excluded service is just a side-effect. This is especially weird when the tag has attributes (`$definition->addExcludedTag('app.model', ['connection' => 'default'])`).

Totally open for a name other than "resource".

Commits
-------

aa91936 [DI] Rename "exclude tag" to "resource tag"
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.

6 participants