Artisan - это CLI-интерфейс Laravel. Помимо встроенных команд, Laravel позволяет создавать собственные команды для автоматизации задач, обработки данных, обслуживания и многого другого.
Создание команд
php artisan make:command SendWeeklyReports
# Creates: app/Console/Commands/SendWeeklyReports.php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use App\Services\ReportService;
use Illuminate\Console\Command;
final class SendWeeklyReports extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'reports:send-weekly
{--type=summary : Report type (summary, detailed, executive)}
{--dry-run : Preview without sending}';
/**
* The console command description.
*/
protected $description = 'Send weekly reports to all subscribed users';
public function __construct(
private readonly ReportService $reportService,
) {
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle(): int
{
$type = $this->option('type');
$dryRun = $this->option('dry-run');
$this->info("Sending {$type} weekly reports...");
if ($dryRun) {
$this->warn('DRY RUN - no emails will be sent.');
}
$users = User::where('weekly_reports', true)->get();
$bar = $this->output->createProgressBar($users->count());
$bar->start();
$sent = 0;
foreach ($users as $user) {
if (!$dryRun) {
$this->reportService->sendWeeklyReport($user, $type);
}
$sent++;
$bar->advance();
}
$bar->finish();
$this->newLine(2);
$this->info("Successfully processed {$sent} reports.");
return Command::SUCCESS;
}
}
Сигнатура команды: аргументы и опции
Аргументы
protected $signature = 'users:import
{file : Path to the CSV file}
{format? : File format (csv, xlsx) - optional}
{--chunk=100 : Number of records per batch}';
// Required argument
// users:import /path/to/users.csv
// Optional argument
// users:import /path/to/users.csv xlsx
// With default value
protected $signature = 'users:import
{file : Path to the CSV file}
{format=csv : File format}';
Опции
protected $signature = 'deploy:assets
{--force : Force overwrite existing files}
{--env= : Target environment}
{--path=public : Output directory}
{--tag=* : Tags to include (repeatable)}';
// Boolean option (flag)
// php artisan deploy:assets --force
// Option with value
// php artisan deploy:assets --env=production
// Option with default
// php artisan deploy:assets (path defaults to "public")
// Repeatable option
// php artisan deploy:assets --tag=css --tag=js --tag=images
Массив аргументов
protected $signature = 'users:notify {users* : User IDs to notify}';
// php artisan users:notify 1 2 3 4 5
protected $signature = 'email:send {to*} {--cc=*}';
// php artisan email:send [email protected] [email protected] [email protected]
Получение аргументов и опций
public function handle(): int
{
// Get argument
$file = $this->argument('file');
// Get all arguments
$allArgs = $this->arguments();
// Get option
$environment = $this->option('env');
$force = $this->option('force'); // bool for flags
// Get all options
$allOptions = $this->options();
// Get array option
$tags = $this->option('tag'); // ['css', 'js', 'images']
return Command::SUCCESS;
}
Ввод-вывод (I/O)
Вывод информации
public function handle(): int
{
// Standard output methods
$this->info('Information message'); // Green text
$this->error('Error message'); // Red background
$this->warn('Warning message'); // Yellow text
$this->comment('Comment message'); // Yellow text
$this->question('Question message'); // Black text on cyan
$this->line('Plain text'); // No formatting
$this->newLine(2); // Empty lines
// With verbosity levels
$this->info('Always shown');
$this->info('Shown with -v', verbosity: 'v');
$this->info('Shown with -vv', verbosity: 'vv');
$this->info('Shown with -vvv', verbosity: 'vvv');
return Command::SUCCESS;
}
Интерактивный ввод
public function handle(): int
{
// Ask for input
$name = $this->ask('What is your name?');
$name = $this->ask('What is your name?', 'Default Name');
// Secret input (hidden)
$password = $this->secret('Enter the database password');
// Confirmation (yes/no)
if ($this->confirm('Do you want to continue?')) {
$this->info('Proceeding...');
}
// Default to yes
if ($this->confirm('Deploy to production?', true)) {
// Deploys by default if user just presses Enter
}
// Autocomplete
$framework = $this->anticipate('Which framework?', [
'Laravel', 'Symfony', 'CodeIgniter',
]);
// Choice (select)
$color = $this->choice('Pick a color', [
'red', 'green', 'blue',
], defaultIndex: 0);
// Multiple choice
$permissions = $this->choice(
'Select permissions',
['read', 'write', 'execute'],
multiple: true
);
return Command::SUCCESS;
}
Таблицы
public function handle(): int
{
$users = User::all(['id', 'name', 'email', 'created_at']);
$this->table(
['ID', 'Name', 'Email', 'Created'],
$users->map(fn (User $user) => [
$user->id,
$user->name,
$user->email,
$user->created_at->format('Y-m-d'),
])
);
return Command::SUCCESS;
}
Прогресс-бар
public function handle(): int
{
$items = Order::where('status', 'pending')->get();
$bar = $this->output->createProgressBar($items->count());
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%');
$bar->start();
foreach ($items as $item) {
$this->processItem($item);
$bar->advance();
}
$bar->finish();
$this->newLine();
// Or use withProgressBar helper
$this->withProgressBar($items, function (Order $order) {
$this->processItem($order);
});
return Command::SUCCESS;
}
Коды возврата
use Illuminate\Console\Command;
public function handle(): int
{
try {
// Process...
return Command::SUCCESS; // 0
} catch (ValidationException $e) {
$this->error($e->getMessage());
return Command::FAILURE; // 1
} catch (\Exception $e) {
$this->error('Unexpected error: ' . $e->getMessage());
return Command::INVALID; // 2
}
}
Вызов команд программно
Из контроллера или сервиса
use Illuminate\Support\Facades\Artisan;
// Basic call
Artisan::call('reports:send-weekly', [
'--type' => 'detailed',
'--dry-run' => true,
]);
// Get output
$output = Artisan::output();
// Queue the command
Artisan::queue('reports:send-weekly', [
'--type' => 'summary',
])->onQueue('reports');
Из другой команды
public function handle(): int
{
// Call another command
$this->call('cache:clear');
// Call silently (suppress output)
$this->callSilently('config:cache');
// Call with arguments
$this->call('users:import', [
'file' => '/path/to/file.csv',
'--format' => 'xlsx',
]);
return Command::SUCCESS;
}
Регистрация команд в планировщике
В Laravel 11 регистрация расписания происходит в routes/console.php:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
// Run command every day at midnight
Schedule::command('reports:send-weekly --type=summary')
->weeklyOn(1, '8:00') // Every Monday at 8:00
->timezone('Europe/Moscow')
->withoutOverlapping() // Prevent parallel execution
->onOneServer() // Run on only one server (requires cache driver)
->emailOutputOnFailure('[email protected]');
// Run with closure
Schedule::call(function () {
DB::table('sessions')
->where('last_activity', '<', now()->subDay())
->delete();
})->daily()->description('Clean expired sessions');
// Common schedule frequencies
Schedule::command('telescope:prune')->daily();
Schedule::command('queue:restart')->hourly();
Schedule::command('backup:run')->dailyAt('02:00');
Schedule::command('sitemap:generate')->weekly();
Schedule::command('reports:monthly')->monthly();
Schedule::command('cache:warm')->everyFiveMinutes();
Schedule::command('health:check')->everyMinute();
// Cron expression
Schedule::command('custom:task')->cron('0 */6 * * *');
// Conditional scheduling
Schedule::command('import:currency-rates')
->hourly()
->when(fn () => app()->environment('production'))
->before(fn () => Log::info('Starting currency import'))
->after(fn () => Log::info('Currency import completed'));
// Output handling
Schedule::command('reports:generate')
->daily()
->sendOutputTo(storage_path('logs/reports.log'))
->emailOutputTo('[email protected]');
Запуск планировщика
# Add to crontab (single entry):
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1
# Test schedule locally:
php artisan schedule:work
# List scheduled tasks:
php artisan schedule:list
# Run specific scheduled command:
php artisan schedule:test
Изолированные команды (Isolated Commands)
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;
final class ImportProducts extends Command implements Isolatable
{
protected $signature = 'products:import {source}';
protected $description = 'Import products from external source';
/**
* Unique ID for isolation lock.
*/
public function isolatableId(): string
{
return $this->argument('source');
}
/**
* Return code when command is already running.
*/
public function isolationLockExpiresAt(): \DateTimeInterface
{
return now()->addHours(2);
}
public function handle(): int
{
// Only one instance with the same source can run at a time
$this->info('Importing from: ' . $this->argument('source'));
// Process...
return Command::SUCCESS;
}
}
Сигналы (Signal Handling)
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\Signals;
final class LongRunningProcess extends Command
{
protected $signature = 'process:run';
protected $description = 'Long running process with graceful shutdown';
private bool $shouldStop = false;
public function handle(): int
{
// Register signal handlers
$this->trap([SIGTERM, SIGINT], function () {
$this->warn('Received stop signal, finishing current batch...');
$this->shouldStop = true;
});
while (!$this->shouldStop) {
$batch = $this->getNextBatch();
if ($batch->isEmpty()) {
sleep(5);
continue;
}
$this->processBatch($batch);
}
$this->info('Gracefully stopped.');
return Command::SUCCESS;
}
}
Тестирование команд
declare(strict_types=1);
namespace Tests\Feature\Commands;
use Tests\TestCase;
final class SendWeeklyReportsTest extends TestCase
{
public function test_command_sends_reports(): void
{
User::factory()->count(3)->create(['weekly_reports' => true]);
$this->artisan('reports:send-weekly', ['--type' => 'summary'])
->expectsOutput('Sending summary weekly reports...')
->expectsOutput('Successfully processed 3 reports.')
->assertExitCode(0);
}
public function test_dry_run_doesnt_send(): void
{
$this->artisan('reports:send-weekly', ['--dry-run' => true])
->expectsOutput('DRY RUN - no emails will be sent.')
->assertExitCode(0);
}
public function test_interactive_command(): void
{
$this->artisan('users:create')
->expectsQuestion('What is the user name?', 'John')
->expectsQuestion('What is the email?', '[email protected]')
->expectsConfirmation('Create this user?', 'yes')
->expectsOutput('User created successfully.')
->assertExitCode(0);
}
public function test_command_with_choice(): void
{
$this->artisan('deploy:assets')
->expectsChoice('Pick environment', 'staging', ['local', 'staging', 'production'])
->assertExitCode(0);
}
public function test_command_with_table(): void
{
User::factory()->create(['name' => 'John', 'email' => '[email protected]']);
$this->artisan('users:list')
->expectsTable(['ID', 'Name', 'Email'], [
[1, 'John', '[email protected]'],
])
->assertExitCode(0);
}
}