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
- dev-master / 4.0.x-dev
- dev-main / 3.3.x-dev
- v3.1.x-dev
- v3.0.x-dev
- v3.0.9
- v3.0.9-RC2
- v3.0.9-RC
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.4.x-dev
- v2.4.17
- v2.4.16
- v2.4.15
- v2.4.14
- v2.4.13
- v2.4.12
- v2.4.11
- v2.4.10
- v2.4.9
- v2.4.8
- v2.4.7
- v2.4.6
- v2.4.5
- v2.4.4
- v2.4.3
- v2.4.2
- v2.4.1
- v2.4.0
- v2.3.x-dev
- v2.3.14
- v2.3.13
- v2.3.12
- v2.3.11
- v2.3.10
- v2.3.9
- v2.3.8
- v2.3.7
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.x-dev
- v2.2.6
- v2.2.5
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- dev-inject-with-tags-working
- dev-inject-with-tags
- dev-patch-2
- dev-patch-1
This package is auto-updated.
Last update: 2026-05-24 12:57:35 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, and an O(1) precomputed (type, tag) index on the compiled container.
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).
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- All existing tests pass (162 in total)
Tag-aware features are strictly additive. Calling code that doesn't use tags 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: 164 pass (was 157 on the v3.3 baseline; +7 new for the tag features and the
array<string, T>bag autowire). - 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).