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/posts | index | posts.index |
| POST | /api/posts | store | posts.store |
| GET | /api/posts/{post} | show | posts.show |
| PUT/PATCH | /api/posts/{post} | update | posts.update |
| DELETE | /api/posts/{post} | destroy | posts.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);
}
Что происходит:
- Eager loading —
with, нет N+1. - withCount — счётчики.
- latest — сортировка.
- paginate(15) — пагинация.
- PostResource::collection — формат.
Реализация store
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create(
$request->validated(),
);
return (new PostResource($post))
->response()
->setStatusCode(201);
}
Что происходит:
StorePostRequest— валидация.$request->user()->posts()— через связь.create— проставляетuser_id.- 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.