Урок 04

Resource-контроллеры для API

Создание

php artisan make:controller Api/PostController --api --model=Post

Флаги:

  • --api — без create и edit.
  • --model=Post — сразу Route Model Binding.
  • Api/ — подпапка Controllers/Api.

Структура

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index() { }
    public function store(Request $request) { }
    public function show(Post $post) { }
    public function update(Request $request, Post $post) { }
    public function destroy(Post $post) { }
}

5 методов вместо 7 (без create и edit).

Регистрация

use App\Http\Controllers\Api\PostController;

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

Что создаст:

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

Реализация index

public function index()
{
    $posts = Post::query()
        ->with(['user:id,name,avatar', 'tags:id,name'])
        ->withCount('comments')
        ->latest()
        ->paginate(15);

    return PostResource::collection($posts);
}

Что происходит:

  1. Eager loading — with, нет N+1.
  2. withCount — счётчики.
  3. latest — сортировка.
  4. paginate(15) — пагинация.
  5. PostResource::collection — формат.

Реализация store

public function store(StorePostRequest $request)
{
    $post = $request->user()->posts()->create(
        $request->validated(),
    );

    return (new PostResource($post))
        ->response()
        ->setStatusCode(201);
}

Что происходит:

  1. StorePostRequest — валидация.
  2. $request->user()->posts() — через связь.
  3. create — проставляет user_id.
  4. 201 — Created.

Альтернатива:

return new PostResource($post);
// 200 OK вместо 201

Правильно — 201, если это создание.

Реализация show

public function show(Post $post): PostResource
{
    return new PostResource(
        $post->load(['user', 'tags', 'comments.user'])
    );
}

Route Model Binding — Laravel сам находит Post.

load — дозагрузка связей.

Реализация update

public function update(UpdatePostRequest $request, Post $post): PostResource
{
    $post->update($request->validated());

    return new PostResource($post->fresh());
}

fresh() — перезагрузить из БД (если есть events).

Или:

return new PostResource($post);

Реализация destroy

public function destroy(Post $post)
{
    $post->delete();

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

204 — No Content, стандарт для DELETE.

Партиции методов

Только index и show:

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

Без destroy:

Route::apiResource('posts', PostController::class)
    ->except(['destroy']);

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

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

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

Контроллер:

public function index(Post $post)
{
    return CommentResource::collection(
        $post->comments()->paginate(15)
    );
}

public function store(StoreCommentRequest $request, Post $post)
{
    $comment = $post->comments()->create([
        ...$request->validated(),
        'user_id' => $request->user()->id,
    ]);

    return (new CommentResource($comment))
        ->response()
        ->setStatusCode(201);
}

Вложенные роуты — сужают область.

Shallow nesting

Если вложенность глубокая — используй shallow:

Route::apiResource('posts.comments', CommentController::class)
    ->shallow();

Что даёт:

  • index, store — под /posts/{post}/comments.
  • show, update, destroy — под /comments/{comment}.

URL короче.

Scoped bindings

Чтобы пост искался среди постов пользователя:

Route::apiResource('users.posts', PostController::class)
    ->scoped();

Или:

->scopeBindings();

Что даёт: Post ищется среди User->posts.

Без этого: /users/1/posts/5 найдёт пост 5, даже если он у юзера 2.

Authorization

Проверка прав:

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

    $post->update($request->validated());

    return new PostResource($post);
}

Или через Policy — Laravel проверяет автоматически, если названия совпадают.

В AuthServiceProvider:

protected $policies = [
    Post::class => PostPolicy::class,
];

Или через атрибут:

#[Authorize('update', 'post')]
public function update(UpdatePostRequest $request, Post $post): PostResource { }

Контроллер-ресурс для API — полный

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;

class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:sanctum')->except(['index', 'show']);
    }

    public function index()
    {
        $posts = Post::query()
            ->with(['user:id,name,avatar', 'tags:id,name'])
            ->withCount('comments')
            ->latest()
            ->paginate(15);

        return PostResource::collection($posts);
    }

    public function store(StorePostRequest $request)
    {
        $post = $request->user()->posts()->create(
            $request->validated(),
        );

        return (new PostResource($post))
            ->response()
            ->setStatusCode(201);
    }

    public function show(Post $post): PostResource
    {
        return new PostResource(
            $post->load(['user', 'tags', 'comments.user'])
        );
    }

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

        $post->update($request->validated());

        return new PostResource($post->fresh());
    }

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

        $post->delete();

        return response()->noContent();
    }
}

Практика

1. Создай контроллер:

php artisan make:controller Api/PostController --api --model=Post

2. Зарегистрируй:

Route::apiResource('posts', \App\Http\Controllers\Api\PostController::class);

3. Реализуй методы с PostResource.

4. Проверь через curl:

curl http://localhost:8000/api/posts
curl http://localhost:8000/api/posts/1

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

1. Забыли --api:

// Будет 7 методов, включая create и edit (не нужны)

2. return $post вместо PostResource:

return $post;   // все поля, включая приватные

// Правильно:
return new PostResource($post);

3. Нет 201 для store:

return new PostResource($post);   // 200

// Правильно:
return (new PostResource($post))->response()->setStatusCode(201);

4. Нет eager loading:

public function index()
{
    return PostResource::collection(Post::paginate(15));
    // N+1 в ресурсе
}

Решение: ->with('user').

5. authorize не работает:

$this->authorize('update', $post);
// Если Policy не зарегистрирована — AuthorizationException

Итог

  • --api — контроллер без форм.
  • apiResource — 5 роутов.
  • index — список с пагинацией.
  • store — создание через связь, 201.
  • show — один ресурс.
  • update — валидация + authorize.
  • destroy — 204.
  • Вложенные — posts.comments.
  • scoped — правильная связь.

Дальше — валидация в API.