Консольные команды в Yii 2: типичные ошибки и как их избежать
Console-контроллеры в Yii 2 выглядят как обычные экшены, но ведут себя иначе: свой env, свои коды выхода, свои утечки памяти. Разбираем ошибки, из-за которых команда падает молча или роняет сервер по крону.

Вы написали консольную команду в Yii 2, локально она отрабатывает за секунду, а на проде под кроном раз в пять минут забивает память и через час сервер уходит в своп. Или ещё хуже: команда завершается с кодом 0, супервизор считает её успешной, а на деле она поймала исключение и не сделала ничего. Консольные контроллеры в Yii 2 (актуальная линейка — 2.0.x, здесь всё написано для неё) отличаются от веб-контроллеров сильнее, чем кажется, и большинство проблем растёт именно из этой разницы.
Разбираем приёмы для тех, кто уже пишет команды через yii\console\Controller и упёрся в конкретные грабли: коды выхода, память в длинных циклах, окружение крона, транзакции. Регистрацию контроллеров и настройку commandMap здесь не трогаем — это отдельная тема.
Код выхода: главная тихая ошибка
Экшен веб-контроллера возвращает то, что отрендерится в браузер. Консольный экшен возвращает код завершения процесса. Если вы просто выполнили работу и ничего не вернули, PHP отдаст 0 — «успех», — даже если внутри всё пошло не так, но исключение вы поймали и залогировали.
Крон, systemd, супервизор и CI смотрят именно на код выхода. Команда, которая молча провалилась с кодом 0, — это данные, которые не выгрузились, письма, которые не ушли, и очередь, которая забилась, а мониторинг молчит.
<?php
namespace app\commands;
use yii\console\Controller;
use yii\console\ExitCode;
class SyncController extends Controller
{
public function actionRun()
{
try {
$count = $this->doSync();
$this->stdout("Синхронизировано записей: {$count}\n");
// Явный код успеха, а не «ничего не вернули»
return ExitCode::OK;
} catch (\Throwable $e) {
// Пишем в stderr, а не в stdout — крон обычно шлёт stderr на почту
$this->stderr('Ошибка синхронизации: ' . $e->getMessage() . "\n");
\Yii::error($e, __METHOD__);
// Ненулевой код: супервизор увидит падение
return ExitCode::UNSPECIFIED_ERROR;
}
}
private function doSync(): int
{
// ... реальная работа
return 0;
}
}
Класс yii\console\ExitCode содержит именованные константы по соглашению sysexits.h — OK (0), UNSPECIFIED_ERROR (1), DATAERR (65) и другие. Возвращайте их, а не голые числа: читаемее и не перепутаете.
Правило простое: любой экшен консольного контроллера всегда возвращает код выхода явно. Если в экшене нет ни одного return — это потенциально молчащая ошибка.
Память в длинных циклах: ActiveRecord и логгер
Веб-запрос живёт миллисекунды и умирает — утечка памяти незаметна. Консольная команда, которая обходит миллион строк, живёт минутами, и всё, что накапливается, накапливается по-настоящему.
Два главных источника роста памяти в Yii 2:
- Загрузка всех записей разом.
Model::find()->all()тянет всё в массив объектов. На большой таблице это OOM ещё до начала работы. - Профилировщик и логгер. При
YII_DEBUG=trueкомпонентdbпишет каждый запрос в лог-таргеты в памяти, а профайлер копит трейсы. За полмиллиона итераций это гигабайты.
Первый пункт лечится потоковой выборкой:
<?php
// batch() выбирает пачками, each() отдаёт по одной записи,
// но под капотом тоже пачками — память не растёт линейно
foreach (Order::find()->each(500) as $order) {
$this->process($order);
}
Второй пункт — держите YII_DEBUG выключенным на проде. Убедитесь, что во входном скрипте yii (в корне приложения) окружение выставлено в prod, а не унаследовано от дефолтного dev. Если отладка нужна, периодически сбрасывайте накопленный лог внутри цикла — вызывайте \Yii::getLogger()->flush(true) раз в N итераций, чтобы буфер не рос.
Диагностика, а не гадание
Не угадывайте, где течёт. Печатайте потребление памяти каждые несколько тысяч итераций и смотрите на тренд:
if ($i % 5000 === 0) {
$mb = round(memory_get_usage(true) / 1048576, 1);
$this->stdout("Обработано {$i}, память {$mb} МБ\n");
}
Если график ровный — течёт где-то в вашем коде (накопление в свойстве, незакрытые ресурсы). Если растёт линейно и вы на ->all() — меняйте на each(). Если растёт при включённом debug — дело в логгере.
Интерактивные подтверждения ломают крон
Метод $this->confirm('Точно удалить?') удобен в терминале, но под кроном ввода нет: команда либо зависает в ожидании, либо получает пустую строку и трактует её как отказ. В обоих случаях автоматика ломается.
У yii\console\Controller есть свойство interactive. Запускайте автоматические команды с флагом --interactive=0, и тогда confirm() сразу возвращает значение по умолчанию без ожидания ввода. В самой команде это учитывается автоматически — специальный код писать не нужно, достаточно передать флаг в строке крона.
Транзакции и частичная запись
Команда упала на 4000-й записи из 10000. Что с базой? Если каждая итерация делает свой save(), у вас 4000 записанных строк и 6000 нет — данные в промежуточном, часто нелегальном состоянии. Перезапуск команды с нуля может продублировать первые 4000.
Здесь нет одного верного ответа, есть выбор стратегии:
| Стратегия | Что выигрываете | Чем платите |
|---|---|---|
| Одна транзакция на всё | Атомарность: либо всё, либо ничего | Долгая блокировка строк, риск таймаута, память под весь набор изменений |
| Пачки по N в транзакции | Компромисс: блокировки короткие, откат ограничен пачкой | При падении часть пачек записана — нужна идемпотентность |
| Без транзакций, идемпотентная запись | Безопасный перезапуск с любого места | Сложнее логика: upsert, отметки об обработке |
Для регулярных фоновых задач обычно выигрывает третий вариант: помечайте обработанные записи (поле processed_at) и выбирайте на входе только необработанные. Тогда повторный запуск после падения безопасен и не требует ручного разбора состояния.
Тайм-аут и «зомби» под кроном
По умолчанию у CLI-процессов PHP max_execution_time = 0, то есть без ограничения. Долгая команда под кроном раз в минуту, которая иногда работает две минуты, накапливает параллельные копии, конкурирующие за одни строки.
Ставьте блокировку на уровне запуска. Yii 2 предоставляет компонент mutex (например, yii\mutex\FileMutex или MysqlMutex — точную конфигурацию сверьте в документации по разделу Mutex):
public function actionRun()
{
$mutex = \Yii::$app->mutex;
// Не ждём освобождения — если занято, просто выходим
if (!$mutex->acquire('sync-orders')) {
$this->stdout("Предыдущий запуск ещё работает, пропускаю\n");
return ExitCode::OK;
}
try {
$this->doSync();
return ExitCode::OK;
} finally {
$mutex->release('sync-orders');
}
}
Когда что выбирать
- Разовая миграция данных вручную — интерактивный режим,
confirm(), одна транзакция, если объём влезает. - Регулярная фоновая задача под кроном —
--interactive=0, mutex, идемпотентная запись, явные коды выхода,each()вместоall(). - Обработка большого одноразового объёма — пачки в транзакциях, диагностика памяти, прогресс в stdout.
Что настроить дальше
Соберите чек-лист для каждой команды перед выкатом на прод: явный код выхода во всех ветках, stderr для ошибок, mutex для кроновых задач, each() в циклах по большим таблицам, идемпотентность для перезапуска. Настройте мониторинг на код выхода — supervisor или systemd умеют перезапускать по ненулевому коду и слать алерт. И проверьте, что на проде YII_DEBUG действительно выключен: одна эта настройка снимает половину проблем с памятью в длинных командах.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: Yii 2 Guide — Console Applications, Yii 2 API — yii\console\ExitCode
Частые вопросы
У меня Yii 1.1, а не Yii 2 — эти советы подойдут?
Команда всё равно съедает память, хотя я поставил each() — что не так?
А если крон и так шлёт письмо при ошибке — зачем возиться с кодами выхода?
Что будет, если команда упадёт посреди транзакции?
Как запустить консольную команду с отключённым интерактивом?
Как не дать двум копиям команды запуститься одновременно под кроном?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.