1. Глубже в детали
  2. Планирование задач

Введение

Ранее, возможно, вам приходилось создавать запись конфигурации cron для каждой задачи, которую вы хотели запланировать на своем сервере. Однако это может быстро стать проблемой, потому что ваше расписание задач больше не находится под управлением системы контроля версий, и вам приходится подключаться к своему серверу по SSH, чтобы просмотреть существующие записи cron или добавить новые записи.

Планировщик команд 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();

Планировка команд оболочки

Метод 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 'modify_10x/scheduling.return_text_262';
}

Внимание Помните, что некоторые часовые пояса используют переход на летнее/зимнее время. При изменении времени перехода на летнее/зимнее время ваша запланированная задача может выполняться дважды или даже вообще не выполняться. По этой причине рекомендуем избегать планирования задач по часовым поясам, когда это возможно.

Предотвращение перекрытия задач

По умолчанию запланированные задачи будут выполняться даже в том случае, если предыдущий экземпляр задачи все еще выполняется. Чтобы предотвратить это, вы можете использовать метод withoutOverlapping:

$schedule->command('emails:send')->withoutOverlapping();

В этом примере команда emails:send команды Artisan будет выполняться каждую минуту, если она еще не выполняется. Метод withoutOverlapping особенно полезен, если у вас есть задачи, сильно различающиеся по времени выполнения, что мешает вам точно предсказать, сколько времени займет выполнение задачи.

При необходимости вы можете указать, сколько минут должно пройти, прежде чем истечет блокировка "без перекрытия". По умолчанию блокировка истекает через 24 часа:

$schedule->command('emails:send')->withoutOverlapping(10);

За кулисами метод withoutOverlapping использует кэш вашего приложения cache, чтобы получить блокировки. При необходимости вы можете очистить эти блокировки кэша с помощью команды 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-docs.com'))
->name('check_uptime:laravel-docs.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, вы можете отправить вывод на указанный адрес электронной почты. Прежде чем отправлять вывод задачи по электронной почте, вы должны настроить сервис электронной почты Laravel:

$schedule->command('report:generate')
->daily()
->sendOutputTo($filePath)
->emailOutputTo('[email protected]');

Если вы хотите отправлять вывод только в случае, если запланированная команда Artisan или системная команда завершается с ненулевым кодом завершения, используйте метод emailOutputOnFailure:

$schedule->command('report:generate')
->daily()
->emailOutputOnFailure('[email protected]');

Внимание Методы 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, указав типизированный аргумент $output вашего замыкания:

use Illuminate\Support\Stringable;
 
$schedule->command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// Задача выполнена успешно...
})
->onFailure(function (Stringable $output) {
// Задача завершилась с ошибкой...
});

Пингование URL-адресов

С использованием методов pingBefore и thenPing планировщик может автоматически отправлять запрос ping на указанный URL перед или после выполнения задачи. Этот метод полезен для уведомления внешнего сервиса, такого как Envoyer, о том, что ваша запланированная задача начинает выполнение или завершает выполнение:

$schedule->command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);

Методы pingBeforeIf и thenPingIf можно использовать для отправки запроса ping на указанный URL только в том случае, если выполняется заданное условие:

$schedule->command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);

Методы pingOnSuccess и pingOnFailure могут использоваться для отправки запроса ping на указанный URL только в случае успеха или неудачи задачи. Ошибка указывает на то, что запланированная команда Artisan или системная команда завершилась с ненулевым кодом завершения:

$schedule->command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);

Для использования всех методов ping требуется библиотека Guzzle HTTP. Обычно Guzzle устанавливается по умолчанию во всех новых проектах Laravel, но вы можете вручную установить Guzzle в свой проект с использованием менеджера пакетов 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',
],
];