Method injection — container-resolved method parameters
Concept
Method injection is the container's ability to resolve typed parameters of any callable — not just constructors — at the moment you ask the container to call that callable. The entry point is Illuminate\Container\Container::call(callable|string $callback, array $parameters = [], ?string $defaultMethod = null). This is how Laravel resolves controller action parameters, route closure parameters, artisan command handle() method parameters, event listener handle() methods, and any callable you explicitly pass to Container::call().
Internally, call() is implemented in the Illuminate\Container\BoundMethod class. The flow is: BoundMethod::call() inspects the callable using PHP's ReflectionFunction or ReflectionMethod, produces a ReflectionParameter[] list, and for each parameter calls Container::make() on the type hint — falling back to default values or the $parameters override array for primitives. The resolved argument list is then passed to call_user_func_array().
This is distinct from constructor injection in one key way: you trigger it on demand, at call time, for any callable including static methods, closures, invokable objects, and ['ClassName', 'method'] strings. The container does not need to own the object to inject into its methods.
Route closures benefit from method injection automatically because Illuminate\Routing\Router calls $this->container->call($action) when dispatching a matched route. Controllers go through Illuminate\Routing\ControllerDispatcher::dispatch(), which calls Container::call([$controller, $method]) after instantiating the controller via the container. So both route parameters (extracted from the URL) and type-hinted services are merged into the same parameter array before the call.
A nuanced gotcha: when you mix route model bindings (primitive route segments resolved to Eloquent models) with service injections in the same controller method, the container resolves type-hinted classes from its bindings first, then fills remaining parameters from the route's parameter bag. The resolution order is defined in BoundMethod::getDependencies().
| Callable form | Supported by Container::call() |
|---|---|
Closure | Yes |
[object, 'method'] | Yes |
['ClassName', 'method'] | Yes (instantiates class first) |
'ClassName@method' | Yes (Laravel-specific string form) |
| Invokable object | Yes (calls __invoke) |
Code Example
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\InvoiceService;
use App\Services\PdfRenderer;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
// --- 1. Standard controller method injection ---
// The container resolves InvoiceService and PdfRenderer from its bindings.
// The Order is resolved via route model binding (route segment -> Eloquent find).
class OrderController extends Controller
{
public function invoice(
Request $request, // resolved from container (singleton)
Order $order, // resolved via route model binding
InvoiceService $invoice, // resolved from container bindings
PdfRenderer $pdf, // resolved from container bindings
): Response {
return $pdf->render($invoice->generate($order));
}
}
// --- 2. Explicit Container::call() ---
// Useful for calling arbitrary callables with DI outside the request cycle.
$result = app()->call(function (InvoiceService $invoice, PdfRenderer $pdf): string {
return $pdf->render($invoice->generate(Order::find(1)));
});
// --- 3. call() with primitive overrides ---
// The 'format' key matches the parameter name in the callable signature.
$result = app()->call(
[\App\Services\ReportService::class, 'generate'],
['format' => 'csv', 'year' => 2025]
);
// --- 4. call() with a string callable (Laravel's ClassName@method form) ---
$result = app()->call('App\Services\ReportService@generate', ['year' => 2025]);
// --- 5. Calling a Closure registered as a route with mixed injection ---
\Illuminate\Support\Facades\Route::get('/report/{order}', function (
Order $order, // route model binding
InvoiceService $svc, // container injection
): \Illuminate\Http\JsonResponse {
return response()->json($svc->toArray($order));
});
// --- 6. Artisan command handle() — container calls this method ---
class GenerateMonthlyReportCommand extends \Illuminate\Console\Command
{
protected $signature = 'report:monthly {--format=pdf}';
// Container resolves InvoiceService — no constructor declaration needed.
public function handle(InvoiceService $invoice): int
{
$invoice->generateMonthly($this->option('format'));
return self::SUCCESS;
}
}Interview Q&A
Q: How does Laravel resolve method parameters in a controller action, and how does it handle the mix of route parameters and service dependencies?
When the Illuminate\Routing\ControllerDispatcher handles a matched route, it calls Container::call([$controller, $method], $routeParameters). Inside Illuminate\Container\BoundMethod::getDependencies(), each ReflectionParameter of the method is inspected. If the parameter has a class type hint, the container calls make() on that type and uses the result. If the parameter has no type hint or a primitive type, the method looks for a matching key in the $routeParameters array by parameter name (after route model binding has resolved URL segments to Eloquent models). This is why a controller can simultaneously receive a type-hinted service and a route-bound Order model — they come from different sources but are merged by BoundMethod before call_user_func_array() is invoked.
Q: How does Container::call() differ from just calling Container::make() and then calling the method manually?
Container::make() only resolves a class to an instance — it does not inject dependencies into subsequent method calls on that instance. Container::call() wraps the entire invocation: it resolves the callable, inspects the method's parameters via Reflection, resolves each typed dependency from the container, merges runtime overrides, and then calls the method with the fully-resolved argument list. Doing this manually would require you to replicate BoundMethod::getDependencies() yourself. The practical difference is that call() lets you add new service dependencies to a controller method or Artisan command handle() method without touching the constructor — Laravel will auto-discover and inject them.
Q: What happens when a method parameter cannot be resolved — no type hint, no container binding, and no value in the override array?
If ReflectionParameter::isDefaultValueAvailable() is true, the default value is used. If not, Illuminate\Container\Container throws an Illuminate\Contracts\Container\BindingResolutionException with a message indicating that the dependency could not be resolved. This is the same exception thrown when constructor auto-resolution fails on a primitive — the container cannot infer what to pass for an untyped or primitively-typed parameter without an explicit override. In practice this surfaces as a 500 error if it happens in a controller, so always type-hint service dependencies and provide route parameter names that match your method parameter names when relying on route-to-method binding.