Route model binding — resolving models from route parameters
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 parameterUser $user→ resolvesUser::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
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