delacry / di
Compiled dependency injection container — fork of nette/di adding tag-based service identity, #[Inject(tag:)], NEON @Type#tag references, and array<string, T> bag-of-services autowire.
Requires
- php: >=7.1 <8.2
- ext-tokenizer: *
- nette/neon: ^3.0
- nette/php-generator: ^3.3.3
- nette/robot-loader: ^3.2
- nette/schema: ^1.1
- nette/utils: ^3.1.4
Requires (Dev)
- nette/tester: ^2.2
- phpstan/phpstan: ^0.12
- tracy/tracy: ^2.3
Conflicts
- nette/bootstrap: <3.0
This package is auto-updated.
Last update: 2026-06-14 13:23:20 UTC
README
A fork of nette/di that adds tag-based dependency injection for services of the same type, so you can register multiple implementations of an interface and pick the right one at the injection site by tag.
Why this fork exists
nette/di#321 - InjectExtension: added support for injecting services by tags - has been open against upstream since May 2025 with no movement. This fork picks the feature up, ships it, and extends the model further: a first-class identity tag on every service definition, a new canonical Container::get() lookup, NEON @Type#tag reference syntax, an O(1) precomputed (type, tag) index on the compiled container, and deterministic ordering of autowired collections via per-definition priority/before/after.
For everything else about Nette DI - service definitions, factories, decorators, NEON syntax, autowiring rules, extension authoring - see nette/di's documentation. All of it works the same here. This README only covers what's different.
What's new
#[Inject(tag: 'X')] on properties, constructor parameters, and inject methods
use Nette\DI\Attributes\Inject; class OrderService { public function __construct( #[Inject(tag: 'fast')] public readonly CacheInterface $cache, ) {} }
Same attribute works on properties:
class OrderService { #[Inject(tag: 'fast')] public CacheInterface $cache; }
…and on inject*() method parameters:
class OrderService { public function injectCache(#[Inject(tag: 'fast')] CacheInterface $cache): void { $this->cache = $cache; } }
#[Inject] on a constructor or inject-method parameter requires a tag - untagged parameters are autowired by native type already, so a bare #[Inject] there is redundant and throws at compile time.
Single-string identity tag per service
services: cache.fast: factory: App\Cache\RedisCache tag: fast cache.slow: factory: App\Cache\FileSystemCache tag: slow fallback: App\Cache\NullCache # untagged services are implicitly tagged "default"
Or via the fluent API:
$builder->addDefinition('cache.fast') ->setType(App\Cache\RedisCache::class) ->setTag('fast');
The single-string tag is intentionally distinct from upstream's existing multi-key tags: { … } metadata bag (which is unchanged and still works). Tags here are an identity discriminator used together with the service type; tags: is a free-form metadata bag used by extensions like LocatorDefinition's tagged: selector.
Container::get($type, ?$tag) canonical lookup
$cache = $container->get(CacheInterface::class, 'fast'); // RedisCache $cache = $container->get(CacheInterface::class); // NullCache (untagged → default)
Backed by a precomputed array<class-string, array<tag, list<name>>> index baked into the generated container at compile time. The hot path is one hash lookup + one count() + one getService() - ~108 ns/op on a 10-implementation interface with tag filtering, ~9.2M ops/s on a single core (measured on PHP 8.4, no opcache JIT). For comparison, plain getService($name) by direct name lookup measures ~40 ns/op.
get() throws MissingServiceException on miss or ambiguity. For a nullable miss, use getOrNull($type, ?$tag) - same fast path, returns null instead of throwing when nothing matches. Ambiguity (multiple services match) still throws on getOrNull() because it's a programming error, not a "does this exist?" question.
$cache = $container->getOrNull(CacheInterface::class, 'optional'); // null if not registered
NEON @Type#tag reference syntax
services: orderService: factory: OrderService arguments: cache: @App\Cache\CacheInterface#fast
Any reference value containing a backslash is treated as a type reference (this is upstream Nette's rule, not new in the fork), so namespaced FQNs like @App\Cache\CacheInterface work as-is. A leading \ is only needed for global-namespace types (@\CacheInterface#fast) to disambiguate them from a service-name reference. NEON's own tokenizer accepts the #tag suffix unquoted, so no escaping required.
Polymorphic resolution
All three of these return the same instance when cache.fast is the only 'fast'-tagged service implementing CacheInterface:
$container->get(CacheInterface::class, 'fast'); $container->get(RedisCache::class, 'fast'); $container->get(RedisCache::class); // RedisCache is the only one
The autowiring index registers each service under all its parent classes and interfaces; the tag filter narrows the candidates to the matching identity.
Tag-keyed bag-of-services autowire: array<string, T>
A constructor parameter PHPDoc-typed as array<string, T> is autowired as a tag-keyed map of every autowired service implementing T:
class PoolRegistry { /** * @param array<string, CacheInterface> $pools */ public function __construct( public readonly array $pools, ) {} }
Given the services above, $pools is filled with ['fast' => $redisCache, 'slow' => $fsCache, 'default' => $fallback]. The generated container emits the array literal at compile time - no runtime aggregation.
The pre-existing T[], list<T> and array<int, T> patterns continue to autowire as numerically-keyed lists, unchanged from upstream. If two services of the same type share the same identity tag, the array<string, T> autowire throws at compile time (the tag → service mapping must be unambiguous).
Deterministic collection ordering: priority / before / after
When a parameter autowires a collection of a type (T[], list<T>, array<int, T>, or the tag-keyed array<string, T> above), the collected services come back in registration order - which, for attribute- or filesystem-discovered services, is not reproducible across machines. Each Definition carries optional ordering metadata that makes the order deterministic:
$builder->addDefinition('appRouter') ->setType(App\AppRouter::class) ->setPriority(100); // higher is collected first $builder->addDefinition('adminRouter') ->setType(Admin\AdminRouter::class) ->setBefore([App\AppRouter::class]) // relative: collected before AppRouter ->setAfter([Core\CoreRouter::class]); // …and after CoreRouter
The order is resolved by Nette\DI\DefinitionOrdering and applied wherever a collection of a type is assembled - autowired collection parameters as well as direct ContainerBuilder::findByType() / findAutowired() calls all return services in this order:
priority(?int, defaultnull→ treated as0) - an absolute tier; higher is collected first.before/after(list<class-string>) - relative, hard constraints against other collected services. They become edges in a topological sort;priority, then the service's type FQCN, then its name break ties among otherwise-unordered services.
Rules:
- A collection in which nothing carries ordering metadata is returned in registration order, untouched - existing code and other Nette DI users see no behavioural change.
before/aftermatch byis_a, so referencing an interface orders this service against every collected implementation of it.- A reference that matches no collected service is silently ignored - that's how a package can declare "I run before X" when X may not be installed.
- A cycle (A before B, B before A) throws
ServiceCreationExceptionat compile time, naming the tangled services.
These are storage-only primitives: the engine reads them, but never sets them itself. A higher layer (e.g. an attribute-driven compiler pass) decides what they mean and calls the setters - the same division of labour as the multi-key tags: bag.
What's removed
- Legacy
@injectdocblock annotation fallback inInjectExtension(use the#[Inject]attribute) - Legacy
@vartype-hint fallback for inject properties (use native type hints) Helpers::parseAnnotation()(no remaining callers after the @inject strip)- Pre-3.0 class aliases:
Nette\DI\ServiceDefinition,Nette\DI\Statement,Nette\DI\Config\IAdapter Definition::generateMethod()(callers updated to useDefinition::generateCode())Definition::isAutowired()(usegetAutowired())
Definition::setClass() / getClass() are kept as deprecated wrappers because tracy/tracy's DI bridge still calls them.
Deprecated (still functional)
Container::getService($name)-@deprecateddocblock points atContainer::get($type, $tag). Docblock-only deprecation; no runtimeE_USER_DEPRECATEDis emitted, sinceget()itself callsgetService()internally.
Backward compatibility
This fork keeps the engine permissive:
addDefinition($name, …)with a non-null name still worksservices: { foo: Bar }NEON keys still registerfooas the service name- Collection ordering is opt-in: a collection with no
priority/before/afteris returned in registration order, exactly as before - All existing tests pass
Tag-aware features and the ordering primitives are strictly additive. Calling code that doesn't use them behaves exactly like upstream nette/di v3.3.
Status
- Based on upstream
nette/div3.3 (commitd16957a). - Not tracking upstream - upstream branches force-push, so changes from upstream are cherry-picked when needed.
- Tests: 178 pass (was 157 on the v3.3 baseline; the tag features, the
array<string, T>bag autowire, and deterministic collection ordering are the additions). - PHP requirement: 8.4 – 8.5 (bumped from upstream's 8.2 – 8.5; the fork uses asymmetric property visibility for
Definition::$tagand other 8.4-only conveniences). If you need 8.2 or 8.3 compatibility, stay on upstreamnette/di.
Documentation
For installation, service definitions, factories, decorators, NEON syntax, autowiring rules, extension authoring - read nette/di's documentation. Only the additions above are fork-specific.
License
BSD-3-Clause / GPL-2.0 / GPL-3.0 (same as upstream nette/di).