Консольная команда как оболочка над Job в Laravel

от автора

в
Время чтения: 3 мин.

В 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» и «строить полноценный постоянно работающий механизм фоновых задач».


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Сколько будет 1 + 6?