Gacela
Documentation version: 2.2.0
Colour theme

Docs Configuration

On this page

Inject attribute

Use #[Inject] when ordinary type-based autowiring cannot express the dependency: to force a concrete implementation, mark container-owned wiring for tooling, or inject a property/setter on a class whose constructor you cannot change.

Quick start

use Gacela\Framework\Attribute\Inject;

final class CatalogService
{
    public function __construct(
        #[Inject] private readonly LoggerInterface $logger,
        #[Inject(RedisCache::class)] private readonly CacheInterface $cache,
    ) {}
}
  • A bare #[Inject] resolves the parameter by its type (same as autowiring, but explicit).
  • #[Inject(RedisCache::class)] forces a specific implementation regardless of the global binding.

Gacela\Framework\Attribute\Inject is the preferred 2.0 import. It extends the container attribute, so both imports can coexist while an application migrates.

Property and setter injection

Gacela 2.0 also supports properties and one-argument setter methods. This is useful for vendor or framework classes whose constructor is fixed:

final class CatalogController extends VendorController
{
    #[Inject]
    private LoggerInterface $logger;

    #[Inject(RedisCache::class)]
    public function setCache(CacheInterface $cache): void
    {
        $this->cache = $cache;
    }
}

Private, protected, and inherited properties work. Constructor injection remains preferable for application-owned classes because dependencies stay visible in the signature.

Readonly, untyped, scalar-typed, and static properties cannot be injected. A promoted property is handled through its constructor parameter and is not injected twice. Property/setter cycles still throw CircularDependencyException.

Resolution order

#[Inject(Target::class)] sits third in the container's general resolution order, after make() overrides and named contextual bindings, and before defaults, type-based contextual bindings, and global bindings. The full ordered list, including the "defaults win over type bindings" pitfall, is Bindings > Resolution order.

Inspecting with debug:dependencies

The debug:dependencies command tags #[Inject] parameters so you can verify wiring at a glance:

vendor/bin/gacela debug:dependencies App\\Catalog\\CatalogService --tree
✓ $logger  LoggerInterface   (inject)
✓ $cache   CacheInterface    (inject -> App\Cache\RedisCache)

The one-level view describes constructor parameters. --tree follows transitive dependencies using the container's applied bindings and contextual bindings. Each node is marked binding, instance, autowired, or unresolvable; cycles are marked and cut. The command reports broken graphs instead of throwing so it remains useful as a diagnostic.

When to use #[Inject] vs bindings

Scenario Approach
Global default for an interface addBinding() in gacela.php
One class needs a different implementation #[Inject(Concrete::class)] on the parameter
Multiple classes need the same override when()->needs()->give() contextual binding
Constructor is controlled by a vendor/framework #[Inject] on a property or setter

#[Inject] is opt-in. Classes without it continue to resolve through ordinary autowiring and bindings.

Symfony integration

In Symfony apps, the gacela-project/symfony-bridge package routes #[Inject] parameters through Gacela's container via a compiler pass. See the Symfony bundle for setup.