In a Laravel accounting app, how would you give LedgerExporter and PayrollExporter different storage disks for the same Filesystem type hint using when()->needs()->give()?
answer
- binding keyed by the consuming class
- when(Consumer)->needs(Abstract)->give(...)
- needs('$name') targets a scalar parameter
- giveConfig() and giveTagged() shortcuts
- only the named class's direct dependencies
basics
~10 sRegister $this->app->when(LedgerExporter::class)->needs(Filesystem::class)->give(fn () => Storage::disk('ledger-archive')) and a second rule for PayrollExporter. The container applies each rule only while building that class, so each exporter gets its own disk.
solid answer
~40 sA contextual binding is keyed by the class being built. `when()` takes one class or an array, `needs()` names the abstract, and `give()` takes a closure, a class name or, for a variadic parameter, an array. So `when(LedgerExporter::class)->needs(Filesystem::class)->give(fn () => Storage::disk('ledger-archive'))` plus a matching rule for `PayrollExporter` hands each exporter its own disk, while every other class still gets the default disk. `needs('$fiscalYearStart')` targets a scalar parameter by name, and `giveConfig('accounting.fiscal_year_start')` and `giveTagged('reports')` are shortcuts. A rule applies only to the named class's own dependencies and only when the container builds that class; `new LedgerExporter(...)` bypasses it. A `give()` closure runs each time the exporter is built, and its result is not cached under the abstract.
code
php · 27 lines<?php
namespace App\Providers;
use App\Exports\LedgerExporter;
use App\Exports\PayrollExporter;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->when(LedgerExporter::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('ledger-archive'));
$this->app->when(PayrollExporter::class)
->needs(Filesystem::class)
->give(fn () => Storage::disk('payroll-secure'));
$this->app->when([LedgerExporter::class, PayrollExporter::class])
->needs('$fiscalYearStart')
->giveConfig('accounting.fiscal_year_start', '01-01');
}
}go deeper
Recall the three-step chain, when() for the consumer, needs() for the type, give() for the value, and that other classes are unaffected.
Explain primitives with needs('$name'), the giveConfig() and giveTagged() shortcuts, and that a give() closure's result is never cached under the abstract.
Show that rules key on the class being built and do not cascade, spot consumers created with new that silently miss their rule, and choose between provider rules and attributes.
Decide where environment-specific wiring belongs across a large app, provider rules versus attributes versus separate services, so a new engineer can find why a class got its dependency.
## The problem contextual binding solves An accounting app has two exporters. `LedgerExporter` writes month-end ledgers to an archive disk; `PayrollExporter` writes payslips to a locked-down disk. Both type-hint `Illuminate\Contracts\Filesystem\Filesystem`, because neither should care *which* disk it writes to. A normal binding cannot help: `bind(Filesystem::class, ...)` is **global**, so every consumer would get the same disk. Out of the box the contract resolves to the default disk for everyone. A **contextual binding** answers a narrower question: *when the container is building this class, and it needs that type, what should it give?* ## The fluent API | Call | Accepts | Meaning | |---|---|---| | `when($concrete)` | a class name or an array of them | the consumer(s) the rule applies to | | `needs($abstract)` | a class/interface name, or `'$paramName'` | the dependency being overridden | | `give($implementation)` | a closure, a class name, an array, or a plain value for primitives | what to inject | | `giveConfig($key, $default = null)` | a config key | a scalar read from config | | `giveTagged($tag)` | a tag name | all services with that tag, as an array | Rules are registered in a service provider's `register()` method, alongside ordinary bindings. ## How the container applies a rule While building a class, the container keeps a **build stack**. For each constructor parameter it: 1. Looks up rules for the class at the **top of the stack**, the one whose constructor it is filling, keyed by the needed abstract (or `'$name'` for a scalar). 2. If a rule gives a **closure**, calls it with the container and uses the return value. 3. If a rule gives a **class name**, resolves that class with `make()`, so that class's own lifetime applies. 4. Treats the whole thing as a one-off build: the result is **never cached** under the abstract, even if the abstract is bound as a singleton. 5. Falls back to normal resolution when no rule matches. Step 1 has a consequence people miss: a rule does **not cascade**. If `LedgerExporter` depends on a `CsvWriter` that also needs `Filesystem`, the `CsvWriter` gets the default disk unless it has its own rule. ## Primitives, config and tags - `needs('$fiscalYearStart')->give('04-01')` fills a scalar parameter by its name, including the `$`. - `give(fn ($app) => ...)` for a primitive is called lazily with the container. - `giveConfig('accounting.fiscal_year_start', '01-01')` reads the value from config when the class is built. - `giveTagged('reports')` resolves every service tagged `reports` and converts the lazy result to a plain **array**. - For a variadic `Filter ...$filters`, `needs(Filter::class)->give([NullFilter::class, TooLongFilter::class])` builds each listed class. ## Limits and traps - The rule only fires when **the container** builds the consumer. `new LedgerExporter(...)` or a hand-written factory bypasses it. - A `give()` closure runs on **every** build of the consumer; if the consumer is not a singleton, that means every resolution. - Aliases are handled: the `Filesystem` contract is an alias of the default-disk binding, and rules keyed on either name match. - The same override can be written on the consumer itself with `#[Storage('ledger-archive')]` or `#[Give(SomeClass::class)]`. The provider form keeps the consumer ignorant of the choice; the attribute form makes it visible in the class. ## Rules in method injection Contextual rules keyed on a class also apply when the container calls one of that class's methods through `App::call()`, because `call()` puts the method's class on the build stack while it resolves class-typed parameters. A queued `ExportLedgerJob` whose `handle(Filesystem $disk)` asks for a disk therefore receives the disk from a `when(ExportLedgerJob::class)->needs(Filesystem::class)` rule. Scalar rules written with `needs('$name')` are different: `call()` fills scalar parameters only from the values passed to it and from defaults, so a primitive rule reaches constructors, not methods. ## Choosing between the two styles Use the **provider rule** when the consumer is reusable, the choice may vary by environment, or a package author should decide. Use the **attribute** when the choice is intrinsic to the class and readers benefit from seeing it in the constructor.
- In Laravel, does when(LedgerExporter::class)->needs(Filesystem::class) also affect a CsvWriter that LedgerExporter depends on?No. The container looks up contextual rules for the class whose constructor it is currently filling. When it builds `CsvWriter`, `CsvWriter` is that class, so it gets the default disk unless it has its own rule. Contextual bindings do not cascade down the object graph.
- In Laravel, how does giveTagged('reports') differ from injecting the same tag with #[Tag('reports')]?`giveTagged()` calls `tagged()` and converts the result with `iterator_to_array()`, so the consumer receives a plain array of built services. `#[Tag('reports')]` returns `tagged()` as-is, a lazy iterable that builds each service during iteration. Type the parameter `array` for the first and `iterable` for the second.
- In Laravel, when should the disk choice live in a provider rule rather than in a #[Storage] attribute on the exporter?A provider rule keeps the exporter unaware of which disk it writes to, so the choice can change per environment or in one place. `#[Storage('ledger-archive')]` is shorter and visible in the class, but hard-codes the disk name into the consumer. Attributes suit app classes where visibility helps; provider rules suit reusable classes whose users choose.
A contextual binding works like a mailroom rule: parcels of the same type go to the archive when addressed to the ledger team and to the locked cabinet when addressed to payroll. Anyone not named on a rule still gets the default shelf.
saying these in an interview costs you the question
- when()->needs()->give() changes what every class receives for that interface.
- A contextual rule also applies to dependencies of the named class's dependencies.
- Contextual bindings still apply when the class is created with new.
- needs() only accepts class or interface names, never scalar parameters.
- A give() closure runs once and its result is cached like a singleton.