#Введение
Раньше для каждой задачи, которую нужно было запланировать на сервере, приходилось писать отдельную запись в cron. Однако это быстро становится неудобным, так как расписание задач больше не находится под контролем версий, и для просмотра или добавления записей cron нужно подключаться к серверу по SSH.
Планировщик команд Laravel предлагает новый подход к управлению запланированными задачами на сервере. Он позволяет удобно и выразительно определять расписание команд прямо в вашем приложении Laravel. При использовании планировщика на сервере требуется всего одна запись в cron. Расписание задач определяется в методе schedule файла app/Console/Kernel.php. Для начала в методе приведён простой пример.
#Определение расписаний
Все запланированные задачи можно определить в методе schedule класса App\Console\Kernel вашего приложения. Для начала рассмотрим пример. В этом примере мы запланируем вызов замыкания каждый день в полночь. Внутри замыкания будет выполнен запрос к базе данных для очистки таблицы:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;
class Kernel extends ConsoleKernel
{
/**
* Определить расписание команд приложения.
*/
protected function schedule(Schedule $schedule): void
{
$schedule->call(function () {
DB::table('recent_users')->delete();
})->daily();
}
}
Помимо планирования с помощью замыканий, вы также можете планировать вызываемые объекты. Вызываемые объекты — это простые PHP-классы с методом __invoke:
$schedule->call(new DeleteRecentUsers)->daily();
Если вы хотите просмотреть обзор запланированных задач и время их следующего запуска, можно использовать Artisan-команду schedule:list:
php artisan schedule:list
#Планирование Artisan-команд
Помимо замыканий, вы можете планировать Artisan-команды и системные команды. Например, метод command позволяет запланировать Artisan-команду по её имени или классу.
При планировании Artisan-команд по имени класса можно передать массив дополнительных аргументов командной строки, которые будут переданы команде при вызове:
use App\Console\Commands\SendEmailsCommand;
$schedule->command('emails:send Taylor --force')->daily();
$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();
#Планирование очередей заданий
Метод job позволяет планировать очередные задания. Это удобный способ планировать задания без использования метода call и определения замыканий для постановки задания в очередь:
use App\Jobs\Heartbeat;
$schedule->job(new Heartbeat)->everyFiveMinutes();
Второй и третий необязательные аргументы метода job позволяют указать имя очереди и соединение очереди, которые будут использоваться для постановки задания:
use App\Jobs\Heartbeat;
// Отправить задание в очередь "heartbeats" через соединение "sqs"...
$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();
#Планирование shell-команд
Метод exec позволяет выполнить команду операционной системы:
$schedule->exec('node /home/forge/script.js')->daily();
#Опции частоты расписания
Мы уже видели несколько примеров настройки задач для запуска с определённой периодичностью. Однако существует гораздо больше вариантов частоты запуска задач:
| Метод | Описание |
|---|---|
->cron('* * * * *'); |
Запуск задачи по произвольному расписанию cron |
->everySecond(); |
Запуск задачи каждую секунду |
->everyTwoSeconds(); |
Запуск задачи каждые две секунды |
->everyFiveSeconds(); |
Запуск задачи каждые пять секунд |
->everyTenSeconds(); |
Запуск задачи каждые десять секунд |
->everyFifteenSeconds(); |
Запуск задачи каждые пятнадцать секунд |
->everyTwentySeconds(); |
Запуск задачи каждые двадцать секунд |
->everyThirtySeconds(); |
Запуск задачи каждые тридцать секунд |
->everyMinute(); |
Запуск задачи каждую минуту |
->everyTwoMinutes(); |
Запуск задачи каждые две минуты |
->everyThreeMinutes(); |
Запуск задачи каждые три минуты |
->everyFourMinutes(); |
Запуск задачи каждые четыре минуты |
->everyFiveMinutes(); |
Запуск задачи каждые пять минут |
->everyTenMinutes(); |
Запуск задачи каждые десять минут |
->everyFifteenMinutes(); |
Запуск задачи каждые пятнадцать минут |
->everyThirtyMinutes(); |
Запуск задачи каждые тридцать минут |
->hourly(); |
Запуск задачи каждый час |
->hourlyAt(17); |
Запуск задачи каждый час в 17 минут |
->everyOddHour($minutes = 0); |
Запуск задачи в каждый нечётный час |
->everyTwoHours($minutes = 0); |
Запуск задачи каждые два часа |
->everyThreeHours($minutes = 0); |
Запуск задачи каждые три часа |
->everyFourHours($minutes = 0); |
Запуск задачи каждые четыре часа |
->everySixHours($minutes = 0); |
Запуск задачи каждые шесть часов |
->daily(); |
Запуск задачи каждый день в полночь |
->dailyAt('13:00'); |
Запуск задачи каждый день в 13:00 |
->twiceDaily(1, 13); |
Запуск задачи дважды в день в 1:00 и 13:00 |
->twiceDailyAt(1, 13, 15); |
Запуск задачи дважды в день в 1:15 и 13:15 |
->weekly(); |
Запуск задачи каждое воскресенье в 00:00 |
->weeklyOn(1, '8:00'); |
Запуск задачи каждую неделю в понедельник в 8:00 |
->monthly(); |
Запуск задачи в первый день каждого месяца в 00:00 |
->monthlyOn(4, '15:00'); |
Запуск задачи каждый месяц 4-го числа в 15:00 |
->twiceMonthly(1, 16, '13:00'); |
Запуск задачи дважды в месяц 1-го и 16-го числа в 13:00 |
->lastDayOfMonth('15:00'); |
Запуск задачи в последний день месяца в 15:00 |
->quarterly(); |
Запуск задачи в первый день каждого квартала в 00:00 |
->quarterlyOn(4, '14:00'); |
Запуск задачи каждый квартал 4-го числа в 14:00 |
->yearly(); |
Запуск задачи в первый день каждого года в 00:00 |
->yearlyOn(6, 1, '17:00'); |
Запуск задачи каждый год 1 июня в 17:00 |
->timezone('America/New_York'); |
Установить часовой пояс для задачи |
Эти методы можно комбинировать с дополнительными ограничениями, чтобы создавать более точные расписания, которые запускаются только в определённые дни недели. Например, можно запланировать команду на еженедельный запуск по понедельникам:
// Запускать раз в неделю по понедельникам в 13:00...
$schedule->call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// Запускать каждый час с 8:00 до 17:00 по будням...
$schedule->command('foo')
->weekdays()
->hourly()
->timezone('America/Chicago')
->between('8:00', '17:00');
Ниже приведён список дополнительных ограничений расписания:
| Метод | Описание |
|---|---|
->weekdays(); |
Ограничить задачу будними днями |
->weekends(); |
Ограничить задачу выходными |
->sundays(); |
Ограничить задачу воскресеньем |
->mondays(); |
Ограничить задачу понедельником |
->tuesdays(); |
Ограничить задачу вторником |
->wednesdays(); |
Ограничить задачу средой |
->thursdays(); |
Ограничить задачу четвергом |
->fridays(); |
Ограничить задачу пятницей |
->saturdays(); |
Ограничить задачу субботой |
->days(array|mixed); |
Ограничить задачу определёнными днями |
->between($startTime, $endTime); |
Ограничить запуск задачи временем между началом и концом |
->unlessBetween($startTime, $endTime); |
Запретить запуск задачи в период между началом и концом |
->when(Closure); |
Ограничить запуск задачи условием |
->environments($env); |
Ограничить запуск задачи определёнными окружениями |
#Ограничения по дням
Метод days позволяет ограничить выполнение задачи определёнными днями недели. Например, можно запланировать команду на запуск по воскресеньям и средам каждый час:
$schedule->command('emails:send')
->hourly()
->days([0, 3]);
Альтернативно, можно использовать константы класса Illuminate\Console\Scheduling\Schedule для указания дней, в которые должна запускаться задача:
use Illuminate\Console\Scheduling\Schedule;
$schedule->command('emails:send')
->hourly()
->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);
#Ограничения по времени
Метод between позволяет ограничить выполнение задачи определённым временем суток:
$schedule->command('emails:send')
->hourly()
->between('7:00', '22:00');
Аналогично, метод unlessBetween исключает выполнение задачи в указанный период времени:
$schedule->command('emails:send')
->hourly()
->unlessBetween('23:00', '4:00');
#Ограничения по условию
Метод when позволяет ограничить выполнение задачи на основе результата проверки условия. Другими словами, если переданное замыкание возвращает true, задача будет выполнена, если другие ограничения не препятствуют запуску:
$schedule->command('emails:send')->daily()->when(function () {
return true;
});
Метод skip можно рассматривать как противоположность when. Если skip возвращает true, запланированная задача не будет выполнена:
$schedule->command('emails:send')->daily()->skip(function () {
return true;
});
При использовании цепочек методов when команда будет выполнена только если все условия when вернут true.
#Ограничения по окружению
Метод environments позволяет запускать задачи только в указанных окружениях (определённых переменной окружения APP_ENV environment variable):
$schedule->command('emails:send')
->daily()
->environments(['staging', 'production']);
#Часовые пояса
С помощью метода timezone можно указать, что время запланированной задачи должно интерпретироваться в заданном часовом поясе:
$schedule->command('report:generate')
->timezone('America/New_York')
->at('2:00')
Если вы постоянно назначаете один и тот же часовой пояс всем задачам, можно определить метод scheduleTimezone в классе App\Console\Kernel. Этот метод должен возвращать часовой пояс по умолчанию для всех запланированных задач:
use DateTimeZone;
/**
* Получить часовой пояс, который должен использоваться по умолчанию для запланированных событий.
*/
protected function scheduleTimezone(): DateTimeZone|string|null
{
return 'America/Chicago';
}
Помните, что некоторые часовые пояса используют переход на летнее время. При изменении времени перехода ваша запланированная задача может выполниться дважды или не выполниться вовсе. Поэтому рекомендуется по возможности избегать планирования с учётом часового пояса.
#Предотвращение наложения задач
По умолчанию запланированные задачи будут запускаться даже если предыдущий экземпляр задачи ещё выполняется. Чтобы этого избежать, можно использовать метод withoutOverlapping:
$schedule->command('emails:send')->withoutOverlapping();
В этом примере команда emails:send Artisan command будет запускаться каждую минуту, если она не выполняется в данный момент. Метод withoutOverlapping особенно полезен для задач с сильно варьирующимся временем выполнения, когда сложно предсказать длительность задачи.
При необходимости можно указать, сколько минут должно пройти до истечения блокировки "без наложения". По умолчанию блокировка снимается через 24 часа:
$schedule->command('emails:send')->withoutOverlapping(10);
Внутри метод withoutOverlapping использует кэш приложения для получения блокировок. При необходимости эти блокировки можно очистить с помощью Artisan-команды schedule:clear-cache. Обычно это требуется только если задача зависла из-за непредвиденной проблемы на сервере.
#Запуск задач на одном сервере
Для использования этой функции ваше приложение должно использовать драйвер кэша database, memcached, dynamodb или redis в качестве драйвера кэша по умолчанию. Кроме того, все серверы должны работать с одним центральным сервером кэша.
Если планировщик вашего приложения работает на нескольких серверах, можно ограничить выполнение задачи только одним сервером. Например, если у вас есть задача, которая генерирует отчёт каждую пятницу вечером, и планировщик запущен на трёх серверах, задача выполнится трижды — на каждом сервере. Это нежелательно.
Чтобы указать, что задача должна выполняться только на одном сервере, используйте метод onOneServer при определении задачи. Первый сервер, который получит задачу, установит атомарную блокировку, предотвращающую запуск задачи на других серверах одновременно:
$schedule->command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
#Именование задач для одного сервера
Иногда нужно запланировать одну и ту же задачу с разными параметрами, при этом указав Laravel запускать каждую вариацию задачи только на одном сервере. Для этого каждой задаче можно присвоить уникальное имя с помощью метода name:
$schedule->job(new CheckUptime('https://laravel.com'))
->name('check_uptime:laravel.com')
->everyFiveMinutes()
->onOneServer();
$schedule->job(new CheckUptime('https://vapor.laravel.com'))
->name('check_uptime:vapor.laravel.com')
->everyFiveMinutes()
->onOneServer();
Аналогично, запланированные замыкания должны иметь имя, если их нужно запускать на одном сервере:
$schedule->call(fn () => User::resetApiRequestCount())
->name('reset-api-request-count')
->daily()
->onOneServer();
#Фоновые задачи
По умолчанию несколько задач, запланированных на одно и то же время, выполняются последовательно в порядке определения в методе schedule. Если у вас есть долгие задачи, это может привести к задержке запуска последующих задач. Чтобы запускать задачи параллельно, можно использовать метод runInBackground:
$schedule->command('analytics:report')
->daily()
->runInBackground();
Метод runInBackground можно использовать только при планировании задач через методы command и exec.
#Режим обслуживания
Запланированные задачи вашего приложения не будут выполняться, когда приложение находится в режиме обслуживания, чтобы не мешать незавершённым операциям на сервере. Однако если нужно принудительно запустить задачу даже в режиме обслуживания, можно вызвать метод evenInMaintenanceMode при определении задачи:
$schedule->command('emails:send')->evenInMaintenanceMode();
#Запуск планировщика
Теперь, когда мы научились определять запланированные задачи, рассмотрим, как их запускать на сервере. Artisan-команда schedule:run проверит все задачи и определит, нужно ли их запускать в зависимости от текущего времени сервера.
Таким образом, при использовании планировщика Laravel достаточно добавить на сервер одну запись cron, которая будет запускать команду schedule:run каждую минуту. Если вы не знаете, как добавить записи cron на сервер, рассмотрите возможность использования сервиса Laravel Forge, который может управлять cron-записями за вас:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
#Задачи с интервалом менее минуты
В большинстве операционных систем cron-задания могут запускаться не чаще одного раза в минуту. Однако планировщик Laravel позволяет запускать задачи с более частой периодичностью, вплоть до одной секунды:
$schedule->call(function () {
DB::table('recent_users')->delete();
})->everySecond();
Когда в приложении определены задачи с интервалом менее минуты, команда schedule:run продолжит работу до конца текущей минуты, а не завершится сразу. Это позволяет запускать все необходимые задачи с высокой частотой в течение минуты.
Поскольку задачи с интервалом менее минуты, которые выполняются дольше ожидаемого, могут задерживать запуск последующих задач, рекомендуется, чтобы все такие задачи ставили в очередь или запускались в фоне для обработки:
use App\Jobs\DeleteRecentUsers;
$schedule->job(new DeleteRecentUsers)->everyTenSeconds();
$schedule->command('users:delete')->everyTenSeconds()->runInBackground();
#Прерывание задач с интервалом менее минуты
Поскольку команда schedule:run при наличии задач с периодичностью меньше минуты выполняется в течение всей минуты с момента запуска, при развёртывании приложения иногда нужно прервать её выполнение. Иначе уже запущенный экземпляр команды schedule:run будет продолжать использовать ранее развернутый код вашего приложения до конца текущей минуты.
Чтобы прервать выполняющиеся вызовы schedule:run, можно добавить команду schedule:interrupt в скрипт деплоя. Эту команду следует запускать после завершения деплоя:
php artisan schedule:interrupt
#Запуск планировщика локально
Обычно на локальной машине разработчика не добавляют запись cron для планировщика. Вместо этого можно использовать Artisan-команду schedule:work. Эта команда будет работать в переднем плане и вызывать планировщик каждую минуту до тех пор, пока вы не остановите её:
php artisan schedule:work
#Вывод задач
Планировщик Laravel предоставляет несколько удобных методов для работы с выводом запланированных задач. С помощью метода sendOutputTo можно отправить вывод в файл для последующего просмотра:
$schedule->command('emails:send')
->daily()
->sendOutputTo($filePath);
Если нужно добавлять вывод в конец файла, используйте метод appendOutputTo:
$schedule->command('emails:send')
->daily()
->appendOutputTo($filePath);
Метод emailOutputTo позволяет отправлять вывод задачи на указанный email. Перед этим необходимо настроить почтовые сервисы Laravel:
$schedule->command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('taylor@example.com');
Если нужно отправлять вывод по email только при ошибке (завершении с ненулевым кодом), используйте метод emailOutputOnFailure:
$schedule->command('report:generate')
->daily()
->emailOutputOnFailure('taylor@example.com');
Методы emailOutputTo, emailOutputOnFailure, sendOutputTo и appendOutputTo доступны только для методов command и exec.
#Хуки задач
С помощью методов before и after можно указать код, который будет выполнен до и после запуска запланированной задачи:
$schedule->command('emails:send')
->daily()
->before(function () {
// Задача вот-вот выполнится...
})
->after(function () {
// Задача выполнена...
});
Методы onSuccess и onFailure позволяют указать код, который выполнится при успешном или неудачном завершении задачи. Неудача означает, что Artisan или системная команда завершилась с ненулевым кодом:
$schedule->command('emails:send')
->daily()
->onSuccess(function () {
// Задача выполнена успешно...
})
->onFailure(function () {
// Задача завершилась с ошибкой...
});
Если команда возвращает вывод, его можно получить в хуках after, onSuccess или onFailure, указав тип Illuminate\Support\Stringable в аргументе $output замыкания:
use Illuminate\Support\Stringable;
$schedule->command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// Задача выполнена успешно...
})
->onFailure(function (Stringable $output) {
// Задача завершилась с ошибкой...
});
#Пинг URL
С помощью методов pingBefore и thenPing планировщик может автоматически отправлять HTTP-запрос на указанный URL до или после выполнения задачи. Это удобно для уведомления внешних сервисов, например Envoyer, о начале или завершении задачи:
$schedule->command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
Методы pingBeforeIf и thenPingIf можно использовать, чтобы пинговать указанный URL только если заданное условие равно true:
$schedule->command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
Методы pingOnSuccess и pingOnFailure отправляют пинг только при успешном или неудачном выполнении задачи. Неудача означает завершение с ненулевым кодом:
$schedule->command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);
Все методы пинга требуют библиотеки Guzzle HTTP. Обычно Guzzle устанавливается по умолчанию во всех новых проектах Laravel, но при необходимости его можно установить вручную через Composer, если он был удалён:
composer require guzzlehttp/guzzle
#События
При необходимости можно слушать события, которые генерирует планировщик. Обычно сопоставления слушателей событий определяются в классе App\Providers\EventServiceProvider вашего приложения:
/**
* Сопоставления слушателей событий для приложения.
*
* @var array
*/
protected $listen = [
'Illuminate\Console\Events\ScheduledTaskStarting' => [
'App\Listeners\LogScheduledTaskStarting',
],
'Illuminate\Console\Events\ScheduledTaskFinished' => [
'App\Listeners\LogScheduledTaskFinished',
],
'Illuminate\Console\Events\ScheduledBackgroundTaskFinished' => [
'App\Listeners\LogScheduledBackgroundTaskFinished',
],
'Illuminate\Console\Events\ScheduledTaskSkipped' => [
'App\Listeners\LogScheduledTaskSkipped',
],
'Illuminate\Console\Events\ScheduledTaskFailed' => [
'App\Listeners\LogScheduledTaskFailed',
],
];