Scheduler

Дата оновлення перекладу 2024-06-10

Scheduler

Компонент Scheduler керує плануванням завдань у вашому PHP-додатку, наприклад запуск завдання щовечора о 3 годині ночі, кожні два тижні, окрім святкових днів, або за будь-яким іншим розкладом, який вам може знадобитися.

Цей компонент корисний для планування таких завдань, як обслуговування (очищення бази даних, очищення кешу тощо), фонова обробка (обробка черг, синхронізація даних тощо), періодичне оновлення даних, заплановані сповіщення (електронні листи, попередження тощо) та інше.

У цьому документі розглядається використання компонента Scheduler у контексті повного стекового додатку Symfony.

Установка

У додатках, що використовують Symfony Flex , виконайте цю команду, щоб встановити компонент Scheduler:

1
$ composer require symfony/scheduler

Основи Symfony Scheduler

Основна перевага використання цього компонента полягає в тому, що автоматизацією керує ваш додаток, що дає вам велику гнучкість, яка неможлива у випадку з завданнями cron (наприклад, динамічні розклади на основі певних умов).

За своєю суттю, компонент Scheduler дозволяє створювати завдання (яке називається повідомленням), яке виконується сервісом і повторюється за певним розкладом. Він має деяку схожість з компонентом Symfony Messenger (наприклад, повідомлення, обробник, автобус, транспорт тощо), але головна відмінність полягає у тому, що Messenger не може працювати з завданнями, які повторюються через регулярні проміжки часу.

Розглянемо наступний приклад додатку, який надсилає деякі звіти клієнтам за розкладом. Спочатку створіть повідомлення Scheduler, яке представляє завдання створення звіту:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Scheduler/Message/SendDailySalesReports.php
namespace App\Scheduler\Message;

class SendDailySalesReports
{
    public function __construct(private int $id) {}

    public function getId(): int
    {
        return $this->id;
    }
}

Далі створіть обробник, який обробляє такі повідомлення:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Scheduler/Handler/SendDailySalesReportsHandler.php
namespace App\Scheduler\Handler;

use App\Scheduler\Message\SendDailySalesReports;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
class SendDailySalesReportsHandler
{
    public function __invoke(SendDailySalesReports $message)
    {
        // ... зробити якусь роботу, щоб відправити звіт клієнтам
    }
}

Замість того, щоб надсилати ці повідомлення негайно (як у компоненті Messenger), мета полягає у створенні цих повідомлень із заздалегідь визначеною періодичністю. Це можливо завдяки SchedulerTransport -
спеціальному транспорту для повідомлень Scheduler.

Цей транспорт автономно генерує різноманітні повідомлення відповідно до призначеної частоти. Наступні зображення ілюструють відмінності між обробкою повідомлень у компонентах Messenger і Scheduler:

В Messenger:

Базовий цикл Symfony Messenger

В Scheduler:

Базовий цикл Symfony Scheduler

Ще одна важлива відмінність полягає в тому, що повідомлення в компоненті Scheduler є повторюваними. Вони представлені за допомогою класу RecurringMessage.

Додавання повторюваних повідомлень до розкладу

Конфігурація частоти повідомлень зберігається у класі, який реалізує ScheduleProviderInterface. Цей постачальник використовує метод getSchedule() для повернення розкладу, що містить різні періодичні повідомлення.

Атрибут AsSchedule, який за замовчуванням посилається на розклад з назвою default, дозволяє вам зареєструватися за певним розкладом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        // ...
    }
}

Tip

За замовчуванням, ім'я розкладу - default, а ім'я транспорту слідує
синтаксису: scheduler_nameofyourschedule(наприклад, scheduler_default).

Tip

Мемоїзація вашого розкладу є гарною практикою для запобігання непотрібної реконструкції,
якщо метод getSchedule() перевіряється іншим сервісом.

Планування повторюваних повідомлень

RecurringMessage - це повідомлення, пов'язане з тригером, який конфігурує
частоту повідомлення. Symfony надає різні типи тригерів:

