0

Method injection — container-resolved method parameters

Intermediate5 min read·lv-02-008
laravel-src

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 formSupported by Container::call()
ClosureYes
[object, 'method']Yes
['ClassName', 'method']Yes (instantiates class first)
'ClassName@method'Yes (Laravel-specific string form)
Invokable objectYes (calls __invoke)

Code Example

php
<?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.