REST API на Yii2: рабочие приёмы и типичные ошибки
Yii2 умеет отдавать REST из коробки, но дефолтная конфигурация роняет вас в 500-е на первом же нестандартном роуте. Разбираем, где стоит доверять ActiveController, а где писать руками.

Yii2 обещает работающий REST за один контроллер: наследуете yii\rest\ActiveController, указываете modelClass, вешаете UrlManager с enablePrettyUrl — и получаете CRUD с фильтрацией, пагинацией и сериализацией. На демо это выглядит магией. В реальном проекте магия ломается на первой связи «многие-ко-многим», на первом кастомном действии и на первом требовании вернуть не всю модель, а три поля. Дальше — приёмы, которые экономят день на каждом из этих мест, и ошибки, которые совершают почти все на старте.
Материал для Yii2 (2.0.x, актуальная ветка 2.0.49+), для разработчика, который уже поднял приложение и умеет писать модели ActiveRecord. Про установку фреймворка и настройку БД тут ничего нет — только слой API.
Где ActiveController помогает, а где мешает
ActiveController генерирует шесть действий: index, view, create, update, delete, options. Пока ваш ресурс — плоская таблица, это идеальный сценарий: код контроллера умещается в три строки.
<?php
namespace app\controllers;
use yii\rest\ActiveController;
class OrderController extends ActiveController
{
public $modelClass = 'app\models\Order';
}Проблемы начинаются, когда бизнес-логика перестаёт совпадать с таблицей. Три частых расхождения:
- Отдаёте лишнее. По умолчанию сериализуются все атрибуты модели, включая
password_hashи внутренние флаги. Это не теоретический риск — это утечка на первом же GET. - Нужно кастомное действие. Например
POST /orders/{id}/cancel. В набор из шести действий оно не входит, и его придётся описывать отдельно. - Права на уровне записи.
ActiveControllerпроверяет доступ к действию, но не к конкретной записи. «Пользователь может редактировать только свои заказы» — это уже ваш код.
Приём 1: управляйте выдачей через fields() и extraFields()
Не полагайтесь на дефолтную сериализацию. Переопределите в модели fields() — метод возвращает массив атрибутов, которые попадут в ответ. Всё, чего там нет, наружу не уйдёт.
<?php
// В классе модели app\models\Order
public function fields()
{
return [
'id',
'status',
'total',
// переименование поля в ответе
'createdAt' => 'created_at',
// вычисляемое поле
'isPaid' => function ($model) {
return $model->status === 'paid';
},
];
}
// Связи — только по запросу через ?expand=items
public function extraFields()
{
return ['items', 'customer'];
}После этого клиент получит связанные items только при запросе GET /orders?expand=items, а не в каждом ответе. Для списков это разница между одним SQL-запросом и проблемой N+1.
Приём 2: кастомные действия — через verbs() и отдельный метод
Чтобы добавить POST /orders/{id}/cancel, объявите действие как обычный метод контроллера и разрешите ему HTTP-глагол через переопределение verbs(). Заодно не забудьте про роут в UrlManager.
<?php
// В OrderController
public function actionCancel($id)
{
$model = $this->findModel($id);
if (!$model->cancel()) {
// валидация вернёт 422 с ошибками
return $model;
}
Yii::$app->response->statusCode = 200;
return ['status' => $model->status];
}
protected function verbs()
{
$verbs = parent::verbs();
$verbs['cancel'] = ['POST'];
return $verbs;
}В конфиге UrlManager добавьте правило поверх стандартного 'class' => 'yii\rest\UrlRule':
'rules' => [
'POST orders/<id:\d+>/cancel' => 'order/cancel',
['class' => 'yii\rest\UrlRule', 'controller' => 'order'],
],Порядок важен: специфичное правило должно стоять выше общего UrlRule, иначе запрос перехватит стандартный роутинг и вернёт 404.
Формат, коды и ошибки
Yii2 по умолчанию согласовывает формат через заголовок Accept и умеет отдавать JSON и XML. XML почти никому не нужен, а его наличие плодит неожиданные ответы, когда клиент забыл заголовок. Прибейте формат явно через ContentNegotiator в behaviors().
Самая частая причина «почему мой API отдаёт XML» — забытый заголовок Accept на стороне клиента и незаданный явно formats у ContentNegotiator. Один раз зафиксируйте JSON — и забудете про этот класс багов.
Приём 3: единый формат ответа и ошибок
Клиенту проще, когда и успех, и ошибка приходят в одной обёртке. Стандартный ErrorHandler Yii2 уже отдаёт ошибки в структурированном виде (name, message, code, status), но формат успешного ответа стоит зафиксировать самостоятельно через событие Response::EVENT_BEFORE_SEND. Не переписывайте сериализатор целиком ради обёртки — это лишняя точка отказа; хватает одного обработчика события.
Про валидацию: если модель не прошла validate(), Yii2 сам вернёт 422 Unprocessable Entity с массивом полей и сообщений. Не ловите это вручную и не превращайте в 400 — 422 здесь семантически корректнее, и клиенты на него рассчитывают.
Приём 4: аутентификация — не забудьте включить её осознанно
Класс yii\filters\auth\HttpBearerAuth и его собратья вешаются в behaviors() контроллера. Тонкость, на которой спотыкаются: аутентификация ломает preflight-запрос OPTIONS, который браузер шлёт перед CORS-запросом. Решение — добавить Cors-фильтр до аутентификации и исключить OPTIONS из проверки.
<?php
public function behaviors()
{
$behaviors = parent::behaviors();
// CORS должен идти ПЕРЕД authenticator
$behaviors['corsFilter'] = [
'class' => \yii\filters\Cors::class,
];
$behaviors['authenticator'] = [
'class' => \yii\filters\auth\HttpBearerAuth::class,
// не проверять токен на preflight
'except' => ['options'],
];
return $behaviors;
}Сравнение подходов к реализации
| Подход | Когда брать | Цена |
|---|---|---|
| ActiveController как есть | Плоский ресурс, CRUD один-в-один с таблицей | Утечка полей, если не переопределить fields(); нет прав на запись |
| ActiveController + fields/verbs | Большинство проектов: CRUD плюс несколько своих действий | Ручной контроль выдачи и роутов, но фреймворк ещё работает на вас |
| Controller от нуля | Логика далеко от таблиц, сложная агрегация, свой формат | Пишете сериализацию, пагинацию и фильтры сами |
Что ломается на проде
- N+1 на списках.
expandбезwith()в запросе даёт по запросу к БД на каждую запись. Настройте жадную загрузку вprepareDataProviderили черезjoinWith. - Пагинация молча режет выдачу. По умолчанию
ActiveDataProviderотдаёт 20 записей на страницу и кладёт метаданные в заголовкиX-Pagination-*. Клиенты, читающие только тело, теряют половину данных и не понимают почему. - PATCH против PUT. Стандартное действие
updateпринимает оба глагола и делает частичное обновление черезload(). Если вы ждёте от PUT полной замены ресурса — это ваш код, из коробки такого разделения нет.
Что настроить дальше
Когда базовый CRUD работает: включите rate limiting через интерфейс yii\filters\RateLimitInterface на модели пользователя — без него один клиент в цикле положит базу. Затем зафиксируйте контракт: сгенерируйте OpenAPI-описание и проверяйте ответы контрактными тестами, а не глазами в Postman. И проверьте, что YII_DEBUG выключен на проде — иначе ErrorHandler отдаёт стектрейсы наружу вместе с путями к файлам.
Разрушитель авто-разводов: Фото заказ-наряда СТО: завышения, дубли работ, лишние позиции.
Спросить за 15 ₽Источники: Yii2 Guide — REST APIs, Yii2 Guide — Data Formatting (fields, extraFields)
Частые вопросы
У меня Yii2 2.0.13, а примеры для 2.0.49 — они подойдут?
API возвращает XML вместо JSON — что не так?
Preflight OPTIONS падает с 401 после включения аутентификации — почему?
Как отдать только часть полей модели, не сломав ActiveRecord?
Отдаётся не 20 записей, а хочу все — как убрать пагинацию?
Валидация не проходит — какой код лучше вернуть, 400 или 422?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.