On this page
Inject attribute
Use #[Inject] when type-based autowiring cannot express the dependency. It forces a concrete implementation, marks
container-owned wiring for tooling, or injects a property or 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, like autowiring but explicit. #[Inject(RedisCache::class)]forces a specific implementation, whatever the global binding says.
In 2.0, prefer importing Gacela\Framework\Attribute\Inject. It extends the container attribute, so both imports work side by
side while an application migrates.
Property and setter injection
Gacela 2.0 also injects properties and one-argument setter methods. Use this for a vendor or framework class 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. For classes your application owns, prefer constructor injection: the dependencies stay visible in the signature.
Readonly, untyped, scalar-typed, and static properties cannot be injected. A promoted property goes through its
constructor parameter and is not injected twice. Property and setter cycles still throw CircularDependencyException.
Resolution order
#[Inject(Target::class)] is third in the container's resolution order: after make() overrides and named contextual
bindings, before defaults, type-based contextual bindings, and global bindings.
Bindings > Resolution order has the full list, including the "defaults win over type
bindings" pitfall.
Inspecting with debug:dependencies
The debug:dependencies command tags #[Inject] parameters, so you can check the 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 lists constructor parameters. --tree follows transitive dependencies through 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 a broken graph instead of throwing, so it stays useful for diagnosis.
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 still resolve through autowiring and bindings.
Symfony integration
In a Symfony app, the gacela-project/symfony-bridge package routes #[Inject] parameters through Gacela's container
with a compiler pass. See the Symfony bundle for setup.