In Laravel, how do you give a middleware access to a service such as a kitchen-status checker, and why doesn't type-hinting it in handle() work?
answer
- the container builds every middleware
- constructor type-hints are auto-wired
- handle() is called directly, not via call()
- extra handle() arguments are route strings
- no request state in singleton-bound services
basics
~20 sType-hint the service in the middleware's constructor; Laravel resolves middleware through the service container, which auto-wires constructor dependencies. handle() is invoked directly with the request, $next and any route parameters, so it gets no method injection.
solid answer
~40 sLaravel's pipeline resolves each middleware class with `$container->make()`, so a constructor such as `public function __construct(private KitchenStatus $kitchen) {}` is auto-wired, including interfaces you have bound in a service provider. `handle()` is different: the pipeline calls it itself with `$request`, `$next` and the strings parsed from the route, like `role:admin`. There is no method injection, so an extra `KitchenStatus $kitchen` parameter in `handle()` would receive a route parameter string or cause an `ArgumentCountError`. Two cautions: a fresh middleware instance is built for each run unless you bind it as a singleton, and you should not inject the `Request` or per-user data into services that are singletons, because under long-lived workers they would outlive the request.
code
php · 22 lines<?php
namespace App\Http\Middleware;
use App\Services\KitchenStatus;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureKitchenIsOpen
{
public function __construct(private KitchenStatus $kitchen) {}
public function handle(Request $request, Closure $next): Response
{
if (! $this->kitchen->acceptingOrders()) {
return response()->json(['message' => 'Kitchen closed'], 503);
}
return $next($request);
}
}go deeper
Recall that dependencies go in the middleware constructor and the container fills them in.
Explain why handle() receives only the request, $next and route string parameters, and what error an extra service type-hint produces.
Reason about instance lifetime: fresh per run by default, singletons shared, and the leak risk of request data in long-lived workers.
Keep middleware thin by pushing rules into injectable services, so the same rule serves middleware, jobs and console commands.
## How Laravel builds a middleware Middleware classes are never created with `new` in application code. When the request passes through a pipeline, `Illuminate\Pipeline\Pipeline` takes each middleware name, splits off any parameters, and calls the **service container** to build the class: ```php $pipe = $this->getContainer()->make($name); ``` The container inspects the constructor and resolves each type-hinted dependency, recursively. This is called **auto-wiring**. It is the same mechanism that builds controllers, jobs and listeners. ## Constructor injection in practice A food-ordering app wants to stop checkout requests when the kitchen is closed. The rule lives in a `KitchenStatus` service: ```php class EnsureKitchenIsOpen { public function __construct(private KitchenStatus $kitchen) {} public function handle(Request $request, Closure $next): Response { if (! $this->kitchen->acceptingOrders()) { return response()->json(['message' => 'Kitchen closed'], 503); } return $next($request); } } ``` What works here: - **Concrete classes** with resolvable constructors are built automatically. - **Interfaces** work once a service provider binds them, for example `$this->app->bind(KitchenStatus::class, RedisKitchenStatus::class)`. - **Scalars** such as a threshold cannot be auto-wired; read them from `config()` in the constructor, or bind the middleware with a closure that supplies them. ## Why handle() gets no injection The pipeline invokes `handle()` itself, with a fixed argument list: 1. The request. 2. The `$next` closure. 3. Every parameter parsed from the middleware string after the colon, split on commas, as strings. There is no call through the container's `call()` method, so no method injection happens. The consequences of type-hinting a service in `handle()`: | Signature | Route definition | Result | |---|---|---| | `handle($request, $next, KitchenStatus $k)` | `->middleware(EnsureKitchenIsOpen::class)` | `ArgumentCountError`, nothing is passed | | `handle($request, $next, KitchenStatus $k)` | `->middleware('kitchen:main')` | `TypeError`, the string `'main'` is passed | | `__construct(KitchenStatus $k)` + `handle($request, $next)` | either | Works | Controllers behave differently: their action methods do receive method injection, which is why developers sometimes expect the same from middleware. ## Supplying configuration through a binding When the middleware needs a scalar alongside a service, a closure binding in a service provider builds it explicitly: ```php $this->app->bind(EnsureKitchenIsOpen::class, fn ($app) => new EnsureKitchenIsOpen( $app->make(KitchenStatus::class), config('ordering.last_order_time'), )); ``` The pipeline still calls `make()`, so it now gets this pre-configured instance. For most cases reading `config()` inside the constructor is simpler and just as testable. ## Instance lifetime Because the container builds the class on each pass through the pipeline, a middleware is **not** a long-lived object by default: - Each request gets a fresh instance, and its constructor runs again. - When the kernel calls a `terminate()` method after the response, it resolves the class again, so it gets **another** fresh instance unless the middleware is bound with `$this->app->singleton()`. - Constructor work should therefore be cheap: resolve services, read config, nothing that performs I/O. ## What not to inject The Octane documentation warns against injecting the **request**, the application container or the config repository into objects bound as singletons: if such a singleton is resolved while the worker boots, it keeps holding that object for every later request the worker serves. Inside the middleware, always use the `$request` argument of `handle()` rather than storing a request in a service's constructor. ## Common mistakes - Resolving services inside `handle()` with `app(KitchenStatus::class)` everywhere, which works but hides the dependency and makes the class harder to test. - Declaring a constructor parameter the container cannot build, such as an unbound interface or a scalar, which fails with a `BindingResolutionException` on the first request that reaches the middleware. - Doing I/O in the constructor, which runs again for every request and again before `terminate()`. ## Testing the middleware Constructor injection is also what makes middleware testable in isolation: - Instantiate it directly with a stub `KitchenStatus` and call `handle()` with a fake request and a closure for `$next`. - Or swap the binding in the container for an HTTP test, so the real route runs with a closed kitchen.
- How would you give the kitchen middleware a configurable closing-time threshold?Scalars cannot be auto-wired, so read it in the constructor with `config('ordering.closing_time')`, or pass it as a route parameter such as `kitchen:22` and accept `string $closesAt` after `$next`. Route parameters arrive as strings, so cast them before comparing.
- How do you unit-test the kitchen middleware without booting the HTTP stack?Construct it directly with a stub `KitchenStatus`, build a request with `Request::create('/checkout', 'POST')`, and call `handle($request, fn ($r) => response('ok'))`. Assert a 503 when the stub reports the kitchen closed, and that the closure's response comes back when it is open. Constructor injection is what makes this possible.
saying these in an interview costs you the question
- Type-hints services in handle() expecting method injection
- Believes middleware must be built manually with new in bootstrap/app.php
- Assumes one middleware instance is reused for every request by default
- Injects the Request into a singleton service's constructor
- Performs database queries inside the middleware constructor