Основы контроллеров
Контроллеры группируют связанную логику обработки запросов в одном классе. В Laravel 11 базовый контроллер значительно упрощён.
Базовый контроллер
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
// Laravel 11: Base controller is minimal
// No traits by default — add only what you need
abstract class Controller
{
// This is the entire base controller in Laravel 11
// No AuthorizesRequests, ValidatesRequests, or DispatchesJobs traits
}
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class UserController extends Controller
{
public function index(): JsonResponse
{
$users = User::paginate(15);
return response()->json($users);
}
public function show(User $user): JsonResponse
{
return response()->json($user);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
$user = User::create($validated);
return response()->json($user, 201);
}
}
Изменение в Laravel 11: Базовый контроллер больше не подключает трейты
AuthorizesRequests,ValidatesRequestsиDispatchesJobs. Если они нужны, их нужно добавить явно. Это следует принципу "включай только то, что используешь".
Создание контроллеров через Artisan
# Basic controller
php artisan make:controller UserController
# Resource controller (with all CRUD methods)
php artisan make:controller UserController --resource
# API resource controller (without create/edit)
php artisan make:controller UserController --api
# Invokable controller (single action)
php artisan make:controller ShowDashboardController --invokable
# Controller with model binding
php artisan make:controller UserController --resource --model=User
# Controller with form requests
php artisan make:controller UserController --resource --model=User --requests
# Nested resource controller
php artisan make:controller CommentController --resource --parent=Post
Resource Controllers
Resource контроллеры предоставляют стандартный набор CRUD-операций.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Article;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ArticleController extends Controller
{
/**
* Display a listing of the resource.
* GET /articles
*/
public function index(): View
{
$articles = Article::latest()->paginate(15);
return view('articles.index', compact('articles'));
}
/**
* Show the form for creating a new resource.
* GET /articles/create
*/
public function create(): View
{
return view('articles.create');
}
/**
* Store a newly created resource in storage.
* POST /articles
*/
public function store(StoreArticleRequest $request): RedirectResponse
{
$article = Article::create($request->validated());
return redirect()
->route('articles.show', $article)
->with('success', 'Article created successfully.');
}
/**
* Display the specified resource.
* GET /articles/{article}
*/
public function show(Article $article): View
{
return view('articles.show', compact('article'));
}
/**
* Show the form for editing the specified resource.
* GET /articles/{article}/edit
*/
public function edit(Article $article): View
{
return view('articles.edit', compact('article'));
}
/**
* Update the specified resource in storage.
* PUT/PATCH /articles/{article}
*/
public function update(UpdateArticleRequest $request, Article $article): RedirectResponse
{
$article->update($request->validated());
return redirect()
->route('articles.show', $article)
->with('success', 'Article updated successfully.');
}
/**
* Remove the specified resource from storage.
* DELETE /articles/{article}
*/
public function destroy(Article $article): RedirectResponse
{
$article->delete();
return redirect()
->route('articles.index')
->with('success', 'Article deleted successfully.');
}
}
Регистрация Resource Controller
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ArticleController;
// Full resource route
Route::resource('articles', ArticleController::class);
// API resource (no create/edit views)
Route::apiResource('articles', ArticleController::class);
// Limit to specific actions
Route::resource('articles', ArticleController::class)
->only(['index', 'show']);
Route::resource('articles', ArticleController::class)
->except(['destroy']);
// Customize parameter names
Route::resource('users', UserController::class)
->parameters(['users' => 'admin_user']);
// Generates: /users/{admin_user} instead of /users/{user}
// Customize resource names
Route::resource('photos', PhotoController::class)
->names([
'create' => 'photos.build',
'store' => 'photos.save',
]);
// Soft delete resource (adds restore and forceDelete)
Route::resource('posts', PostController::class)->withTrashed();
// Allows Route Model Binding to find soft-deleted models
Route::resource('posts', PostController::class)->withTrashed(['show', 'edit']);
Для экзамена:
Route::resource()создаёт 7 маршрутов (index, create, store, show, edit, update, destroy).Route::apiResource()создаёт 5 маршрутов (без create и edit). Метод->withTrashed()позволяет Route Model Binding находить мягко удалённые модели.
API Resource Controllers
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Http\Resources\UserCollection;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
final class UserController extends Controller
{
/**
* GET /api/users
*/
public function index(): AnonymousResourceCollection
{
$users = User::with('roles')
->latest()
->paginate(15);
return UserResource::collection($users);
}
/**
* POST /api/users
*/
public function store(StoreUserRequest $request): JsonResponse
{
$user = User::create($request->validated());
return (new UserResource($user))
->response()
->setStatusCode(201);
}
/**
* GET /api/users/{user}
*/
public function show(User $user): UserResource
{
$user->load(['roles', 'permissions']);
return new UserResource($user);
}
/**
* PUT /api/users/{user}
*/
public function update(UpdateUserRequest $request, User $user): UserResource
{
$user->update($request->validated());
return new UserResource($user);
}
/**
* DELETE /api/users/{user}
*/
public function destroy(User $user): JsonResponse
{
$user->delete();
return response()->json(null, 204);
}
}
Invokable Controllers (одно действие)
Invokable контроллеры содержат единственный метод __invoke(). Идеальны для действий, которые не вписываются в CRUD-паттерн.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Order;
use App\Services\ExportService;
use Illuminate\Http\Response;
final class ExportOrdersController extends Controller
{
public function __construct(
private readonly ExportService $exportService,
) {}
/**
* Single action controller — only __invoke() method.
*/
public function __invoke(string $format = 'csv'): Response
{
$orders = Order::with('items')->get();
$content = $this->exportService->export($orders, $format);
return response($content)
->header('Content-Type', 'text/csv')
->header('Content-Disposition', 'attachment; filename="orders.csv"');
}
}
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Invokable controller — no method specified
Route::get('/export/orders', ExportOrdersController::class);
// With parameter
Route::get('/export/orders/{format}', ExportOrdersController::class);
// With name
Route::get('/export/orders', ExportOrdersController::class)->name('orders.export');
Для Senior: Invokable контроллеры следуют принципу SRP (Single Responsibility Principle). Каждый контроллер отвечает за одно действие. Используйте их для: генерации отчётов, импорта/экспорта, одноразовых действий (подтверждение email, подписка на рассылку).
Dependency Injection в контроллерах
Constructor Injection
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Contracts\PaymentGatewayInterface;
use App\Services\OrderService;
use App\Services\NotificationService;
final class OrderController extends Controller
{
// Dependencies injected via constructor
// Container resolves them automatically
public function __construct(
private readonly OrderService $orderService,
private readonly PaymentGatewayInterface $paymentGateway,
private readonly NotificationService $notifications,
) {}
public function store(StoreOrderRequest $request)
{
// Use injected services
$order = $this->orderService->create($request->validated());
$this->paymentGateway->charge($order);
$this->notifications->sendOrderConfirmation($order);
return new OrderResource($order);
}
}
Method Injection
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
final class UserController extends Controller
{
// Method injection: dependencies resolved per-method
// Useful when dependency is needed only in one method
public function update(
Request $request,
User $user, // Route Model Binding
UserService $userService, // Method injection
): JsonResponse {
$userService->update($user, $request->validated());
return response()->json($user->fresh());
}
// You can combine constructor and method injection
public function show(
User $user, // Route Model Binding
): JsonResponse {
return response()->json($user->load('posts'));
}
}
Для экзамена: Laravel контроллеры поддерживают ОБА типа внедрения зависимостей: конструктор (constructor injection) и метод (method injection). Route Model Binding (типизированные параметры Eloquent модели) -- это тоже форма method injection.
Middleware в контроллерах (Laravel 11)
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
final class PostController extends Controller implements HasMiddleware
{
/**
* Define middleware for this controller.
* Laravel 11 approach — interface-based.
*/
public static function middleware(): array
{
return [
// Apply to all methods
'auth',
// Apply only to specific methods
new Middleware('verified', only: ['store', 'update', 'destroy']),
// Exclude from specific methods
new Middleware('throttle:api', except: ['index', 'show']),
// Closure middleware (inline)
new Middleware(function ($request, $next) {
if (! $request->user()?->hasVerifiedEmail()) {
return redirect('/email/verify');
}
return $next($request);
}, only: ['store']),
];
}
public function index() { /* ... */ }
public function show(Post $post) { /* ... */ }
public function store(Request $request) { /* ... */ }
public function update(Request $request, Post $post) { /* ... */ }
public function destroy(Post $post) { /* ... */ }
}
Контроллеры с трейтами
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
final class PostController extends Controller
{
use AuthorizesRequests;
public function update(Request $request, Post $post)
{
// Authorization check using trait
$this->authorize('update', $post);
$post->update($request->validated());
return new PostResource($post);
}
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return response()->noContent();
}
}
Nested Resource Controllers
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Full nested resource
Route::resource('posts.comments', CommentController::class);
// Generates:
// GET /posts/{post}/comments → comments.index
// GET /posts/{post}/comments/create → comments.create
// POST /posts/{post}/comments → comments.store
// GET /posts/{post}/comments/{comment} → comments.show
// GET /posts/{post}/comments/{comment}/edit → comments.edit
// PUT /posts/{post}/comments/{comment} → comments.update
// DELETE /posts/{post}/comments/{comment} → comments.destroy
// Shallow nesting — reduces URL depth
Route::resource('posts.comments', CommentController::class)->shallow();
// Nested (need parent context):
// GET /posts/{post}/comments → comments.index
// GET /posts/{post}/comments/create → comments.create
// POST /posts/{post}/comments → comments.store
// Shallow (don't need parent):
// GET /comments/{comment} → comments.show
// GET /comments/{comment}/edit → comments.edit
// PUT /comments/{comment} → comments.update
// DELETE /comments/{comment} → comments.destroy
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Comment;
use Illuminate\Http\JsonResponse;
final class CommentController extends Controller
{
// Nested: both parent and child are injected
public function index(Post $post): JsonResponse
{
$comments = $post->comments()->paginate(10);
return response()->json($comments);
}
public function store(Request $request, Post $post): JsonResponse
{
$comment = $post->comments()->create(
$request->validated()
);
return response()->json($comment, 201);
}
// Shallow: only child is injected
public function show(Comment $comment): JsonResponse
{
return response()->json($comment->load('post'));
}
}
Паттерн: тонкие контроллеры
<?php
declare(strict_types=1);
// WRONG: Fat controller with too much logic
namespace App\Http\Controllers;
final class OrderController extends Controller
{
public function store(Request $request): JsonResponse
{
// ❌ Too much business logic in controller
$validated = $request->validate([...]);
$order = Order::create($validated);
$order->items()->createMany($request->items);
$total = $order->items->sum(fn ($item) => $item->price * $item->quantity);
$order->update(['total' => $total]);
Mail::to($order->user)->send(new OrderConfirmation($order));
event(new OrderPlaced($order));
return response()->json($order, 201);
}
}
// CORRECT: Thin controller delegating to service/action
namespace App\Http\Controllers;
use App\Actions\CreateOrderAction;
use App\Http\Requests\StoreOrderRequest;
use App\Http\Resources\OrderResource;
final class OrderController extends Controller
{
public function store(
StoreOrderRequest $request,
CreateOrderAction $createOrder,
): OrderResource {
// ✅ Controller only orchestrates
$order = $createOrder->execute($request->validated());
return new OrderResource($order);
}
}
<?php
declare(strict_types=1);
// Action class — single responsibility
namespace App\Actions;
use App\Models\Order;
use App\Events\OrderPlaced;
use App\Mail\OrderConfirmation;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
final readonly class CreateOrderAction
{
public function execute(array $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create($data);
$order->items()->createMany($data['items']);
$order->calculateTotal();
Mail::to($order->user)->queue(new OrderConfirmation($order));
event(new OrderPlaced($order));
return $order;
});
}
}
Для Senior: Контроллеры должны быть "тонкими" -- минимум логики. Бизнес-логика выносится в: 1) Action classes (для сложных одноразовых операций), 2) Services (для переиспользуемой логики), 3) Form Requests (для валидации), 4) Resources (для трансформации данных). Контроллер только оркестрирует вызовы.