В Laravel довольно легко написать консольную команду, внутри которой постепенно оказывается вся бизнес-логика: получение данных, обработка, запросы к API, запись в базу и логирование.
Для небольшого скрипта это нормально. Но если задача растёт, удобнее разделить ответственность:
Command отвечает за запуск, Job — за выполнение работы.
Так одну и ту же задачу можно запускать из консоли, очереди, планировщика или из кода приложения.
Простейший пример
Допустим, нам нужно пересчитать статистику.
Создадим Job:
php artisan make:job RecalculateStatistics
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class RecalculateStatistics implements ShouldQueue
{
use Queueable;
public function handle(): void
{
// Получение данных
// Расчёты
// Сохранение результата
}
}
И консольную команду:
php artisan make:command RecalculateStatisticsCommand
namespace App\Console\Commands;
use App\Jobs\RecalculateStatistics;
use Illuminate\Console\Command;
class RecalculateStatisticsCommand extends Command
{
protected $signature = 'statistics:recalculate';
protected $description = 'Пересчитать статистику';
public function handle(): int
{
RecalculateStatistics::dispatch();
$this->info('Задача добавлена в очередь.');
return self::SUCCESS;
}
}
Теперь команда ничего не знает о том, как именно пересчитывается статистика. Она только инициирует операцию.
php artisan statistics:recalculate
А если очередь не нужна?
Разделение Command и Job полезно даже тогда, когда задача должна выполняться прямо сейчас.
Job можно выполнить синхронно:
public function handle(): int
{
RecalculateStatistics::dispatchSync();
$this->info('Готово.');
return self::SUCCESS;
}
Получается удобная схема:
Command
↓
Job
↓
Business logic
При этом способ запуска можно поменять практически без изменения самой задачи.
Например, сегодня:
RecalculateStatistics::dispatchSync();
а завтра:
RecalculateStatistics::dispatch();
Параметры тоже удобно передавать через Command
Например:
protected $signature = 'statistics:recalculate
{date?}
{--force}';
Команда разбирает CLI-параметры:
public function handle(): int
{
RecalculateStatistics::dispatchSync(
date: $this->argument('date'),
force: $this->option('force'),
);
return self::SUCCESS;
}
А Job получает уже нормальные значения:
class RecalculateStatistics
{
public function __construct(
public ?string $date = null,
public bool $force = false,
) {}
public function handle(): void
{
// ...
}
}
Это ещё одна причина не складывать всё в Command: детали CLI остаются на уровне CLI.
Долгие команды
Иногда задача занимает несколько минут или даже часов.
Например:
php artisan statistics:recalculate
Если она выполняется синхронно, терминал должен оставаться открытым до завершения процесса.
На сервере такую команду можно запустить через nohup:
nohup php artisan statistics:recalculate > storage/logs/statistics.log 2>&1 &
Разберём эту конструкцию.
nohup позволяет процессу продолжить работу после закрытия SSH-сессии.
> storage/logs/statistics.log
перенаправляет стандартный вывод в файл.
2>&1
отправляет туда же stderr.
А последний:
&
запускает процесс в фоне.
В результате можно подключиться по SSH, запустить большую операцию и отключиться:
nohup php artisan statistics:recalculate \
> storage/logs/statistics.log 2>&1 &
Смотреть выполнение в реальном времени
Лог можно открыть через:
tail -f storage/logs/statistics.log
Если внутри команды используется:
$this->info('Processing users...');
или обычный вывод в stdout, он будет попадать в этот файл.
Это особенно удобно для разовых миграций данных, импортов, пересчётов и других административных задач.
Узнать PID процесса
После запуска shell обычно выводит PID:
[1] 28451
Также можно сразу получить его:
nohup php artisan statistics:recalculate \
> storage/logs/statistics.log 2>&1 & echo $!
И при необходимости проверить процесс:
ps -p 28451
или остановить:
kill 28451
nohup или очередь?
Важно не путать эти два механизма.
nohup — это способ оставить обычный Linux-процесс работающим после закрытия терминала.
Laravel Queue — полноценный механизм фонового выполнения задач.
Если операция является постоянной частью приложения, обычно лучше:
RecalculateStatistics::dispatch();
и запущенный queue worker под Supervisor или systemd.
Очередь даёт намного больше возможностей: retries, timeout, failed jobs, несколько воркеров, разные очереди и контроль выполнения.
nohup хорош для другого сценария — разовой большой административной команды.
Например:
nohup php artisan catalog:reindex > storage/logs/reindex.log 2>&1 &
или:
nohup php artisan legacy:import > storage/logs/import.log 2>&1 &
Для таких операций поднимать отдельную инфраструктуру часто просто незачем.
Command, Job и Service
Если логика становится ещё больше, не обязательно переносить сотни строк непосредственно в Job.
Можно сделать ещё один уровень:
Command
↓
Job
↓
Service
Например:
class RecalculateStatistics
{
public function handle(StatisticsService $service): void
{
$service->recalculate();
}
}
Тогда роли становятся совсем прозрачными.
Command отвечает за интерфейс запуска:
php artisan statistics:recalculate
Job отвечает за выполнение задачи и при необходимости за работу с очередью.
Service содержит непосредственно бизнес-логику.
При этом не нужно превращать это в обязательный архитектурный ритуал. Если Job состоит из понятных 30 строк кода, отдельный Service может ничего не дать.
Запуск из Scheduler
Ещё один плюс такого разделения — задачу легко запускать автоматически.
Можно запланировать непосредственно Job:
use App\Jobs\RecalculateStatistics;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new RecalculateStatistics)
->dailyAt('03:00');
Или оставить Scheduler на уровне консольной команды:
Schedule::command('statistics:recalculate')
->dailyAt('03:00');
Какой вариант выбрать — зависит от того, является ли Command самостоятельной точкой входа со своей логикой запуска или просто тонкой оболочкой.
Практическое правило
Для маленькой одноразовой команды вполне нормально написать:
public function handle(): int
{
// 20 строк простой логики
return self::SUCCESS;
}
Не нужно создавать Job только ради самого факта наличия Job.
Но когда команда начинает разрастаться, выполняется долго или ту же операцию потенциально потребуется запускать из других мест, удобно перейти к схеме:
artisan command
↓
Job
↓
Service
Тогда консоль остаётся просто одним из способов запуска задачи.
А для большой разовой операции на сервере всегда остаётся простой вариант:
nohup php artisan my:command > storage/logs/my-command.log 2>&1 &
Это хорошо закрывает промежуток между «запустить руками в SSH» и «строить полноценный постоянно работающий механизм фоновых задач».


Добавить комментарий