In a Laravel controller or service, how do contextual attributes such as #[CurrentUser], #[Config] and #[DB] fill a parameter?
answer
- attribute on the parameter, not the class
- Illuminate\Container\Attributes namespace
- #[Config('key', default)] reads config
- #[CurrentUser] asks the guard's user resolver
- custom: implement ContextualAttribute with resolve()
basics
~20 sA contextual attribute on a parameter tells the container how to produce that one value: #[Config('app.timezone')] reads a config key, #[CurrentUser] injects the authenticated user and #[DB('reporting')] a named connection. They work in constructors, controller actions and container-called methods.
solid answer
~30 sLaravel ships attributes in `Illuminate\Container\Attributes` that implement the `ContextualAttribute` contract. When the container meets one on a parameter, it skips type-based resolution and asks the attribute for the value: `#[Config('app.timezone')]` returns that config value (with an optional default), `#[CurrentUser]` returns the guard's current user or `null`, `#[DB('reporting')]` returns that database connection, `#[Storage('ledger-archive')]` a filesystem disk, and `#[Give(DatabaseRepository::class)]` a specific implementation for an interface-typed parameter. They apply to constructors of container-built classes, controller action and route closure parameters, and methods run through `App::call()`. You can write your own by implementing `ContextualAttribute` with a `resolve()` method.
code
php · 21 lines<?php
namespace App\Exports;
use App\Models\User;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Database\Connection;
class LedgerExporter
{
public function __construct(
#[Storage('ledger-archive')] private Filesystem $disk,
#[DB('reporting')] private Connection $db,
#[Config('accounting.fiscal_year_start', '01-01')] private string $fyStart,
#[CurrentUser] private ?User $requestedBy = null,
) {}
}go deeper
Recall the common attributes, #[Config], #[CurrentUser], #[DB] and #[Storage], and that they sit on a parameter and need no provider entry.
Explain that the container checks for ContextualAttribute before type-based resolution, and where that check runs: constructors, actions and call().
Know the guest edge of #[CurrentUser] in actions versus constructors, and decide when a named disk or connection belongs in the class versus in a provider rule.
Weigh visible per-parameter wiring against central provider configuration, and set a convention so a codebase does not mix both styles arbitrarily.
## What a contextual attribute is Normally Laravel's container fills a parameter from its **type**: a class is auto-wired, an interface follows its binding, a scalar takes its default. A **contextual attribute** is a PHP attribute placed on one parameter that overrides that rule for that parameter alone. The container detects any attribute implementing `Illuminate\Contracts\Container\ContextualAttribute`, instantiates it, and uses the value its `resolve()` method returns. The name "contextual" comes from contextual binding: the value depends on *where* it is injected, not just on the type. Attributes keep the decision next to the parameter it concerns. There is no provider entry to find, and two classes can ask for the same type in different ways. ## The built-in attributes All live in `Illuminate\Container\Attributes`: | Attribute | Argument | Injects | |---|---|---| | `#[Config('app.timezone', 'UTC')]` | config key, optional default | the config value | | `#[CurrentUser]` / `#[Authenticated]` | optional guard | the authenticated user, or `null` | | `#[Auth('web')]` | optional guard | the guard object | | `#[DB('reporting')]` / `#[Database]` | optional connection | a database `Connection` | | `#[Storage('ledger-archive')]` | optional disk | a filesystem disk | | `#[Cache('redis')]` | optional store | a cache repository | | `#[Log('daily')]` | optional channel | a logger | | `#[Give(DatabaseRepository::class)]` | class, optional parameters | that class, built by the container | | `#[Tag('reports')]` | tag name | the tagged services, as a lazy iterable | | `#[RouteParameter('invoice')]` | optional name | the matching route parameter | | `#[RequestAttribute('tenant')]` | key | a value from the request's attribute bag | | `#[Context('uuid')]` | key, default | a value from the Context repository | The driver attributes (`Auth`, `Cache`, `DB`, `Log`, `Storage`, `CurrentUser`) accept a string or an enum for the name, and fall back to the default guard, store, connection, channel or disk when given none. ## Where they work - **Constructors** of any class the container builds: controllers, listeners, jobs, services resolved with `make()`. - **Controller actions and route closures**: the router checks each parameter for a contextual attribute before it tries to auto-wire the type. - **Methods run through `App::call()`**, which includes a job's `handle()` and an Artisan command's `handle()`. - **Not** objects created with `new`, because nothing asks the container. ## #[CurrentUser] and guests `#[CurrentUser]` calls the auth manager's user resolver for the given guard, so a guest yields `null`. What happens next depends on where the parameter is: - In a **route closure or controller action**, the `null` is passed straight to the parameter. A non-nullable `User $user` then fails with a PHP `TypeError`. - In a **constructor**, a `null` from the attribute falls through to ordinary resolution, so a non-nullable `User $user` receives a new, unsaved `User` model rather than an error. Both outcomes are surprising. Put the route behind `auth` middleware, or type the parameter `?User $user = null` and handle the guest explicitly. ## Writing your own 1. Create a class marked `#[Attribute(Attribute::TARGET_PARAMETER)]`. 2. Implement `Illuminate\Contracts\Container\ContextualAttribute`. 3. Add a `resolve()` method that receives the attribute instance, the container and the `ReflectionParameter`, and returns the value. 4. Optionally add an `after()` method, which the container calls with the resolved object once it is injected. Alternatively, register a handler for an existing attribute class with `$this->app->whenHasAttribute(MyAttribute::class, fn ($attribute, $app) => ...)`. ## Attributes versus provider rules Every built-in attribute has an older equivalent written in a service provider: `#[Config('key')]` matches `when(X::class)->needs('$param')->giveConfig('key')`, `#[Give(Impl::class)]` matches `needs(Interface::class)->give(Impl::class)`, and `#[Tag('reports')]` is close to `giveTagged('reports')`. The difference is **where the decision lives**: - With an **attribute**, a reader of the class sees exactly what each parameter receives, and there is nothing to keep in sync elsewhere. - With a **provider rule**, the class stays ignorant of the choice, which suits reusable classes and choices that vary by environment. - Attributes cannot see runtime state beyond what their `resolve()` method reads from the container, so anything conditional still belongs in code. For everyday application classes, attributes are the shorter and more discoverable option; for a package or a shared library class, a provider rule keeps the consumer decoupled.
- In Laravel, what does #[Give(DatabaseRepository::class)] on an interface-typed parameter do that a plain type hint cannot?It builds `DatabaseRepository` through the container for that one parameter, regardless of what the interface is bound to globally. It is the attribute form of `when(...)->needs(Interface::class)->give(DatabaseRepository::class)`, and it accepts an optional parameter array that is passed to `make()`.
- In Laravel, what does a custom contextual attribute's resolve() method receive?The attribute instance (carrying its constructor arguments), the container, and the `ReflectionParameter` being filled. It returns the value to inject. Laravel's own `#[Config]` is a one-line `resolve()` that reads the config repository with the attribute's key and default.
saying these in an interview costs you the question
- #[Config('app.timezone')] reads the TIMEZONE variable directly from .env.
- Contextual attributes need a matching binding registered in a service provider.
- #[CurrentUser] redirects guests to the login page.
- Contextual attributes also apply to objects created with new.
- #[DB('reporting')] opens a new PDO connection on every injection.