In Laravel, how do the #[Bind], #[Singleton] and #[Scoped] attributes declare a binding on the interface itself, and when does a provider binding override them?
answer
- attributes read at first resolution
- #[Bind(Impl::class, environments: [...])]
- environment match beats the '*' default
- lifetime from #[Singleton] or #[Scoped]
- bindings table checked before attributes
basics
~20 s#[Bind(RedisEventPusher::class)] on an interface tells the container which class to build when the interface is resolved, with no provider code; #[Singleton] or #[Scoped] beside it sets the lifetime. An explicit provider binding for the interface is checked first and wins.
solid answer
~40 sWhen the container resolves a type with no registered binding, it reads the type's `#[Bind]` and `#[BindWhen]` attributes in declaration order. `#[Bind(Concrete::class)]` applies in every environment by default (`environments: ['*']`); `#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]` applies only when the app environment matches, and a matching environment-specific attribute beats the wildcard wherever it is declared. The chosen class is then registered as an ordinary binding: shared with `#[Singleton]`, scoped with `#[Scoped]`, non-shared otherwise. `#[Singleton]` or `#[Scoped]` on a concrete class also makes its auto-wired resolutions shared. Because the container checks its bindings table before it looks at attributes, a provider's `bind()` or `singleton()` for the interface overrides the attribute.
code
php · 16 lines<?php
namespace App\Contracts;
use App\Services\FakeEventPusher;
use App\Services\RedisEventPusher;
use Illuminate\Container\Attributes\Bind;
use Illuminate\Container\Attributes\Singleton;
#[Bind(RedisEventPusher::class)]
#[Bind(FakeEventPusher::class, environments: ['local', 'testing'])]
#[Singleton]
interface EventPusher
{
public function push(string $event, array $payload): void;
}go deeper
Recall that #[Bind] on an interface names its implementation and #[Singleton] or #[Scoped] sets the lifetime, with no provider code.
Explain declaration order, environment matching against the wildcard fallback, and that an explicit provider binding is checked first and wins.
Judge when an interface naming its own implementation is acceptable coupling, and how to keep test and local fakes from leaking into production wiring.
Set a rule for where bindings live, attributes on app interfaces versus providers for package and cross-module seams, so wiring stays discoverable.
## Declaring a binding where the abstraction lives Traditionally, an interface-to-class binding lives in a service provider: `$this->app->bind(EventPusher::class, RedisEventPusher::class)`. Laravel also lets the **interface declare it** with attributes from `Illuminate\Container\Attributes`: - `#[Bind(RedisEventPusher::class)]` names the implementation; - `#[Singleton]` or `#[Scoped]` names the lifetime. No provider entry is needed; the first time something resolves `EventPusher`, the container reads the attributes and registers the binding itself. ## How the container reads the attributes For an abstract such as `EventPusher`, `make()` works through these steps: 1. Look in the **bindings table**. If a provider registered anything for `EventPusher`, use it and stop; attributes are never read. 2. Otherwise reflect on `EventPusher` and walk its `#[Bind]` and `#[BindWhen]` attributes **in declaration order**. 3. A `#[BindWhen]` whose condition returns true wins immediately. 4. A `#[Bind]` whose environment list matches the current environment wins immediately. 5. A `#[Bind]` with the default `['*']` is remembered as a **fallback** and the walk continues. 6. If something won, register it with `scoped()`, `singleton()` or `bind()` depending on the lifetime attribute, and resolve it. 7. If nothing won, the interface is not instantiable and resolution fails as usual. Because the winner is registered as a normal binding, the attributes are not read again once something has won; later resolutions go straight to the bindings table. ## Environments - `environments` accepts a string, an array or a backed enum; the default is `['*']`. - An empty list throws `InvalidArgumentException` when the attribute is instantiated. - The comparison uses the application environment (`APP_ENV`, via `config('app.env')`), which the framework hands to the container when it loads configuration. - An environment-specific match wins over a wildcard even when the wildcard is declared first, so the common pattern reads naturally: a production default, then fakes for `local` and `testing`. ## Lifetimes | Attributes on the type | Registered as | Repeated resolution returns | |---|---|---| | `#[Bind(X::class)]` alone | `bind()` | a new `X` each time | | `#[Bind(X::class)]` + `#[Singleton]` | `singleton()` | the same `X` | | `#[Bind(X::class)]` + `#[Scoped]` | `scoped()` | the same `X` until the scope is flushed | | `#[Singleton]` on a concrete class | shared auto-wiring | the same instance | The lifetime attributes also work without `#[Bind]` on a concrete class: `#[Singleton] class RateTable` makes every auto-wired `RateTable` the same object. ## Provider bindings versus attributes - **Precedence.** The bindings table is consulted first, so any explicit `bind()`, `singleton()` or `instance()` for the interface overrides its attributes, including the lifetime. - **Discoverability.** Attributes put the answer where a reader looks first, on the interface. Provider bindings put all wiring in one place. - **Coupling.** An interface that names its implementation now depends on that class. For an application's own interfaces that is often acceptable; for a package interface meant to be implemented elsewhere, a provider binding keeps the abstraction clean. - **Conditional wiring** beyond environments belongs to `#[BindWhen]`, which is newer and has its own caveats. ## Common mistakes - **Expecting the attribute to win.** A leftover `bind()` in a provider silently overrides the interface's attributes, including `#[Singleton]`. When an attribute seems ignored, search the providers first. - **Forgetting the fallback.** Declaring only environment-specific `#[Bind]` attributes leaves every other environment, often production, with an unresolvable interface. - **Putting `#[Bind]` on a class.** The attribute is read when the annotated type is resolved, so it belongs on the interface or abstract class that consumers type-hint, not on the implementation. - **Assuming eager construction.** None of these attributes builds anything at boot; the implementation is created on first resolution, and a bad class name only fails then. - **Mixing styles for one interface.** Choose attributes or a provider binding per interface; having both makes the effective wiring depend on which one a reader happens to find.
- In Laravel, if an interface has #[Bind] attributes but none matches the environment and there is no wildcard, what happens?No concrete is chosen, so the container treats the interface as unbound and resolution fails with the usual 'is not instantiable' `BindingResolutionException`. Adding a `#[Bind]` with the default `['*']` gives every other environment a fallback.
- In Laravel, does #[Singleton] on an interface still apply when a provider binds that interface with plain bind()?No. The provider binding is found first and it is non-shared, and the lifetime attribute on an interface is only consulted when the container registers the binding from attributes. Use `singleton()` in the provider, or drop the provider binding and rely on the attributes.
saying these in an interview costs you the question
- #[Bind] attributes are ignored unless a provider also registers the interface.
- The first #[Bind] declared always wins, whatever its environments.
- An attribute binding overrides a provider's bind() for the same interface.
- #[Singleton] builds the bound class eagerly when the app boots.
- The container re-reads #[Bind] attributes on every resolution.