CronExpressionTrigger
Тригер, який використовує такий же синтаксис, як утиліта командного рядка cron.
CallbackTrigger
Тригер, який використовує зворотний виклик для визначення наступної дати запуску.
ExcludeTimeTrigger
Тригер, який виключає певні проміжки часу з заданого тригера.
JitterTrigger
Тригер, який додає випадковий джиттер до заданого тригера. Джиттер - це деякий час, який додається/віднімається до початкової дати/часу запуску. Це дозволяє розподілити навантаження запланованих завдань замість того, щоб запускати їх в один і той самий час.
PeriodicalTrigger
Тригер, який використовує DateInterval для визначення наступної дати запуску.

JitterTrigger та ExcludeTimeTrigger є декораторами і змінюють поведінку тригера, який вони обгортають. Ви можете отримати декорований тригер, а також декоратори за допомогою виклику методів inner та :method:()Symfony\Component\Scheduler\Trigger\AbstractDecoratedTrigger::decorators`:

1
2
3
4
$trigger = new ExcludeTimeTrigger(new JitterTrigger(CronExpressionTrigger::fromSpec('#midnight', new MyMessage()));

$trigger->inner(); // CronExpressionTrigger
$trigger->decorators(); // [ExcludeTimeTrigger, JitterTrigger]

Більшість з них можна створити за допомогою класу RecurringMessage, як показано у наступних прикладах.

Тригери виразів сron

Перед використанням cron-тригерів необхідно встановити наступну залежність:

1
$ composer require dragonmantank/cron-expression

Потім визначте дату/час тригера, використовуючи той самий синтаксис, що і в
утиліті командного рядка cron:

1
2
3
4
RecurringMessage::cron('* * * * *', new Message());

// опціонально ви можете визначити часовий пояс, використовуваний виразом cron
RecurringMessage::cron('* * * * *', new Message(), new \DateTimeZone('Africa/Malabo'));

Tip

Якщо вам потрібна допомога у створенні/розумінні виразів cron, відвідайте сайт crontab.guru.

Ви також можете використовувати деякі спеціальні значення, які представляють загальні вирази cron:

  • @yearly, @annually - Запускати раз на рік, опівчночі 1го січня - 0 0 1 1 *
  • @monthly - Запускати раз на місяць, опівночі першого числа місяця - 0 0 1 * *
  • @weekly - Запускати раз на тиждень, опівночі у неділю - 0 0 * * 0
  • @daily, @midnight - Запускати раз на день, опівночі - 0 0 * * *
  • @hourly - Запускати раз на годину, в першу хвилину - 0 * * * *

Наприклад:

1
RecurringMessage::cron('@daily', new Message());

Tip

Ви також можете визначити завдання cron за допомогою атрибута AsCronTask .

Хешовані вирази сron

Якщо ви запланували багато тригерів на один і той самий час (наприклад, опівночі, 0 0 * * *) це призведе до створення дуже довгого списку розкладів на один і той самий час. Це може спричинити проблему, якщо завдання має витік пам'яті.

Ви можете додати символ хешування (#) до виразів, щоб генерувати рандомні значення. Хоча значення є рандомними, вони є передбачуваними і послідовними, тому що вони генеруються на основі повідомлення. Повідомлення з рядковим представленням my task і визначеною частотою # # * * * матиме ідемпотентну частоту у вигляді 56 20 * * * (щодня о 20:56).

Ви також можете використовувати хеш-діапазони (#(x-y)) для визначення списку можливих значень для цієї рандомної частини. Наприклад, # #(0-7) * * * означає щодня, в певний час між північчю та 7 ранку. Використання # без діапазону створює діапазон будь-яких валідних значень для поля. # # # # # і є скороченням від #(0-59) #(0-23) #(1-28) #(1-12) #(0-6).

Ви також можете використовувати деякі спеціальні значення, які представляють поширені хешовані вирази cron:

????????? ?????????????? ??
#hourly # * * * * (? ????? ??????? ?????? ??????)
#daily # # * * * (? ?????? ??? ??????? ???)
#weekly # # * * # (? ?????? ??? ??????? ?????)
#weekly@midnight # #(0-2) * * # (#midnight ??????? ??? ??????? ?????)
#monthly # # # * * (? ?????? ??? ??????? ???, ??? ?? ??????)
#monthly@midnight # #(0-2) # * * (#midnight ??????? ??? ??????? ??????)
#annually # # # # * (? ?????? ??? ??????? ???, ??? ?? ???)
#annually@midnight # #(0-2) # # * (#midnight ??????? ??? ??? ?? ???)
#yearly # # # # * ????????? ??? #annually
#yearly@midnight # #(0-2) # # * ????????? ??? #annually@midnight
#midnight # #(0-2) * * * (? ?????? ??? ??? ????????? ?? 2:59am, ????? ????)

Наприклад:

1
RecurringMessage::cron('#midnight', new Message());

Note

Діапазон днів місяця 1-28, щоб врахувати лютий який має мінімум 28 днів.

Періодичні тригери

Ці тригери дозволяють налаштовувати частоту, використовуючи різні типи даних (string, integer, DateInterval). Вони також підтримують відносні формати визначені функціями PHP datetime:

1
2
3
4
5
6
7
RecurringMessage::every('10 seconds', new Message());
RecurringMessage::every('3 weeks', new Message());
RecurringMessage::every('first Monday of next month', new Message());

$from = new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris'));
$until = '2023-06-12';
RecurringMessage::every('first Monday of next month', new Message(), $from, $until);

Tip

Ви також можете перевизначити періодичні завдання, використовуючи атрибут AsPeriodicTask .

Користувацькі тригери

Користувацькі тригери дозволяють динамічно конфігурувати будь-яку частоту. Вони створюються як сервіси, що реалізують TriggerInterface.

Наприклад, якщо ви хочете надсилати звіти клієнтам щодня, крім святкових днів:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php
namespace App\Scheduler\Trigger;

class ExcludeHolidaysTrigger implements TriggerInterface
{
    public function __construct(private TriggerInterface $inner)
    {
    }

    // використати цей метод, щоб надати гарне імʼя для відображення задля
    // ідентифікації вашого тригера (полегшує налагодження)
    public function __toString(): string
    {
        return $this->inner.' (except holidays)';
    }

    public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
    {
        if (!$nextRun = $this->inner->getNextRunDate($run)) {
            return null;
        }

        // циклічно, доки не отримаєте наступну дату запуску, яка не є святковою
        while (!$this->isHoliday($nextRun) {
            $nextRun = $this->inner->getNextRunDate($nextRun);
        }

        return $nextRun;
    }

    private function isHoliday(\DateTimeImmutable $timestamp): bool
    {
        // додати деяку логіку для визначення, чи є заданий $timestamp святом
        // повернути true, якщо так, false - в іншому випадку
    }
}

Потім визначте ваше повторюване повідомлення:

1
2
3
4
5
6
RecurringMessage::trigger(
    new ExcludeHolidaysTrigger(
        CronExpressionTrigger::fromSpec('@daily'),
    ),
    new SendDailySalesReports('...'),
);

Нарешті, повідомлення, що повторюються, мають бути прикріплені до розкладу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::trigger(
                    new ExcludeHolidaysTrigger(
                        CronExpressionTrigger::fromSpec('@daily'),
                    ),
                    new SendDailySalesReports()
                ),
                RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport())
            );
    }
}

Отже, це RecurringMessage охоплюватиме як тригер, що визначає частоту генерації повідомлення, так і саме повідомлення, яке оброблятиметься певним обробником.

Але що цікаво знати, так це те, що він також надає вам можливість генерувати повідомлення динамічно.

Динамічне бачення для згенерованих повідомлень

Це особливо корисно, коли повідомлення залежить від даних, що зберігаються в базах даних або сторонніх сервісах.

Наслідуючи попередній приклад генерації звітів: вони залежать від запитів клієнтів. Залежно від конкретних запитів, може знадобитися генерувати будь-яку кількість звітів з певною частотою. Для таких динамічних сценаріїв це дає вам можливість динамічно, а не статично визначати наші повідомлення. Це досягається за допомогою визначення CallbackMessageProvider.

По суті, це означає, що ви можете динамічно, під час виконання, визначати ваші повідомлення за допомогою зворотного виклику, який виконується кожного разу, коли планувальник перевіряє наявність повідомлень, які потрібно згенерувати:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::trigger(
                    new ExcludeHolidaysTrigger(
                        CronExpressionTrigger::fromSpec('@daily'),
                    ),
                // замість того, щоб бути статичним, як у попередньому прикладі
                new CallbackMessageProvider([$this, 'generateReports'], 'foo')),
                RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport())
            );
    }

    public function generateReports(MessageContext $context)
    {
        // ...
        yield new SendDailySalesReports();
        yield new ReportSomethingReportSomethingElse();
    }
}

Досліджуння альтернатив для створення ваших повторюваних повідомлень

Існує також інший спосіб створення RecurringMessage, і це можна зробити додавши один з цих атрибутів до сервісу або команди: AsPeriodicTask і AsCronTask.

Для обох цих атрибутів ви маєте можливість визначити розклад для використання за допомогою опції schedule. За замовчуванням буде використано розклад із назвою default. Також, за замовчуванням, буде викликано метод __invoke вашого сервісу, але ви також можете вказати метод для виклику за допомогою опції method і ви можете вказати аргументи за допомогою опції arguments, якщо це необхідно.

Приклад AsCronTask

Це найпростіший спосіб визначення тригера cron з таким атрибутом:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Scheduler/Task/SendDailySalesReports.php
namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsCronTask;

#[AsCronTask('0 0 * * *')]
class SendDailySalesReports
{
    public function __invoke()
    {
        // ...
    }
}

Атрибут приймає більше параметрів для налаштування тригера:

1
2
3
4
5
6
7
8
// рандомно додає до 6 секунд до часу тригера, щоб уникнути сплесків навантажень
#[AsCronTask('0 0 * * *', jitter: 6)]

// визначає імʼя методу для виклику натомість, а також аргументи, які треба йому передати
#[AsCronTask('0 0 * * *', method: 'sendEmail', arguments: ['email' => 'admin@example.com'])]

// визначає часовий пояс для використання
#[AsCronTask('0 0 * * *', timezone: 'Africa/Malabo')]

Приклад AsPeriodicTask

Це найпростіший спосіб визначення періодичного тригера з таким атрибутом:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Scheduler/Task/SendDailySalesReports.php
namespace App\Scheduler\Task;

use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;

#[AsPeriodicTask(frequency: '1 day', from: '2022-01-01', until: '2023-06-12')]
class SendDailySalesReports
{
    public function __invoke()
    {
        // ...
    }
}

Note

Опції from та until є необов'язковими. Якщо їх не визначено, завдання буде виконуватися нескінченно.

Атрибут #[AsPeriodicTask] приймає багато параметрів для налаштування тригера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// частота може бути визначена як ціле число, що представляє кількість секунд
#[AsPeriodicTask(frequency: 86400)]

// рандомно додає до 6 секунд до часу тригера, щоб уникнути сплесків навантажень
#[AsPeriodicTask(frequency: '1 day', jitter: 6)]

// визначає імʼя методу для виклику натомість, а також аргументи, які треба йому передати
#[AsPeriodicTask(frequency: '1 day', method: 'sendEmail', arguments: ['email' => 'admin@symfony.com'])]
class SendDailySalesReports
{
    public function sendEmail(string $email): void
    {
        // ...
    }
}

// визначає часовий пояс для використання
#[AsPeriodicTask(frequency: '1 day', timezone: 'Africa/Malabo')]

Управління запланованими повідомленнями

Зміна запланованих повідомлень в реальному часі

Хоча планування розкладу заздалегідь є корисним, рідко буває так, що графік залишається статичним протягом тривалого часу. Через певний період деякі RecurringMessages можуть застаріти, в той час як інші, можливо, потрібно буде інтегрувати в планування.

Як правило, щоб полегшити велике робоче навантаження, повідомлення, що повторюються у розкладі, зберігаються у пам'яті, щоб уникнути повторного обчислення кожного разу, коли транспорт планувальника генерує повідомлення. Однак цей підхід може мати і зворотний бік.

За прикладом генерування звітів, наведеним вище, компанія може проводити певні рекламні акції в певні періоди (і про них потрібно повідомляти неодноразово протягом певного періоду часу), або видалення старих звітів потрібно зупинити за певних обставин.

Ось чому Scheduler включає в себе механізм динамічної модифікації розкладу і врахування всіх змін в режимі реального часу.

Стратегії для додавання, видалення та модифікації записів в рамках розкладу

Розклад надає вам можливість add(), remove(), або clear() всі пов'язані з ним повторювані повідомлення, що призведе до обнулення та переобчислення стека повторюваних повідомлень у пам'яті.

Наприклад, з різних причин, якщо немає необхідності у генеруванні звіту, може бути використано зворотний виклик, щоб умовно пропустити генерування деяких або всіх звітів.

Однак, якщо метою є повне видалення повторюваного повідомлення та його повторення,
Schedule пропонує методи remove()

або removeById(). Це може бути особливо корисним у вашому випадку, зокрема, якщо вам потрібно зупинити генерування повторюваного повідомлення, що передбачає видалення старих звітів.

У своєму обробнику ви можете перевірити умову і, якщо вона є ствердною, отримати доступ до Schedule і викликати цей метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
                $this->removeOldReports;
            );
    }

    // ...

    public function removeCleanUpMessage()
    {
        $this->getSchedule()->getSchedule()->remove($this->removeOldReports);
    }
}

// src/Scheduler/Handler/.php
namespace App\Scheduler\Handler;

#[AsMessageHandler]
class CleanUpOldSalesReportHandler
{
    public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void
    {
        // зробити якусь роботу тут...

        if ($isFinished) {
            $this->mySchedule->removeCleanUpMessage();
        }
    }
}

Тим не менш, ця система може бути не найбільш підходящою для всіх сценаріїв. Крім того, в ідеалі обробник повинен бути створений для обробки повідомлень того типу, для якого він призначений, без прийняття рішень про додавання або видалення нового повторюваного повідомлення.

Наприклад, якщо через зовнішню подію потрібно додати повторюване повідомлення, спрямоване на видалення звітів, це може бути складно зробити в обробнику.
Це пов'язано з тим, що обробник більше не буде викликаний або виконаний, коли більше не буде повідомлень такого типу.

Однак, Scheduler також має систему подій, яка інтегрована у Symfony повного стеку шляхом приєднання до подій Symfony Messenger. Ці події розгортаються через слухач, що забезпечує зручний спосіб реагування на них.

Управління запланованими повідомленнями через події

Стратегічне управління подіями

Мета полягає в тому, щоб забезпечити гнучкість у прийнятті рішень про те, коли вживати заходів, при цьому зберігаючи відокремленість. Було представлено три основні типи подій

  • PRE_RUN_EVENT
  • POST_RUN_EVENT
  • FAILURE_EVENT

Доступ до розкладу є надзвичайно важливою функцією, що дозволяє легко додавати або видаляти типи повідомлень. Крім того, можна буде отримати доступ до до поточного оброблюваного повідомлення та його контексту.

Розглядаючи наш сценарій, ви можете прослухати PRE_RUN_EVENT і перевірити, чи виконано певну умову. Наприклад, ви можете вирішити додати повторюване повідомлення для повторного очищення старих звітів, з тією ж або іншою конфігурацією, або додати будь-яке інше повторюване повідомлення.

Якщо ви вирішили обробити видалення повторюваного повідомлення, ви можете
зробити це у слухачі цієї події. Важливо зазначити, що вона виявляє специфічну

функцію shouldCancel(), яка дозволяє запобігти передачі та обробці обробником повідомлення видаленого повторюваного повідомлення:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
            );
            ->before(function(PreRunEvent $event) {
                $message = $event->getMessage();
                $messageContext = $event->getMessageContext();

                // має доступ до розкладу
                $schedule = $event->getSchedule()->getSchedule();

                // може напряму звертатися до RecurringMessage, яке обробляється
                $schedule->removeById($messageContext->id);

                // дозволити виклик ShouldCancel() та уникати обробки повідомлення
                    $event->shouldCancel(true);
            }
            ->after(function(PostRunEvent $event) {
                // Зробити те, що ви хочете
            }
            ->onFailure(function(FailureEvent $event) {
                // Зробити те, що ви хочете
            }
    }
}

Події планувальника

PreRunEvent

Клас події: PreRunEvent

PreRunEvent дозволяє змінювати Schedule`

