0

Route model binding — resolving models from route parameters

Advanced5 min read·fw-03-006
sql

Concept

Route model binding automatically resolves Eloquent (or any) model instances from route parameters. Instead of $user = User::findOrFail($id) in every controller, the router does it for you.

Implicit binding: When a route parameter name matches a controller method parameter name AND the type-hint is a model class, Laravel (and custom frameworks) resolve the model automatically.

  • Route: /users/{user} → controller parameter User $user → resolves User::find($user_param).

Explicit binding: Route::model('user', User::class) — explicitly declare that {user} should resolve to a User model.

Custom key: {user:email} — use a different field: User::where('email', $value)->firstOrFail().

Custom resolution logic: Route::bind('user', fn($value) => User::findOrFail($value)).

Implementation: During dispatch, before calling the controller action, the dispatcher iterates the action's parameters. For each parameter with a model type-hint that matches a route param name, it calls the resolution callback.

404 on missing model: When the model isn't found, throw a ModelNotFoundException which the exception handler converts to a 404 response.

Why this matters: It standardizes model resolution, reduces boilerplate, and ensures consistent 404 handling when models don't exist.

Code Example

php
<?php
namespace Framework\Routing;

use Framework\Container\Container;
use Framework\Http\Request;
use Framework\Http\Response;

class Dispatcher
{
    private array $modelBindings = []; // ['param_name' => callable]

    public function __construct(private readonly Container $container) {}

    // Register a model binding
    public function model(string $param, string $modelClass): void
    {
        $this->bind($param, fn($value) => $modelClass::findOrFail($value));
    }

    public function bind(string $param, callable $resolver): void
    {
        $this->modelBindings[$param] = $resolver;
    }

    public function dispatch(MatchedRoute $match, Request $request): Response
    {
        $params = $this->resolveBindings($match->params);
        // ... then dispatch with resolved $params
        return $this->callAction($match->route->action, $request, $params);
    }

    private function resolveBindings(array $params): array
    {
        return array_map(function($value, $key) {
            if (isset($this->modelBindings[$key])) {
                return ($this->modelBindings[$key])($value);
            }
            return $value;
        }, $params, array_keys($params));
    }

    // For implicit binding — during parameter resolution
    private function resolveArgs(\ReflectionFunctionAbstract $fn, Request $request, array $params): array
    {
        $args = [];
        foreach ($fn->getParameters() as $param) {
            $typeName = $param->getType()?->getName();
            $paramName = $param->getName();

            // Type-hinted and name matches a route param → implicit model binding
            if ($typeName && isset($params[$paramName]) && class_exists($typeName)) {
                $reflClass = new \ReflectionClass($typeName);
                if ($reflClass->hasMethod('find')) {
                    // It's a model — resolve it
                    $args[] = $typeName::findOrFail($params[$paramName]);
                    continue;
                }
            }

            $args[] = $params[$paramName] ?? $this->container->make($typeName ?? 'null');
        }
        return $args;
    }
}

// Usage
// Route: GET /users/{user}
// Controller: public function show(User $user): Response
// → Dispatcher auto-resolves User::findOrFail($routeParam)

// Custom binding
$dispatcher->model('post', Post::class);
// Route: GET /posts/{post}
// → Post::findOrFail($routeParam) automatically

// Custom logic
$dispatcher->bind('post', fn($slug) => Post::where('slug', $slug)->firstOrFail());
// Route: GET /posts/{post}  → param is the slug, not an ID