Своя artisan-команда в Laravel: как объявить и где что хранить
Три способа задать сигнатуру, аргументы и опции в artisan-команде Laravel 11.x — с ценой каждого варианта и критерием выбора для реального проекта.

Вы пишете команду php artisan report:generate, которая должна принимать месяц, флаг --force и список email для рассылки. Вопрос не «как создать класс» — это делает make:command за секунду. Вопрос в том, как объявить входные параметры так, чтобы через полгода их можно было читать без открытия исходников. В Laravel 11.x на это есть минимум три способа, и они не эквивалентны. Разберём, чем каждый платит.
Что вообще решает artisan-команда
Команда — это класс, наследующий Illuminate\Console\Command, с двумя ключевыми полями: $signature (или устаревающая пара $name + getArguments()) и $description. Логика живёт в методе handle(). Всё остальное — вопрос того, как вы описываете вход.
Скелет создаётся стандартно:
php artisan make:command GenerateReport
Файл появится в app/Console/Commands/GenerateReport.php. В Laravel 11.x команды из этой папки регистрируются автоматически — отдельный вызов load() в Console\Kernel больше не нужен, самого файла ядра в новой структуре нет. Дальше начинается развилка: как описать аргументы и опции.
Вариант 1: строка $signature
Самый распространённый способ. Вся сигнатура — одна строка с DSL Laravel:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GenerateReport extends Command
{
// month — обязательный аргумент, emails — массив, force — булев флаг
protected $signature = 'report:generate
{month : Месяц в формате YYYY-MM}
{emails?* : Получатели рассылки}
{--force : Перезаписать существующий отчёт}
{--format=pdf : Формат выгрузки}';
protected $description = 'Собирает месячный отчёт и рассылает получателям';
public function handle(): int
{
$month = $this->argument('month');
$emails = $this->argument('emails'); // массив, возможно пустой
$force = $this->option('force'); // bool
$format = $this->option('format'); // 'pdf' по умолчанию
$this->info("Готовлю отчёт за {$month} в формате {$format}");
return self::SUCCESS;
}
}
Синтаксис DSL стоит запомнить, потому что он неочевиден:
{month}— обязательный аргумент;{month?}— необязательный;{emails*}— массив (можно передать несколько значений);{emails?*}— необязательный массив;{month=2025-01}— со значением по умолчанию;{--force}— флаг без значения, вернётtrue/false;{--format=pdf}— опция со значением по умолчанию;{--M|month=}— опция с короткой формой-M.
Плюс: вся сигнатура видна целиком в одном месте, и php artisan help report:generate сразу покажет описания после двоеточия. Минус: у строки нет проверки на этапе редактора. Опечатка в {--fromat=pdf} не подсветится нигде — вы узнаете о ней, когда $this->option('format') вернёт не то. И чем больше параметров, тем хуже читается многострочный литерал.
Вариант 2: методы getArguments() и getOptions()
Старый способ, доставшийся от Symfony Console. Вместо строки вы описываете параметры массивами объектов режима:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class GenerateReport extends Command
{
protected $name = 'report:generate';
protected $description = 'Собирает месячный отчёт и рассылает получателям';
protected function getArguments(): array
{
return [
['month', InputArgument::REQUIRED, 'Месяц в формате YYYY-MM'],
['emails', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Получатели'],
];
}
protected function getOptions(): array
{
return [
['force', null, InputOption::VALUE_NONE, 'Перезаписать отчёт'],
['format', null, InputOption::VALUE_OPTIONAL, 'Формат', 'pdf'],
];
}
public function handle(): int
{
// доступ к параметрам такой же, как в варианте 1
return self::SUCCESS;
}
}
Обратите внимание: здесь используется $name, а не $signature. Смешивать их нельзя — если задать $signature, методы getArguments() и getOptions() игнорируются, потому что строка сама их формирует. Значения по-прежнему читаются через argument() и option().
Плюс: константы InputArgument::REQUIRED и InputOption::VALUE_NONE явные, ошибиться в них сложнее, чем в символах ? и *. Битовая маска IS_ARRAY | OPTIONAL прямо говорит, что параметр — необязательный массив. Минус: многословно. Пять параметров превращаются в две простыни, и описание команды растекается по классу.
Строковый$signature— это сахар над теми же объектамиInputArgumentиInputOption. Laravel парсит строку в конструкторе и вызывает те же методы. Вы не выбираете между «мощным» и «простым» — вы выбираете между «коротким и без проверки» и «длинным и явным».
Вариант 3: PromptsForMissingInput для интерактива
Если команду запускают руками, а не из cron, забытый обязательный аргумент роняет её с ошибкой Not enough arguments. Начиная с Laravel 9.x есть трейт, который вместо падения спрашивает недостающее:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class GenerateReport extends Command implements PromptsForMissingInput
{
protected $signature = 'report:generate {month : Месяц в формате YYYY-MM}';
protected $description = 'Собирает месячный отчёт';
// Тексты подсказок для пропущенных обязательных аргументов
protected function promptForMissingArgumentsUsing(): array
{
return [
'month' => 'За какой месяц собрать отчёт? (YYYY-MM)',
];
}
public function handle(): int
{
$this->info('Месяц: '.$this->argument('month'));
return self::SUCCESS;
}
}
Запуск php artisan report:generate без аргумента теперь выдаст интерактивный вопрос вместо ошибки. Важная оговорка: в неинтерактивной среде (флаг --no-interaction или запуск из планировщика без TTY) подсказка не сработает, и вы снова получите падение. Для команд, живущих только в cron, этот трейт бесполезен — там нужна валидация внутри handle().
Сравнение вариантов
| Критерий | $signature | getArguments/getOptions | PromptsForMissingInput |
|---|---|---|---|
| Многословность | минимальная | высокая | как у выбранной базы |
| Проверка опечаток редактором | нет | частичная (константы) | нет |
| Читается целиком в одном месте | да | нет | да |
| Спрашивает пропущенное | нет | нет | да, только в TTY |
| Годится для cron | да | да | да, но интерактив не нужен |
Когда что выбирать
- До пяти-шести параметров, команда для cron или CI — берите
$signature. Компактно, вся сигнатура читается разом, интерактив всё равно не понадобится. - Десяток параметров с хитрыми режимами (несколько массивов, опции с короткими алиасами и значениями по умолчанию) —
getArguments()/getOptions()читаются лучше, чем перегруженный строковый литерал, и константы страхуют от опечаток. - Команда, которую запускают руками разные люди — добавьте
PromptsForMissingInputповерх$signature. Забытый аргумент превратится в вопрос, а не в ошибку.
Валидация значений — отдельная тема; ни один из трёх вариантов не проверяет, что month действительно в формате YYYY-MM. Это делается уже в handle() или через Validator.
Что ломается на практике
- Массив без звёздочки.
{emails}примет только одно значение, а несколько вернут ошибку. Массив — это{emails*}. - Опция-флаг вернула строку. Если написать
{--force=}вместо{--force}, artisan ждёт значение, иoption('force')вернёт не bool. Флаг без значения — без=. - Команда не появилась в списке. В Laravel 11.x проверьте, что файл лежит именно в
app/Console/Commands. Из других папок автозагрузка не подхватит — регистрируйте вручную вroutes/console.phpили черезwithCommands()вbootstrap/app.php. - Изменили сигнатуру, а help показывает старую. Сбросьте кеш:
php artisan clear-compiledи, если включён,php artisan optimize:clear.
Что сделать дальше
Выбрали способ объявления — переходите к трём вещам, которые превращают учебную команду в рабочую. Первое: возвращайте из handle() корректный код (self::SUCCESS / self::FAILURE), иначе CI и cron не отличат успех от ошибки. Второе: если команда пойдёт в планировщик, опишите расписание в routes/console.php через Schedule::command() и проверьте, что она переживает --no-interaction. Третье: покройте handle() тестом через $this->artisan('report:generate', [...])->assertExitCode(0) — это дешевле, чем ловить регрессию на проде. Точный синтаксис планировщика и тестовых хелперов сверяйте с разделами Console и Testing в документации вашей версии.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Laravel Documentation — Artisan Console
Частые вопросы
У меня Laravel 10.x, а не 11.x — команды из примеров сработают?
Команда не появляется в php artisan list — что проверить?
Можно ли смешать $signature и getArguments() в одном классе?
Почему option('force') возвращает false, хотя я передал --force?
PromptsForMissingInput спросит аргумент в cron?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.