або скасувати повідомлення до того, як воно буде спожито:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\PreRunEvent;

public function onMessage(PreRunEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    // зробити щось з розкладом, контекстом чи повідомленням

    // та/або скасувати повідомлення
    $event->shouldCancel(true);
}

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані в цій події та їхні пріоритети:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PreRunEvent"

PostRunEvent

Клас події: PostRunEvent

PostRunEvent дозволяє змінювати Schedule`

після того, як повідомлення буде спожито:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\PostRunEvent;

public function onMessage(PostRunEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    // зробити щось з розкладом, контекстом чи повідомленням
}

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані в цій події та їхні пріоритети:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\PostRunEvent"

FailureEvent

Клас події: FailureEvent

FailureEvent дозволяє змінювати Schedule`,

коли споживання повідомлення викликає виключення:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Scheduler\Event\FailureEvent;

public function onMessage(FailureEvent $event): void
{
    $schedule = $event->getSchedule();
    $context = $event->getMessageContext();
    $message = $event->getMessage();

    $error = $event->getError();

    // зробити щось з розкладом, контекстом, повідомленням або помилкою (логування, ...)

    // та/або ігнорувати невдалу подію
    $event->shouldIgnore(true);
}

Виконайте цю команду, щоб дізнатися, які слухачі зареєстровані в цій події та їхні пріоритети:

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Scheduler\Event\FailureEvent"

