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.
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.
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.
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.
$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 registeredservices:
orderService:
factory: OrderService
arguments:
cache: @App\Cache\CacheInterface#fastAny 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.
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 oneThe autowiring index registers each service under all its parent classes and interfaces; the tag filter narrows the candidates to the matching identity.
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).
- 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.
Container::getService($name)-@deprecateddocblock points atContainer::get($type, $tag). Docblock-only deprecation; no runtimeE_USER_DEPRECATEDis emitted, sinceget()itself callsgetService()internally.
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.
- 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.
For installation, service definitions, factories, decorators, NEON syntax, autowiring rules, extension authoring - read nette/di's documentation. Only the additions above are fork-specific.
BSD-3-Clause / GPL-2.0 / GPL-3.0 (same as upstream nette/di).