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

Первый REST-контроллер на Yii2 пишется за пять минут: наследуетесь от yii\rest\ActiveController, указываете modelClass — и получаете полный CRUD с пагинацией, фильтрацией и HTTP-кодами. Проблемы начинаются, когда этот эндпоинт видит фронтенд или сторонний потребитель. Модель отдаёт все свои поля, включая служебные. Пагинация возвращает данные в формате, который клиент не ждёт. А запрос списка заказов с товарами внутри превращается в сотни SQL-запросов.
Статья про приёмы, которые снимают эти боли на Yii2 2.0.x (актуально и для веток 2.0.40+). Мы не разбираем, как поднять первый контроллер, — предполагаем, что базовый CRUD у вас уже работает и вы упёрлись в конкретику.
Что модель отдаёт наружу и как это контролировать
Главная ловушка ActiveController — сериализация. По умолчанию модель через toArray() отдаёт все атрибуты таблицы. Значит, в JSON улетают password_hash, auth_key, внутренние флаги и всё, что вы не собирались показывать.
Контролировать это правильно на уровне модели, а не контроллера. У yii\base\Model есть два метода — fields() и extraFields().
class User extends \yii\db\ActiveRecord
{
// Поля, которые уходят в ответ всегда
public function fields()
{
return [
'id',
'username',
'email',
// Вычисляемое поле: ключ + замыкание
'fullName' => function ($model) {
return trim($model->first_name . ' ' . $model->last_name);
},
];
}
// Поля, которые отдаются только по запросу ?expand=
public function extraFields()
{
return ['profile', 'orders'];
}
}Теперь password_hash в ответ не попадёт, даже если кто-то добавит его в таблицу и забудет. А profile подтянется только при явном GET /users?expand=profile. Это разделение — не косметика: extraFields позволяет держать список лёгким, а детали грузить по требованию.
Правило, которое экономит инциденты с утечкой данных: белый список в fields(), а не чёрный список исключений. Добавили колонку в таблицу — она не появится в API, пока вы её туда осознанно не впишете.Пагинация: клиент ждёт не то, что вы отдаёте
ActiveController с IndexAction отдаёт коллекцию с пагинацией, но метаданные кладёт не в тело, а в HTTP-заголовки: X-Pagination-Total-Count, X-Pagination-Page-Count, X-Pagination-Current-Page. Фронтенд-разработчики про это часто не знают и жалуются, что «API не отдаёт общее число записей». Оно отдаётся — просто в заголовке, а не в JSON.
Если вам нужна пагинация внутри тела ответа (частый запрос от SPA), меняйте формат сериализатора:
public function actions()
{
$actions = parent::actions();
// Переопределяем dataProvider, оставляя логику родителя
return $actions;
}
// В контроллере — свой serializer
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items', // данные лягут в поле items
];С collectionEnvelope ответ станет объектом с полями items и _meta, где _meta содержит totalCount, pageCount, currentPage и perPage. Это то, что обычно и хочет клиент.
Отдельная боль — лимит страницы. По умолчанию pageSize ограничен, но злоумышленник или неудачный запрос могут попросить огромную страницу. Контролируйте это в Pagination:
use yii\data\ActiveDataProvider;
use yii\data\Pagination;
public function prepareDataProvider()
{
return new ActiveDataProvider([
'query' => Order::find()->where(['user_id' => Yii::$app->user->id]),
'pagination' => new Pagination([
'defaultPageSize' => 20,
'pageSizeLimit' => [1, 100], // клиент не выйдет за 100
]),
]);
}N+1: где Yii2 незаметно делает сотни запросов
Самый дорогой промах в REST на Yii2 — вложенные ресурсы без жадной загрузки. Отдаёте список из 50 заказов, для каждого в fields() тянете покупателя — получаете 1 запрос на заказы плюс 50 на покупателей. Под нагрузкой это кладёт базу.
Yii2 умеет грузить связи жадно через with(), но ActiveController сам за вас этого не сделает. Свяжите expand с with вручную:
public function prepareDataProvider()
{
$query = Order::find();
// Если клиент просит expand=customer — грузим связь жадно
$expand = Yii::$app->request->get('expand', '');
if (in_array('customer', explode(',', $expand), true)) {
$query->with('customer');
}
return new ActiveDataProvider(['query' => $query]);
}Проверить, что N+1 ушёл, просто: включите панель Yii Debug (модуль yii\debug\Module) и посмотрите вкладку Database. Было 51 запрос — стало 2. Без замера вы не узнаете, что связь грузится лениво: ответ-то вернётся корректный, просто медленно.
Фильтрация без ручного разбора GET-параметров
Писать if (isset($_GET['status'])) $query->andWhere(...) для каждого фильтра — путь к простыням кода и SQL-инъекциям. В Yii2 для этого есть DataFilter и модель поиска.
| Подход | Когда брать | Цена |
|---|---|---|
| Search-модель (Gii) | Фильтры совпадают с полями таблицы, нужна валидация | Лишний класс на каждую сущность |
yii\data\DataFilter | Гибкие условия от клиента (диапазоны, операторы) | Клиент шлёт сложный JSON-фильтр, надо документировать формат |
Ручной разбор в prepareDataProvider | Один-два простых фильтра | Легко забыть экранирование, дублируется между контроллерами |
Для типового API берите search-модель: она валидирует входные параметры и сама собирает andFilterWhere, который игнорирует пустые значения. Это защищает и от инъекций, и от фильтрации по мусору.
Коды ответов и ошибки: не отдавайте HTML вместо JSON
Ещё один частый прокол: при исключении API возвращает HTML-страницу ошибки Yii, а клиент ждёт JSON. Причина — не настроен формат ответа для ошибок. Пропишите обработчик в конфиге приложения:
// config/web.php, секция components
'response' => [
'class' => 'yii\web\Response',
'on beforeSend' => function ($event) {
$response = $event->sender;
if ($response->data !== null && Yii::$app->request->get('suppress_response_code')) {
// Единый конверт, если клиент просит always-200 (для старых клиентов)
$response->data = [
'success' => $response->isSuccessful,
'data' => $response->data,
];
$response->statusCode = 200;
}
},
],
'errorHandler' => [
'errorAction' => null, // не рендерить HTML-экшен, отдавать формат ответа
],Для чистого REST не заворачивайте всё в always-200 — используйте настоящие коды: 201 на создание, 204 на удаление, 422 на ошибку валидации с телом [{field, message}], 404 через NotFoundHttpException. Yii2 сам сериализует эти исключения в JSON, если формат ответа выставлен верно через ContentNegotiator.
Аутентификация и CORS в связке
Порядок фильтров в behaviors() критичен. Если authenticator отработает раньше, чем corsFilter, то preflight-запрос OPTIONS получит 401 — и браузер заблокирует весь запрос ещё до основной проверки. CORS-фильтр должен стоять первым, а аутентификатор — пропускать OPTIONS.
public function behaviors()
{
$behaviors = parent::behaviors();
// CORS раньше аутентификации
$behaviors['corsFilter'] = [
'class' => \yii\filters\Cors::class,
];
$behaviors['authenticator'] = [
'class' => \yii\filters\auth\HttpBearerAuth::class,
// OPTIONS не требует токена — иначе preflight упадёт
'except' => ['options'],
];
return $behaviors;
}Что ломается на практике
- Забытый
fields()— служебные поля утекают в ответ. Проверяйте реальный JSON, а не предположения. - N+1 при
expand— работает корректно, но медленно. Ловится только замером количества SQL-запросов. - HTML вместо JSON при ошибке — клиент падает на парсинге. Тестируйте не только happy path, но и 404, 422, 500.
- CORS после аутентификатора — preflight отдаёт 401, фронтенд с другого домена не работает.
- Отсутствие
pageSizeLimit— запрос?per-page=100000тянет всю таблицу в память.
Что настроить дальше
Когда базовые приёмы на месте, займитесь наблюдаемостью и версионированием. Разнесите версии API по разным модулям или префиксам (/v1, /v2) через UrlManager с enablePrettyUrl — менять контракт на живом эндпоинте нельзя. Включите на стейджинге Yii Debug и смотрите вкладку Database под реальной нагрузкой: N+1 виден только там. Для контракта с фронтендом добавьте генерацию OpenAPI-описания — так клиент перестанет угадывать, где лежат метаданные пагинации. Инвалидация кеша ответов — отдельная тема, здесь мы её не трогали.
Prompt-инженер: Идеальные запросы для Midjourney, ChatGPT и других моделей.
Спросить за 15 ₽Источники: Yii2 Guide — REST: Resources и ActiveController, Yii2 API — yii\rest\Serializer
Частые вопросы
У меня Yii2 версии ниже 2.0.40, приёмы сработают?
Почему клиент не видит общее число записей в ответе?
Как понять, есть ли у меня проблема N+1, если ответ приходит корректно?
Не сработало — при ошибке API всё равно отдаёт HTML. Что проверить?
Безопасно ли отдавать связи через expand без ограничений?
Стоит ли заворачивать все ответы в конверт success/data с кодом 200?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.