Споживання повідомлень

Компонент Scheduler пропонує два способи споживання повідомлень, залежно від ваших потреб: за допомогою команди messenger:consume або за допомогою створення працівника програмно. Перший спосіб є рекомендованим при використанні компонента Scheduler у контексті повностекового Symfony-додатку, друге - більш прийнятне при використанні компонента Scheduler як окремого компонента.

Запуск робітника

Після того, як ви визначили і прикріпили до розкладу ваші повторювані повідомлення, вам знадобиться механізм для генерування та споживання повідомлень відповідно до визначеної частоти. Для цього компонент Scheduler використовує команду messenger:consume з компонента Messenger:

1
2
3
4
$ php bin/console messenger:consume scheduler_nameofyourschedule

# використайте -vv, якщо вам потрібні деталі стосовно того, що відбувається
$ php bin/console messenger:consume scheduler_nameofyourschedule -vv
Symfony Scheduler - згенерувати та спожити

Створення споживача програмно

Альтернативою попередньому рішенню є створення та виклик робітника, який буде споживати повідомлення. Компонент постачається з готовим до використання робітником з ім'ям Scheduler, який ви можете використовувати у своєму коді:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\Scheduler\Scheduler;

$schedule = (new Schedule())
    ->with(
        RecurringMessage::trigger(
            new ExcludeHolidaysTrigger(
                CronExpressionTrigger::fromSpec('@daily'),
            ),
            new SendDailySalesReports()
        ),
    );

