Artisan — это интерфейс командной строки Laravel, который предоставляет множество полезных команд для операций разработки и production. Помимо использования встроенных команд, создание пользовательских команд Artisan позволяет автоматизировать повторяющиеся задачи, запланированные задания и разовые операции. Это руководство рассматривает создание профессиональных команд Artisan с точки зрения senior-разработчика.
Почему пользовательские команды важны
Пользовательские команды Artisan позволяют:
- Автоматизировать задачи: повторяющиеся задачи превращаются в однострочные команды
- Запланированные задания: операции наподобие Cron внутри Laravel
- Операции с данными: migrations, imports, exports
- Задачи обслуживания: очистка, управление cache
- Инструменты разработчика: утилиты, специфичные для проекта
Создание команд
Генерация команды
php artisan make:command SendWeeklyReport
Это создаст app/Console/Commands/SendWeeklyReport.php:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SendWeeklyReport extends Command
{
/**
* Имя и сигнатура консольной команды.
*
* @var string
*/
protected $signature = 'report:weekly';
/**
* Описание консольной команды.
*
* @var string
*/
protected $description = 'Send weekly report to all active users';
/**
* Выполнить консольную команду.
*/
public function handle(): int
{
$this->info('Sending weekly reports...');
// Логика команды здесь
$this->info('Reports sent successfully!');
return Command::SUCCESS;
}
}
Сигнатура команды
// Базовая команда
protected $signature = 'report:weekly';
// С обязательным аргументом
protected $signature = 'user:create {email}';
// С необязательным аргументом
protected $signature = 'user:create {email?}';
// Со значением по умолчанию
protected $signature = 'user:create {[email protected]}';
// С опцией (флагом)
protected $signature = 'report:weekly {--queue}';
// С опцией, имеющей значение
protected $signature = 'report:weekly {--format=pdf}';
// С сокращением опции
protected $signature = 'report:weekly {--Q|queue}';
// Аргумент-массив (несколько значений)
protected $signature = 'email:send {users*}';
// Опция-массив
protected $signature = 'email:send {--id=*}';
// С описаниями
protected $signature = 'user:create
{email : The email of the user}
{--admin : Whether the user is an admin}
{--role=user : The role to assign}';
Ввод и вывод команды
Получение ввода
public function handle(): int
{
// Получить аргумент
$email = $this->argument('email');
// Получить все аргументы
$allArgs = $this->arguments();
// Получить опцию
$format = $this->option('format');
// Получить все опции
$allOptions = $this->options();
// Проверить, была ли передана опция (флаг)
if ($this->option('queue')) {
// Обработать queued
}
// Аргументы-массивы
$userIds = $this->argument('users'); // Возвращает массив
return Command::SUCCESS;
}
Запрос ввода
public function handle(): int
{
// Простой вопрос
$name = $this->ask('What is your name?');
// Со значением по умолчанию
$name = $this->ask('What is your name?', 'Guest');
// Скрытый ввод (пароли)
$password = $this->secret('Enter the database password');
// Подтверждение
if ($this->confirm('Do you wish to continue?')) {
// Пользователь подтвердил
}
// Подтверждение со значением по умолчанию
if ($this->confirm('Continue?', true)) {
// По умолчанию — да
}
// Выбор/список
$role = $this->choice(
'What is your role?',
['admin', 'user', 'guest'],
0 // Индекс по умолчанию
);
// Множественный выбор
$permissions = $this->choice(
'Select permissions',
['read', 'write', 'delete'],
null,
null,
true // Разрешить множественный выбор
);
// Anticipate (автодополнение)
$name = $this->anticipate('What is your name?', ['Taylor', 'Jeffrey']);
return Command::SUCCESS;
}
Вывод
public function handle(): int
{
// Стандартный вывод
$this->line('Plain text');
$this->info('Informational message');
$this->comment('Comment style');
$this->question('Question style');
$this->error('Error message');
$this->warn('Warning message');
$this->newLine();
$this->newLine(3); // 3 пустые строки
// Вывод таблицы
$this->table(
['Name', 'Email', 'Role'],
[
['John', '[email protected]', 'admin'],
['Jane', '[email protected]', 'user'],
]
);
// Индикатор прогресса
$users = User::all();
$bar = $this->output->createProgressBar(count($users));
$bar->start();
foreach ($users as $user) {
$this->processUser($user);
$bar->advance();
}
$bar->finish();
$this->newLine();
// Или проще — с callback
$this->withProgressBar($users, function ($user) {
$this->processUser($user);
});
return Command::SUCCESS;
}
Практические примеры
Команда импорта данных
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class ImportUsers extends Command
{
protected $signature = 'users:import
{file : Path to CSV file}
{--dry-run : Show what would be imported without saving}';
protected $description = 'Import users from CSV file';
public function handle(): int
{
$file = $this->argument('file');
$dryRun = $this->option('dry-run');
if (!file_exists($file)) {
$this->error("File not found: {$file}");
return Command::FAILURE;
}
$csv = array_map('str_getcsv', file($file));
$header = array_shift($csv);
$this->info(sprintf('Found %d users to import', count($csv)));
if ($dryRun) {
$this->warn('DRY RUN - No changes will be made');
}
$imported = 0;
$skipped = 0;
DB::beginTransaction();
try {
$this->withProgressBar($csv, function ($row) use ($header, $dryRun, &$imported, &$skipped) {
$data = array_combine($header, $row);
if (User::where('email', $data['email'])->exists()) {
$skipped++;
return;
}
if (!$dryRun) {
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password'] ?? 'changeme'),
]);
}
$imported++;
});
if (!$dryRun) {
DB::commit();
} else {
DB::rollBack();
}
$this->newLine();
$this->info("Imported: {$imported}, Skipped: {$skipped}");
return Command::SUCCESS;
} catch (\Exception $e) {
DB::rollBack();
$this->error("Import failed: {$e->getMessage()}");
return Command::FAILURE;
}
}
}
Команда очистки
<?php
namespace App\Console\Commands;
use App\Models\Session;
use App\Models\PasswordReset;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanupOldData extends Command
{
protected $signature = 'cleanup:old-data
{--days=30 : Delete data older than this many days}
{--force : Skip confirmation}';
protected $description = 'Clean up old sessions, password resets, and temp files';
public function handle(): int
{
$days = (int) $this->option('days');
$cutoff = now()->subDays($days);
$this->info("Cleaning up data older than {$days} days ({$cutoff})");
if (!$this->option('force') && !$this->confirm('Continue?')) {
$this->info('Cancelled.');
return Command::SUCCESS;
}
// Очистить сессии
$sessions = Session::where('last_activity', '<', $cutoff->timestamp)->count();
Session::where('last_activity', '<', $cutoff->timestamp)->delete();
$this->info("Deleted {$sessions} old sessions");
// Очистить сбросы пароля
$resets = PasswordReset::where('created_at', '<', $cutoff)->count();
PasswordReset::where('created_at', '<', $cutoff)->delete();
$this->info("Deleted {$resets} old password resets");
// Очистить временные файлы
$files = Storage::files('temp');
$deleted = 0;
foreach ($files as $file) {
$lastModified = Storage::lastModified($file);
if ($lastModified < $cutoff->timestamp) {
Storage::delete($file);
$deleted++;
}
}
$this->info("Deleted {$deleted} temp files");
return Command::SUCCESS;
}
}
Планирование команд
Регистрация в Kernel
app/Console/Kernel.php:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
Commands\SendWeeklyReport::class,
Commands\CleanupOldData::class,
];
protected function schedule(Schedule $schedule): void
{
// Запускать ежедневно в полночь
$schedule->command('cleanup:old-data --force')
->daily()
->at('00:00');
// Запускать еженедельно по понедельникам
$schedule->command('report:weekly')
->weeklyOn(1, '8:00');
// Запускать каждый час
$schedule->command('stats:update')
->hourly();
// Запускать каждые 5 минут
$schedule->command('queue:work --stop-when-empty')
->everyFiveMinutes()
->withoutOverlapping();
// Запускать при выполнении условий
$schedule->command('backup:database')
->daily()
->environments(['production'])
->onSuccess(function () {
// Уведомлять об успехе
})
->onFailure(function () {
// Оповещать о сбое
});
// Выводить в лог
$schedule->command('reports:generate')
->daily()
->appendOutputTo(storage_path('logs/reports.log'));
}
}
Настройка Cron
Добавьте в crontab сервера:
# Запускать Laravel scheduler каждую минуту
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1
Настройка Cron в Docker
docker/laravel-schedule:
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1
# Сохранить пустую строку в конце
Dockerfile:
COPY docker/laravel-schedule /etc/cron.d/laravel-schedule
RUN chmod 0644 /etc/cron.d/laravel-schedule && \
crontab /etc/cron.d/laravel-schedule
# Запускать cron в entrypoint
CMD service cron start && php-fpm
Вызов команд программно
use Illuminate\Support\Facades\Artisan;
// Вызвать команду
Artisan::call('users:import', [
'file' => '/path/to/file.csv',
'--dry-run' => true,
]);
// Получить вывод
$output = Artisan::output();
// Поставить команду в очередь
Artisan::queue('report:weekly');
// Из другой команды
public function handle(): int
{
$this->call('cache:clear');
// С аргументами
$this->call('user:create', [
'email' => '[email protected]',
'--admin' => true,
]);
// Тихо (без вывода)
$this->callSilently('migrate');
return Command::SUCCESS;
}
Ключевые выводы
- Используйте описательные сигнатуры: понятные имена и описания аргументов
- Корректно обрабатывайте ошибки: возвращайте соответствующие коды завершения
- Поддерживайте режим dry-run: позволяйте пользователям предварительно просматривать разрушительные операции
- Используйте индикаторы прогресса: визуальная обратная связь для длительных операций
- Планируйте с ограничениями: используйте
withoutOverlapping() для безопасности - Логируйте вывод команды: отслеживайте результаты запланированных команд
Команды Artisan — мощные инструменты автоматизации: грамотно спроектированные команды экономят часы ручной работы и снижают риск человеческих ошибок.