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.