$scheduler = new Scheduler(handlers: [
    SendDailySalesReports::class => new SendDailySalesReportsHandler(),
    // додати більше обробників, якщо у вас більше типів повідомлень
], schedules: [
    $schedule,
    // планвальник може приймати стільки розкладів, скільки вам потрібно
]);

// нарешті, запустити планувальник, коли він буде готовий
$scheduler->run();

Note

Scheduler можна використовувати при використанні компонента Scheduler як окремого компонента. Якщо ви використовуєте його у контексті фреймворку, наполегливо рекомендується використовувати команду messenger:consume, як описано у попередньому розділі.

Налагодження розкладу

Команда debug:scheduler надає список розкладів разом з їхніми повторюваними
повідомленнями. Ви можете звузити список до конкретного розкладу:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ php bin/console debug:scheduler

  Scheduler
  =========

  default
  -------

    ------------------- ------------------------- ----------------------
    Тригер              Постачальник              Наступний запуск
    ------------------- ------------------------- ----------------------
    every 2 days        App\Messenger\Foo(0:17..)  Sun, 03 Dec 2023 ...
    15 4 */3 * *        App\Messenger\Foo(0:17..)  Mon, 18 Dec 2023 ...
   -------------------- -------------------------- ---------------------

# ви також можете вказати дату для використання в якості наступної дати запуску:
$ php bin/console debug:scheduler --date=2025-10-18

