MidТеория9 min

Контроллеры

Resource контроллеры, invokable контроллеры, middleware в контроллерах, DI, API resource контроллеры

Основы контроллеров

Контроллеры группируют связанную логику обработки запросов в одном классе. В 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 (для трансформации данных). Контроллер только оркестрирует вызовы.


Проверь себя

Какие трейты подключены в базовом контроллере Laravel 11 по умолчанию?

Как определить middleware в контроллере в Laravel 11?

Чем отличается `Route::resource()->shallow()` от обычного nested resource?

Что такое Invokable Controller и когда его использовать?

Сколько маршрутов создаёт `Route::resource('posts', PostController::class)` и какие методы контроллера используются?