Add Symfony-aligned tagged collections, keyed defaults, and configurable autowire alias resolution - #273
Open
jmather wants to merge 2 commits into
Open
Add Symfony-aligned tagged collections, keyed defaults, and configurable autowire alias resolution#273jmather wants to merge 2 commits into
jmather wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This aligns several dependency-resolution behaviors with the Symfony DependencyInjection semantics that this project explicitly cites as an inspiration, while preserving existing NDI behavior by default.
The central distinction is cardinality: plural provider populations are valid; policy belongs to singular resolution.
This PR adds:
!taggedform;compile({ validate: true })mechanism.Background
I have a long history with Symfony -- the framework, and the community. So when I went shopping for a proper DI container for Node, and happened across
node-dependency-injection, to say I was excited... barely scratches the surface of my joy. :)During my due diligence process, an agent noted a small concern:
Which led to the first PR on my fork, but after comparing
node-dependency-injectionagainstsymfony/dependency-injectionfor the feature surface I was accustomed to leveraging, the@taggedand autowire alignments felt worth doing. Which led to ... well ... this.I really appreciate that this project exists. Thank you so much for making it a thing, and I hope you find these changes as useful as I do. Please let me know if you have any concerns.
Tagged collections
Existing
!tagged foobehavior is preserved unchanged for compatibility: it returns an array in definition order and does not assign global meaning to arbitrary tag attributes.An explicit reference-style form is added alongside the newer keyed-reference family:
@tagged(tag)follows Symfony-style tagged-iterator semantics:prioritytag attribute;@tagged(tag, indexAttribute)additionally projects the collection as aMap, using the requested tag attribute as the member key and falling back to the service id when absent.Arbitrary tag attributes remain consumer-owned metadata; the container does not impose universal semantics on them.
Keyed defaults
Keyed groups are plural collections and therefore do not require a default.
The invariant is now:
getKeyed(group)resolves it;This adjusts the validator behavior that inspected ordinary tags for
default: trueand reportedkeyed_group_no_default. The validator now checks keyed metadata (instead of tags) and reportskeyed_group_multiple_defaultsinstead.Runtime
getKeyed(group)therefore requires exactly one asserted default: zero defaults remains an error for singular resolution, while multiple defaults are now rejected as ambiguous rather than resolving whichever default happens to be encountered first.Configurable autowire alias resolution
Currently, TypeScript autowiring creates an interface alias while classes are being discovered. When multiple classes implement the same interface, the first discovered implementation wins and later implementations leave the existing alias alone.
That behavior is retained exactly as the default through a new
_defaultsoption:Supported policies are:
firstfirst-or-uniquebeforeOptimizationpasses and exactly one autowire-discovered implementation remains registered, repair ituniqueunique-or-failAmbiguousAutowireExceptionnoneThe late-resolution modes run after
beforeOptimizationcompiler passes and before optimization begins instantiating definitions. This lets conditions, explicit aliases, binds,AutowireOverridePass, and user compiler passes clarify the graph before uniqueness is evaluated.A valid existing alias is always treated as authoritative, regardless of whether it came from configuration, autowire, or a compiler pass.
Candidate selection remains scoped to implementations discovered by TypeScript autowiring. TypeScript interface implementation metadata does not exist on runtime JavaScript classes, so this does not pretend the generic container can infer interfaces from arbitrary definitions.
Compatibility
firstis the default and preserves current behavior, including existing applications and the repository's historical autowire tests that rely on first-discovered interface selection.The safer strategies are opt-in. This makes the feature suitable for a backward-compatible release while leaving room for a future major version to choose a stricter default if desired.
Alias integrity validation
Alias validity is a container concern, not an autowire-policy concern.
ContainerValidatornow reportsinvalid_aliaswhen an alias points directly to a service that is not registered. This follows current runtime lookup behavior, which resolves one alias hop and then requires a registered definition.The check is exposed only through the existing validation surface:
Compilation without validation preserves current behavior; an unused invalid alias is not proactively rejected and will still fail naturally if requested at runtime.
Because
compile({ validate: true })validates both before and after compilation, aliases that begin valid but are changed or invalidated by compiler passes are checked again against the compiled graph.Tests
Regression coverage includes:
@tagged(...)collections;!taggedcompatibility;firstautowire alias resolution;first-or-uniquerepair after a compiler pass removes the originally selected implementation;uniqueresolution;unique-or-failambiguity detection;nonedisabling interface alias synthesis;_defaults.autowireAliasResolutionconfiguration loading;compile({ validate: true });The repository Build workflow passes build, Standard, and the complete test suite across Node.js 15 through 25.
Rationale
NDI's history explicitly cites Symfony as a source of inspiration and added arbitrary tag attributes in the same early release. More recently, NDI added keyed service/reference semantics. This change keeps the resulting concepts distinct:
The goal is not to port Symfony wholesale, but to preserve the same semantic boundaries using NDI's existing model and API style.