Routing и Middleware -- основа HTTP-обработки в Laravel. Вопросы на экзамене часто проверяют нюансы, которые разработчики упускают в повседневной работе.
Route Model Binding
Implicit Binding
<?php
use App\Models\User;
// TRAP 1: Parameter name MUST match variable name
Route::get('/users/{user}', function (User $user) {
return $user;
});
// {user} matches $user -> works!
// WRONG: parameter name doesn't match
Route::get('/users/{id}', function (User $user) {
return $user;
});
// {id} does NOT match $user -> $user will NOT be auto-resolved!
// Instead, $user will be injected from container (empty model)
Экзаменационный вопрос: "Будет ли работать route model binding, если параметр маршрута {id}, а переменная User $user?"
Ответ: Нет. Имя параметра маршрута ДОЛЖНО совпадать с именем переменной (без $). {user} -> $user.
Custom Key для binding
<?php
// Default: binds by primary key (id)
Route::get('/users/{user}', fn (User $user) => $user);
// /users/1 -> User::find(1)
// Custom key in route:
Route::get('/users/{user:slug}', fn (User $user) => $user);
// /users/john-doe -> User::where('slug', 'john-doe')->firstOrFail()
// Custom key in model (global):
class User extends Model
{
public function getRouteKeyName(): string
{
return 'slug'; // Always bind by slug
}
}
// Now {user} always resolves by slug
TRAP 2: Если определён getRouteKeyName() в модели, он применяется ВЕЗДЕ. Чтобы в отдельных маршрутах использовать id, нужно явно указать {user:id}.
Scoped Binding (вложенные ресурсы)
<?php
// TRAP 3: Scoped binding with nested resources
Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
return $post;
});
// When you specify key (:slug), Laravel automatically scopes:
// Post::where('slug', $slug)->where('user_id', $user->id)->firstOrFail()
// Without custom key, scoping is NOT automatic:
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
return $post;
});
// Post::find($postId) -> NO scoping to user!
// To force scoping WITHOUT custom key:
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
return $post;
})->scopeBindings();
// Or globally in route group:
Route::scopeBindings()->group(function () {
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
return $post;
});
});
Экзаменационный вопрос: "Как работает scoped binding в nested resources?"
Ответ: Scoped binding автоматически фильтрует дочернюю модель по foreign key родительской ТОЛЬКО когда указан custom key (:slug). Без custom key нужен ->scopeBindings() или Route::scopeBindings()->group().
Binding и Soft Deletes
<?php
// TRAP 4: Soft deleted models are NOT found by default
Route::get('/posts/{post}', function (Post $post) {
return $post;
});
// /posts/1 -> 404 if post is soft deleted!
// To include soft deleted:
Route::get('/posts/{post}', function (Post $post) {
return $post;
})->withTrashed();
// Or in model:
class Post extends Model
{
public function resolveRouteBinding($value, $field = null): ?Model
{
return $this->where($field ?? $this->getRouteKeyName(), $value)
->withTrashed()
->first();
}
}
Explicit Binding
<?php
// In RouteServiceProvider or AppServiceProvider boot():
use Illuminate\Support\Facades\Route;
public function boot(): void
{
Route::model('user', User::class);
// Custom resolution logic:
Route::bind('user', function (string $value) {
return User::where('slug', $value)
->orWhere('id', $value)
->firstOrFail();
});
}
Enum Binding
<?php
// TRAP 5: Enums in routes (Laravel 11+)
enum Category: string
{
case Fruit = 'fruit';
case Vegetable = 'vegetable';
}
Route::get('/foods/{category}', function (Category $category) {
return $category->value;
});
// /foods/fruit -> works
// /foods/meat -> 404 (not a valid enum value)
Middleware
Middleware Priority
<?php
// TRAP 6: Middleware execution order
// Global middleware runs in ORDER they are registered
// Route middleware runs in ORDER they are applied
// In bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) {
$middleware->priority([
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
]);
})
// Priority matters! Session MUST start before Auth checks
// because Auth reads session for user data
Middleware Parameters
<?php
// TRAP 7: Passing parameters to middleware
Route::get('/admin', function () {
// ...
})->middleware('role:admin');
// Multiple parameters:
Route::get('/admin', function () {
// ...
})->middleware('role:admin,editor');
// In middleware class:
class RoleMiddleware
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!in_array($request->user()->role, $roles)) {
abort(403);
}
return $next($request);
}
}
// Registration in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => RoleMiddleware::class,
]);
})
Terminable Middleware
<?php
// TRAP 8: terminate() runs AFTER response is sent to client
class LogRequestMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// Before request (going IN)
$request->start_time = microtime(true);
$response = $next($request);
// After request (going OUT, but response not sent yet)
return $response;
}
// Called AFTER response is sent to browser
public function terminate(Request $request, Response $response): void
{
$duration = microtime(true) - $request->start_time;
logger()->info("Request took {$duration}s");
}
}
Экзаменационный вопрос: "Когда вызывается метод terminate() middleware?"
Ответ: ПОСЛЕ того, как ответ отправлен клиенту. Это не задерживает ответ. Полезно для логирования, очистки, аналитики.
Middleware Groups
<?php
// TRAP 9: Middleware groups vs individual middleware
// bootstrap/app.php (Laravel 11+):
->withMiddleware(function (Middleware $middleware) {
// Append to existing 'web' group
$middleware->web(append: [
\App\Http\Middleware\LocaleMiddleware::class,
]);
// Prepend to existing 'api' group
$middleware->api(prepend: [
\App\Http\Middleware\ApiVersionMiddleware::class,
]);
// Remove from group
$middleware->web(remove: [
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
]);
})
Excluding Middleware
<?php
// TRAP 10: withoutMiddleware on specific routes
Route::middleware('auth')->group(function () {
Route::get('/dashboard', DashboardController::class);
// This route does NOT require auth
Route::get('/public-stats', StatsController::class)
->withoutMiddleware('auth');
});
// Works with multiple middleware:
Route::get('/webhook', WebhookController::class)
->withoutMiddleware(['auth', 'csrf']);
Rate Limiting
Определение rate limiter
<?php
// In AppServiceProvider boot() or bootstrap/app.php:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Multiple limits:
RateLimiter::for('uploads', function (Request $request) {
return [
Limit::perMinute(10)->by($request->user()->id), // 10 per minute per user
Limit::perDay(100)->by($request->user()->id), // 100 per day per user
];
});
// No limit for specific users:
RateLimiter::for('api', function (Request $request) {
return $request->user()?->isPremium()
? Limit::none()
: Limit::perMinute(60)->by($request->ip());
});
// Custom response on limit:
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)
->by($request->ip())
->response(function (Request $request, array $headers) {
return response('Rate limit exceeded', 429, $headers);
});
});
Применение rate limiting
<?php
// Apply to route
Route::middleware('throttle:api')->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});
// TRAP 11: throttle:X,Y syntax
Route::middleware('throttle:60,1')->get('/test', fn () => 'ok');
// 60 requests per 1 minute
// Using named limiter:
Route::middleware('throttle:uploads')->post('/upload', UploadController::class);
Route Groups
Group attributes
<?php
// TRAP 12: Group attributes are MERGED, not replaced
Route::prefix('admin')
->middleware(['auth', 'admin'])
->name('admin.')
->group(function () {
// This route:
Route::get('/users', UserController::class)->name('users');
// Full URL: /admin/users
// Full name: admin.users
// Middleware: auth, admin
});
// Nested groups:
Route::prefix('api/v1')
->middleware('api')
->group(function () {
Route::prefix('admin')
->middleware('admin')
->group(function () {
Route::get('/users', fn () => 'users');
// URL: /api/v1/admin/users
// Middleware: api + admin (merged!)
});
});
Controller group
<?php
// TRAP 13: Controller group (Laravel 11+)
Route::controller(OrderController::class)->group(function () {
Route::get('/orders', 'index');
Route::post('/orders', 'store');
Route::get('/orders/{order}', 'show');
});
// DON'T specify class in the group:
// Route::get('/orders', [OrderController::class, 'index']); // REDUNDANT
Fallback Routes
<?php
// TRAP 14: Fallback route catches ALL unmatched routes
Route::fallback(function () {
return response()->json(['error' => 'Not Found'], 404);
});
// TRAP: Fallback must be the LAST route defined
// If defined before other routes, those routes still work
// But any non-matching URL hits fallback instead of 404 exception
// Fallback in API:
Route::prefix('api')->group(function () {
Route::get('/users', UserController::class);
// API-specific fallback
Route::fallback(function () {
return response()->json(['error' => 'API endpoint not found'], 404);
});
});
Redirect Routes
<?php
// TRAP 15: redirect() route uses 302 by default
Route::redirect('/old', '/new'); // 302 (temporary)
Route::redirect('/old', '/new', 301); // 301 (permanent)
Route::permanentRedirect('/old', '/new'); // 301
// View route:
Route::view('/about', 'about', ['version' => '1.0']);
// No controller needed!
Route Caching
<?php
// TRAP 16: Route caching and closures
// Closure routes CANNOT be cached!
Route::get('/test', function () {
return 'test';
});
// Running `php artisan route:cache` will FAIL with closure routes
// Always use controller classes for production
// Cache routes:
// php artisan route:cache
// Clear cache:
// php artisan route:clear
Экзаменационный вопрос: "Что произойдёт при выполнении php artisan route:cache если есть маршруты с closure?"
Ответ: Команда выбросит исключение. Closure-based маршруты нельзя кешировать. Используйте контроллеры.
Current Route
<?php
// TRAP 17: Getting current route info
$route = Route::current(); // Route object
$name = Route::currentRouteName(); // 'users.show'
$action = Route::currentRouteAction(); // 'App\Http\Controllers\UserController@show'
// In request:
$request->route(); // Route object
$request->route('user'); // Route parameter value
$request->routeIs('users.*'); // Check if name matches pattern
// In Blade:
@if(request()->routeIs('admin.*'))
// Admin sidebar
@endif
Subdomain Routing
<?php
// TRAP 18: Subdomain routing captures subdomain as parameter
Route::domain('{account}.app.test')->group(function () {
Route::get('/users/{id}', function (string $account, string $id) {
// $account = subdomain value
// Parameters are passed in ORDER: subdomain first, then route params
});
});
// TRAP: Subdomain parameter comes BEFORE route parameters in callback!