# ви також можете вказати дату для використання в якості наступної дати запуску для розкладу:
$ php bin/console debug:scheduler name_of_schedule --date=2025-10-18

# використати опцію --all щоб також відобразити завершені повторювані повідомлення
$ php bin/console debug:scheduler --all

Ефективне управління з Symfony Scheduler

Коли робітник перезапускається або вимикається на деякий час, транспорт Scheduler не зможе генерувати повідомлення (оскільки вони створюються на льоту транспортом планувальника). Це означає, що будь-які повідомлення, заплановані для відправлення під час неактивності робітника, не будуть відправлені, а Scheduler втратить відстеження останнього обробленого повідомлення. Після перезапуску він переобчислить повідомлення, які мають бути створені з цього моменту.

Для прикладу, розглянемо повторюване повідомлення, яке має надсилатися кожні 3 дні. Якщо робітника перезапустити на 2-й день, повідомлення буде надіслано через 3 дні після перезапуску, на 5-й день.

Хоча така поведінка не обов'язково є проблемою, існує ймовірність, що вона може не відповідати тому, чого ви прагнете.

Тому планувальник дозволяє запам'ятовувати дату останнього виконання повідомлення за допомогою опції stateful (і компонента Cache). Це дозволяє системі зберігати стан розкладу, гарантуючи, що при перезапуску робітника він відновиться з тієї точки, на якій зупинився:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

        return $this->schedule ??= (new Schedule())
            ->with(
                // ...
            )
            ->stateful($this->cache)
    }
}

