PHPackages                             delacry/di - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. delacry/di

ActiveLibrary[Framework](/categories/framework)

delacry/di
==========

Compiled dependency injection container — fork of nette/di adding tag-based service identity, #\[Inject(tag:)\], NEON @Type#tag references, and array&lt;string, T&gt; bag-of-services autowire.

v3.0.9(4y ago)0145↓66.7%BSD-3-ClausePHPPHP &gt;=7.1 &lt;8.2CI failing

Since May 11Pushed 1mo agoCompare

[ Source](https://github.com/delacry/di)[ Packagist](https://packagist.org/packages/delacry/di)[ Docs](https://nette.org)[ RSS](/packages/delacry-di/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (8)Versions (63)Used By (0)

nette/di - delacry fork
=======================

[](#nettedi---delacry-fork)

A fork of [nette/di](https://github.com/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
--------------------

[](#why-this-fork-exists)

[nette/di#321](https://github.com/nette/di/pull/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, deterministic ordering of autowired collections via per-definition `priority`/`before`/`after`, and priority-ordered service decoration (onion chains).

For everything else about Nette DI - service definitions, factories, decorators, NEON syntax, autowiring rules, extension authoring - see [nette/di's documentation](https://doc.nette.org/dependency-injection). All of it works the same here. This README only covers what's different.

What's new
----------

[](#whats-new)

### `#[Inject(tag: 'X')]` on properties, constructor parameters, and inject methods

[](#injecttag-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

[](#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

[](#containergettype-tag-canonical-lookup)

```
$cache = $container->get(CacheInterface::class, 'fast'); // RedisCache
$cache = $container->get(CacheInterface::class);          // NullCache (untagged → default)
```

Backed by a precomputed `array` 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

[](#neon-typetag-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

[](#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`

[](#tag-keyed-bag-of-services-autowire-arraystring-t)

A constructor parameter PHPDoc-typed as `array` is autowired as a tag-keyed map of every autowired service implementing `T`:

```
class PoolRegistry
{
    /**
     * @param array $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` and `array` 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` autowire throws at compile time (the tag → service mapping must be unambiguous).

### Deterministic collection ordering: `priority` / `before` / `after`

[](#deterministic-collection-ordering-priority--before--after)

When a parameter autowires a *collection* of a type (`T[]`, `list`, `array`, or the tag-keyed `array` 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`, default `null` → treated as `0`) - an absolute tier; higher is collected first.
- **`before` / `after`** (`list`) - 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`/`after` match by `is_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 `ServiceCreationException` at 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.

### Service decoration: priority-ordered onion chains

[](#service-decoration-priority-ordered-onion-chains)

A *decorator* implements the same type as the service it wraps, receives the inner instance as a constructor argument, and takes over that service's slot for autowiring. `Definition::decorate()` declares the relationship:

```
$builder->addDefinition()
    ->setType(App\Cache\RedisCache::class);

$builder->addDefinition()
    ->setType(App\Cache\LoggingCache::class)   // also implements CacheInterface
    ->decorate(CacheInterface::class);      // wrap whatever serves CacheInterface
```

Now `get(CacheInterface::class)` returns the `LoggingCache`, constructed with the `RedisCache` as its `CacheInterface` argument. The wrapped service stays registered but no longer wins autowiring.

Several decorators of one slot stack into an onion ordered by decoration **priority - highest is outermost** (the layer consumers receive):

```
$builder->addDefinition()->setType(TimingCache::class)
    ->decorate(CacheInterface::class, priority: 100);   // outermost
$builder->addDefinition()->setType(LoggingCache::class)
    ->decorate(CacheInterface::class, priority: 0);     // inner

// get(CacheInterface::class) === TimingCache → LoggingCache → RedisCache
```

This decoration priority is its **own axis**, separate from the collection `priority` above - it orders the onion, not collection membership, so a deeply nested decorator never shifts where the service sorts in an `array` / `list` collection.

#### Decorating a tagged slot

[](#decorating-a-tagged-slot)

`decorate($type, $tag)` wraps one `(type, tag)` identity, leaving the other tagged services of that type alone:

```
$builder->addDefinition()->setType(RedisCache::class)->setTag('fast');
$builder->addDefinition()->setType(FileSystemCache::class)->setTag('slow');

$builder->addDefinition()->setType(AuditedCache::class)
    ->decorate(CacheInterface::class, 'fast');

$container->get(CacheInterface::class, 'fast'); // AuditedCache wrapping RedisCache
$container->get(CacheInterface::class, 'slow'); // FileSystemCache, untouched
```

The outermost wrapper inherits the slot's identity tag, so `get($type, $tag)` transparently returns the chain. Because a definition carries a single identity tag, all of one definition's `decorate()` targets must share one tag.

#### Decorating several types at once

[](#decorating-several-types-at-once)

A decorator implementing more than one interface wraps each by calling `decorate()` per type - handy when one underlying service fills several roles:

```
$builder->addDefinition()->setType(FileIo::class);       // implements Reader, Writer
$builder->addDefinition()->setType(TracingIo::class)     // implements Reader, Writer
    ->decorate(Reader::class)
    ->decorate(Writer::class);
// get(Reader::class) and get(Writer::class) both return the one TracingIo, which
// receives the single FileIo through a `Reader&Writer` constructor parameter.
```

#### Which constructor parameter receives the inner

[](#which-constructor-parameter-receives-the-inner)

The weaver injects the inner into the parameter **already bound to the slot's service** - by a NEON `@Type#tag` argument, or by an extension that resolved a tagged parameter (e.g. `#[Inject(tag:)]`) into a `Reference`:

```
final class AuditedCache implements CacheInterface
{
    public function __construct(
        #[Inject(tag: 'metrics')] private CacheInterface $metrics, // a collaborator, left alone
        #[Inject(tag: 'fast')]    private CacheInterface $inner,   // the decorated 'fast' slot
    ) {}
}
```

If no parameter is pre-bound to the slot, the first unbound parameter whose type the inner satisfies is used (an exact match on the decorated type ahead of a supertype or intersection). The weaver reads the **resolved argument**, not the attribute - so `#[Inject]`, any injection attribute a downstream extension resolves, or a plain NEON reference all produce the same `Reference` it reads, and the engine never has to know about the attribute.

#### Mechanics

[](#mechanics)

- Chains are woven by `Nette\DI\Decoration` during `ContainerBuilder::complete()`; `get()` / `getByType()` / autowiring all see only the outermost wrapper.
- The wrapped base and any buried decorators are dropped from autowiring (still registered, referenced internally by the chain); a head that is itself wrapped in another slot is autowiring-restricted to the types it heads, so it can't win a slot it sits inside.
- Decorating a type with no base service - or with more than one base in the slot - throws `ServiceCreationException` at compile time.
- Like `priority`/`before`/`after`, `decorate()` is a storage-only primitive: the engine reads it, a higher layer (e.g. an `#[AsDecorator]` compiler pass) sets it.

What's removed
--------------

[](#whats-removed)

- Legacy `@inject` docblock annotation fallback in `InjectExtension` (use the `#[Inject]` attribute)
- Legacy `@var` type-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 use `Definition::generateCode()`)
- `Definition::isAutowired()` (use `getAutowired()`)

`Definition::setClass()` / `getClass()` are kept as deprecated wrappers because tracy/tracy's DI bridge still calls them.

Deprecated (still functional)
-----------------------------

[](#deprecated-still-functional)

- `Container::getService($name)` - `@deprecated` docblock points at `Container::get($type, $tag)`. Docblock-only deprecation; no runtime `E_USER_DEPRECATED` is emitted, since `get()` itself calls `getService()` internally.

Backward compatibility
----------------------

[](#backward-compatibility)

This fork keeps the engine permissive:

- `addDefinition($name, …)` with a non-null name still works
- `services: { foo: Bar }` NEON keys still register `foo` as the service name
- Collection ordering is opt-in: a collection with no `priority`/`before`/`after` is 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
------

[](#status)

- Based on upstream `nette/di` v3.3 (commit `d16957a`).
- Not tracking upstream - upstream branches force-push, so changes from upstream are cherry-picked when needed.
- Tests: 182 pass (was 157 on the v3.3 baseline; the tag features, the `array` bag autowire, deterministic collection ordering, and service decoration 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::$tag` and other 8.4-only conveniences). If you need 8.2 or 8.3 compatibility, stay on upstream `nette/di`.

Documentation
-------------

[](#documentation)

For installation, service definitions, factories, decorators, NEON syntax, autowiring rules, extension authoring - read [nette/di's documentation](https://doc.nette.org/dependency-injection). Only the additions above are fork-specific.

License
-------

[](#license)

BSD-3-Clause / GPL-2.0 / GPL-3.0 (same as upstream nette/di).

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance61

Regular maintenance activity

Popularity15

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity90

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 91.1% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~47 days

Recently: every ~4 days

Total

57

Last Release

1778d ago

Major Versions

v2.4.15 → v3.0.02019-04-03

v2.4.16 → v3.0.22019-12-17

v2.4.17 → v3.0.62020-11-25

v2.4.x-dev → v3.0.82021-03-02

PHP version history (7 changes)v2.2.0PHP &gt;=5.3.1

v2.4.0PHP &gt;=5.6.0

v3.0.0PHP &gt;=7.1

v2.4.17PHP &gt;=5.6 &lt;8.1

v3.0.6PHP &gt;=7.1 &lt;8.1

v3.0.9-RCPHP &gt;=7.1 &lt;8.2

v3.1.x-devPHP &gt;=7.2 &lt;8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/45132928?v=4)[delacry](/maintainers/delacry)[@delacry](https://github.com/delacry)

---

Top Contributors

[![dg](https://avatars.githubusercontent.com/u/194960?v=4)](https://github.com/dg "dg (1223 commits)")[![fprochazka](https://avatars.githubusercontent.com/u/158625?v=4)](https://github.com/fprochazka "fprochazka (16 commits)")[![delacry](https://avatars.githubusercontent.com/u/45132928?v=4)](https://github.com/delacry "delacry (15 commits)")[![matej21](https://avatars.githubusercontent.com/u/1276059?v=4)](https://github.com/matej21 "matej21 (13 commits)")[![JanTvrdik](https://avatars.githubusercontent.com/u/175109?v=4)](https://github.com/JanTvrdik "JanTvrdik (10 commits)")[![enumag](https://avatars.githubusercontent.com/u/539462?v=4)](https://github.com/enumag "enumag (9 commits)")[![hrach](https://avatars.githubusercontent.com/u/284263?v=4)](https://github.com/hrach "hrach (5 commits)")[![janbarasek](https://avatars.githubusercontent.com/u/4738758?v=4)](https://github.com/janbarasek "janbarasek (5 commits)")[![Vrtak-CZ](https://avatars.githubusercontent.com/u/112567?v=4)](https://github.com/Vrtak-CZ "Vrtak-CZ (5 commits)")[![milo](https://avatars.githubusercontent.com/u/439140?v=4)](https://github.com/milo "milo (5 commits)")[![pavelkouril](https://avatars.githubusercontent.com/u/636912?v=4)](https://github.com/pavelkouril "pavelkouril (4 commits)")[![lookyman](https://avatars.githubusercontent.com/u/3863468?v=4)](https://github.com/lookyman "lookyman (4 commits)")[![xificurk](https://avatars.githubusercontent.com/u/117465?v=4)](https://github.com/xificurk "xificurk (3 commits)")[![kravco](https://avatars.githubusercontent.com/u/115938?v=4)](https://github.com/kravco "kravco (3 commits)")[![adaamz](https://avatars.githubusercontent.com/u/4347332?v=4)](https://github.com/adaamz "adaamz (2 commits)")[![kukulich](https://avatars.githubusercontent.com/u/260445?v=4)](https://github.com/kukulich "kukulich (2 commits)")[![sallyx](https://avatars.githubusercontent.com/u/4263901?v=4)](https://github.com/sallyx "sallyx (2 commits)")[![vojtech-dobes](https://avatars.githubusercontent.com/u/415925?v=4)](https://github.com/vojtech-dobes "vojtech-dobes (2 commits)")[![vrana](https://avatars.githubusercontent.com/u/117453?v=4)](https://github.com/vrana "vrana (2 commits)")[![Majkl578](https://avatars.githubusercontent.com/u/144181?v=4)](https://github.com/Majkl578 "Majkl578 (2 commits)")

---

Tags

factorynettediiocdicstaticcompiled

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/delacry-di/health.svg)

```
[![Health](https://phpackages.com/badges/delacry-di/health.svg)](https://phpackages.com/packages/delacry-di)
```

###  Alternatives

[nette/di

💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.

92641.5M1.6k](/packages/nette-di)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[nette/bootstrap

🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.

68737.6M663](/packages/nette-bootstrap)[nette/application

🏆 Nette Application: a full-stack component-based MVC kernel for PHP that helps you write powerful and modern web applications. Write less, have cleaner code and your work will bring you joy.

45715.9M1.1k](/packages/nette-application)[rdlowrey/auryn

Auryn is a dependency injector for bootstrapping object-oriented PHP applications.

7242.3M77](/packages/rdlowrey-auryn)[contributte/translation

Symfony/Translation integration for Nette Framework.

781.9M60](/packages/contributte-translation)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
