Урок 04

Контроллеры

Что такое контроллер

Контроллер — класс, который обрабатывает запросы. Вместо замыканий в роутах — методы в контроллере.

Зачем:

  • Структура — код по сущностям.
  • Тестируемость — легко тестировать.
  • DI — зависимости через конструктор.
  • Middleware — на группу методов.

Создание

php artisan make:controller PostController

Файл app/Http/Controllers/PostController.php:

namespace App\Http\Controllers;

class PostController extends Controller
{
    //
}

Базовый Controller — из App\Http\Controllers\Controller.

Базовый контроллер

class PostController extends Controller
{
    public function index()
    {
        return 'Список постов';
    }

    public function show(string $id)
    {
        return "Пост #{$id}";
    }
}

Каждый публичный метод — действие.

Привязка к роутам

Массивом:

use App\Http\Controllers\PostController;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{id}', [PostController::class, 'show']);

Группировка:

Route::controller(PostController::class)->group(function () {
    Route::get('/posts', 'index');
    Route::get('/posts/{id}', 'show');
    Route::post('/posts', 'store');
});

Внедрение зависимостей

Laravel сам разрешает зависимости:

public function show(PostService $service, string $id)
{
    return $service->find($id);
}

PostService создаётся контейнером и передаётся.

В конструкторе:

class PostController extends Controller
{
    public function __construct(
        private PostService $service,
    ) {}
}

Dependency Injection в аргументах

Можно смешивать:

public function update(
    Request $request,          // Laravel подставит
    PostService $service,      // и это тоже
    int $id,                    // из URL
) { }

Порядок: Laravel ищет классы в контейнере, остальное — из роута.

Типы ответов

Строка

return 'Hello';

Массив (JSON)

return ['status' => 'ok'];

View

return view('posts.index', ['posts' => $posts]);
return view('posts.show', compact('post'));

JSON

return response()->json(['status' => 'ok']);
return response()->json($data, 201);
return response()->json(['error' => 'Not found'], 404);

Пустой ответ

return response()->noContent();   // 204

Файл

return response()->download($path);
return response()->file($path);

Redirect

return redirect('/home');
return redirect()->route('posts.index');
return redirect()->action([PostController::class, 'index']);
return back();
return back()->withInput();
return back()->withErrors(['title' => 'Ошибка']);
return back()->with('success', 'Сохранено');

View с заголовками

return response()->view('posts.index', $data)
    ->header('X-Custom', 'value');

Middleware

В контроллере (старый способ):

public function __construct()
{
    $this->middleware('auth');
    $this->middleware('admin')->only(['destroy']);
    $this->middleware('log')->except(['index']);
}

⚠️ В Laravel 11+ этот способ устарел. Используй роуты или атрибуты.

В роутах:

Route::middleware('auth')->group(function () {
    Route::resource('posts', PostController::class);
});

Resource-контроллеры

Полный CRUD:

php artisan make:controller PostController --resource

Методы:

class PostController extends Controller
{
    public function index() { }         // GET /posts
    public function create() { }        // GET /posts/create
    public function store(Request $r) { } // POST /posts
    public function show(string $id) { }  // GET /posts/{id}
    public function edit(string $id) { }  // GET /posts/{id}/edit
    public function update(Request $r, string $id) { } // PUT/PATCH
    public function destroy(string $id) { } // DELETE
}

Подключение:

Route::resource('posts', PostController::class);

Что создаст:

МетодURLДействиеИмя
GET/postsindexposts.index
GET/posts/createcreateposts.create
POST/postsstoreposts.store
GET/posts/{post}showposts.show
GET/posts/{post}/editeditposts.edit
PUT/PATCH/posts/{post}updateposts.update
DELETE/posts/{post}destroyposts.destroy

Ограничение:

Route::resource('posts', PostController::class)->only(['index', 'show']);
Route::resource('posts', PostController::class)->except(['destroy']);

API Resource

Без create и edit (в API нет форм):

php artisan make:controller Api/PostController --api
Route::apiResource('posts', PostController::class);

Создаёт 5 роутов: index, store, show, update, destroy.

Вложенные ресурсы

Route::resource('posts.comments', CommentController::class);

URL: /posts/{post}/comments/{comment}.

Обработка ошибок

Исключения:

if (!$post) {
    abort(404);
}

if (!$request->user()->can('update', $post)) {
    abort(403, 'Нет доступа');
}

abort_if / abort_unless:

abort_if($post->user_id !== auth()->id(), 403);

throw:

throw new \Illuminate\Database\Eloquent\ModelNotFoundException();
throw new \Symfony\Component\HttpKernel\Exception\HttpException(500);

Практика

namespace App\Http\Controllers;

use App\Models\Post;
use App\Services\PostService;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function __construct(
        private PostService $service,
    ) {}

    public function index()
    {
        $posts = Post::with('user')
            ->latest()
            ->paginate(15);

        return view('posts.index', compact('posts'));
    }

    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'title' => 'required|string|max:255',
            'body' => 'required|string|min:10',
        ]);

        $post = $request->user()->posts()->create($data);

        return redirect()
            ->route('posts.show', $post)
            ->with('success', 'Пост создан');
    }

    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        $data = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'body' => 'sometimes|required|string|min:10',
        ]);

        $post->update($data);

        return back()->with('success', 'Обновлено');
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();

        return redirect()
            ->route('posts.index')
            ->with('success', 'Удалено');
    }
}

Частые ошибки

1. Забыли use:

Route::get('/x', [PostController::class, 'index']);   // Class not found

2. Не тот namespace:

namespace App\Controllers;   // неверно
namespace App\Http\Controllers;   // верно

3. Внедряешь зависимость в приватный метод — не сработает.

4. $this->middleware() в конструкторе — устарело в Laravel 11+.

5. Забыли return:

public function index()
{
    Post::all();   // return null
}

Итог

  • Контроллер — класс обработчиков.
  • [Controller::class, 'method'] — привязка к роуту.
  • DI — через конструктор и аргументы.
  • response()->json, view, redirect — ответы.
  • Resource — 7 методов CRUD.
  • abort() — бросить HTTP-ошибку.
  • authorize() — проверка прав.

Дальше — Blade.