Щоб ефективніше масштабувати свої розклади, ви можете використовувати кількох робітників. У таких випадках гарною практикою є додавання блокування, щоб запобігти виконанню одного і того ж завдання більше одного разу:

// src/Scheduler/SaleTaskProvider.php namespace AppScheduler;

#[AsSchedule('uptoyou')] class SaleTaskProvider implements ScheduleProviderInterface { public function getSchedule(): Schedule { $this->removeOldReports = RecurringMessage::cron('3 8 1', new CleanUpOldSalesReport());

return $this->schedule ??= (new Schedule())
->with(
// ...

) ->lock($this->lockFactory->createLock('my-lock')

}

}

Tip

Час обробки повідомлення має значення. Якщо це займає багато часу, всі наступні обробки повідомлень можуть затриматися. Тому рекомендується передбачити це і планувати частоту, більшу за час обробки повідомлення.

Крім того, для кращого масштабування ваших розкладів, ви можете обгорнути ваше повідомлення у RedispatchMessage. Це дозволить вам вказати транспорт, на який буде повторно відправлено ваше повідомлення перед тим, як воно буде передано відповідному обробнику:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function getSchedule(): Schedule
    {
        return $this->schedule ??= (new Schedule())
            ->with(
                RecurringMessage::every('5 seconds', new RedispatchMessage(new Message(), 'async'))
            );
    }
}

При використанні RedispatchMessage, Symfony додасть
ScheduledStamp до повідомлення, що допоможе вам ідентифікувати ці повідомлення за потреби.