跳到主要內容

排程器

編輯此頁面

排程器元件管理您的 PHP 應用程式中的任務排程,例如每天晚上 3 點、每兩週一次(假日除外)或您可能需要的任何其他自訂排程執行任務。

此元件適用於排程維護 (資料庫清理、快取清除等)、背景處理 (佇列處理、資料同步等)、週期性資料更新、排程通知 (電子郵件、警報) 等任務。

本文檔重點在使用完整堆疊 Symfony 應用程式環境中的排程器元件。

安裝

在使用 Symfony Flex 的應用程式中,執行此命令以安裝排程器元件

1
$ composer require symfony/scheduler

提示

MakerBundle v1.58.0 開始,您可以執行 php bin/console make:schedule 來產生基本排程,您可以自訂該排程以建立您自己的排程器。

Symfony 排程器基礎

使用此元件的主要優點是自動化由您的應用程式管理,這為您提供了 cron jobs 無法實現的靈活性 (例如,基於特定條件的動態排程)。

Scheduler 元件的核心功能是讓您建立一個任務 (稱為訊息),該任務由服務執行並按排程重複執行。它與 Symfony Messenger 元件 (例如訊息、處理器、匯流排、傳輸等) 有些相似之處,但主要區別在於 Messenger 無法處理定期重複的任務。

考慮以下應用程式範例,該應用程式依排程向客戶發送一些報告。首先,建立一個代表建立報告任務的排程器訊息

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)
    {
        // ... do some work to send the report to the customers
    }
}

目標不是立即發送這些訊息 (如 Messenger 元件中那樣),而是根據預定義的頻率建立它們。這要歸功於 SchedulerTransport,這是排程器訊息的特殊傳輸方式。

傳輸會根據指派的頻率自主產生各種訊息。下圖說明了 Messenger 和排程器元件中訊息處理的差異

在 Messenger 中

Symfony Messenger basic cycle

在排程器中

Symfony Scheduler basic cycle

另一個重要的區別是排程器元件中的訊息是週期性的。它們透過 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
    {
        // ...
    }
}

提示

預設情況下,排程名稱為 default,傳輸名稱遵循語法:scheduler_nameofyourschedule (例如 scheduler_default)。

提示

記憶化您的排程是一種良好的做法,可以防止在 getSchedule() 方法被另一個服務檢查時不必要的重建。

排程週期性訊息

RecurringMessage 是一個與觸發器相關聯的訊息,它配置訊息的頻率。Symfony 提供了不同類型的觸發器

CronExpressionTrigger
一種使用與 cron 命令列工具 相同語法的觸發器。
CallbackTrigger
一種使用 callback 判斷下一次執行日期的觸發器。
ExcludeTimeTrigger
一種從給定觸發器中排除特定時間的觸發器。
JitterTrigger
一種將隨機抖動新增至給定觸發器的觸發器。抖動是新增到原始觸發日期/時間的一些時間。這允許分散排程任務的負載,而不是在完全相同的時間執行所有任務。
PeriodicalTrigger
一種使用 DateInterval 判斷下一次執行日期的觸發器。

JitterTriggerExcludeTimeTrigger 是裝飾器,用於修改它們包裝的觸發器的行為。您可以透過呼叫 inner()decorators() 方法來取得裝飾的觸發器以及裝飾器

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

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

它們中的大多數可以透過 RecurringMessage 類別建立,如下列範例所示。

Cron 表達式觸發器

在使用 cron 觸發器之前,您必須安裝以下相依性

1
$ composer require dragonmantank/cron-expression

然後,使用與 cron 命令列工具 相同的語法定義觸發日期/時間

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

// optionally you can define the timezone used by the cron expression
RecurringMessage::cron('* * * * *', new Message(), new \DateTimeZone('Africa/Malabo'));

提示

如果您需要協助建構/理解 cron 表達式,請查看 crontab.guru 網站

您也可以使用一些代表常見 cron 表達式的特殊值

  • @yearly, @annually - 每年執行一次,1 月 1 日午夜 - 0 0 1 1 *
  • @monthly - 每月執行一次,每月 1 日午夜 - 0 0 1 * *
  • @weekly - 每週執行一次,週日午夜 - 0 0 * * 0
  • @daily, @midnight - 每天執行一次,午夜 - 0 0 * * *
  • @hourly - 每小時執行一次,第一分鐘 - 0 * * * *

例如

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

提示

您也可以使用 AsCronTask 屬性定義 cron 任務。

雜湊 Cron 表達式

如果您在同一時間排程了許多觸發器 (例如,在午夜 0 0 * * *),這將在該確切時間建立一個非常長的執行中排程清單。如果任務有記憶體洩漏,這可能會導致問題。

您可以在表達式中新增雜湊符號 (#) 以產生隨機值。雖然這些值是隨機的,但它們是可預測且一致的,因為它們是根據訊息產生的。字串表示為 my task 且定義頻率為 # # * * * 的訊息將具有 56 20 * * * (每天晚上 8: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:59 之間的某個時間)

例如

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

注意

月份範圍是 1-28,這是為了考慮到 2 月,其最少有 28 天。

週期性觸發器

這些觸發器允許使用不同的資料類型 (stringintegerDateInterval) 配置頻率。它們也支援 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);

提示

您也可以使用 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)
    {
    }

    // use this method to give a nice displayable name to
    // identify your trigger (it eases debugging)
    public function __toString(): string
    {
        return $this->inner.' (except holidays)';
    }

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

        // loop until you get the next run date that is not a holiday
        while ($this->isHoliday($nextRun)) {
            $nextRun = $this->inner->getNextRunDate($nextRun);
        }

        return $nextRun;
    }

    private function isHoliday(\DateTimeImmutable $timestamp): bool
    {
        // add some logic to determine if the given $timestamp is a holiday
        // return true if holiday, false otherwise
    }
}

然後,定義您的週期性訊息

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 來實現的。

從本質上講,這表示您可以透過在每次排程器傳輸檢查要產生的訊息時執行的 callback,在執行時動態定義您的訊息。

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
// 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'),
                    ),
                    // instead of being static as in the previous example
                    new CallbackMessageProvider([$this, 'generateReports'], 'foo')
                ),
                RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport())
            );
    }

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

探索製作週期性訊息的替代方案

還有另一種建立 RecurringMessage 的方法,可以透過將以下其中一個屬性新增至服務或命令來完成:AsPeriodicTaskAsCronTask

對於這兩個屬性,您都可以透過 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
9
10
11
12
13
// adds randomly up to 6 seconds to the trigger time to avoid load spikes
#[AsCronTask('0 0 * * *', jitter: 6)]

// defines the method name to call instead as well as the arguments to pass to it
#[AsCronTask('0 0 * * *', method: 'sendEmail', arguments: ['email' => 'admin@example.com'])]

// defines the timezone to use
#[AsCronTask('0 0 * * *', timezone: 'Africa/Malabo')]

// when applying this attribute to a Symfony console command, you can pass
// arguments and options to the command using the 'arguments' option:
#[AsCronTask('0 0 * * *', arguments: 'some_argument --some-option --another-option=some_value')]
class MyCommand extends Command

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()
    {
        // ...
    }
}

注意

fromuntil 選項是選填的。如果未定義,則任務將無限期執行。

#[AsPeriodicTask] 屬性接受許多參數來自訂觸發器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// the frequency can be defined as an integer representing the number of seconds
#[AsPeriodicTask(frequency: 86400)]

// adds randomly up to 6 seconds to the trigger time to avoid load spikes
#[AsPeriodicTask(frequency: '1 day', jitter: 6)]

// defines the method name to call instead as well as the arguments to pass to it
#[AsPeriodicTask(frequency: '1 day', method: 'sendEmail', arguments: ['email' => 'admin@symfony.com'])]
class SendDailySalesReports
{
    public function sendEmail(string $email): void
    {
        // ...
    }
}

// when applying this attribute to a Symfony console command, you can pass
// arguments and options to the command using the 'arguments' option:
#[AsPeriodicTask(frequency: '1 day', arguments: 'some_argument --some-option --another-option=some_value')]
class MyCommand extends Command

管理排程訊息

即時修改排程訊息

雖然預先規劃排程是有益的,但排程很少在一段時間內保持靜態。在一段時間後,某些 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/CleanUpOldSalesReportHandler.php
namespace App\Scheduler\Handler;

#[AsMessageHandler]
class CleanUpOldSalesReportHandler
{
    public function __invoke(CleanUpOldSalesReport $cleanUpOldSalesReport): void
    {
        // do some work here...

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

然而,此系統可能並非適用於所有情境。此外,處理器應理想地設計為處理其預期的訊息類型,而無需決定新增或移除新的週期性訊息。

例如,如果由於外部事件,需要新增旨在刪除報表的週期性訊息,則在處理器內實現此目的可能具有挑戰性。這是因為一旦沒有更多該類型的訊息,就不會再呼叫或執行處理器。

但是,Scheduler 也具有事件系統,該系統透過嫁接到 Symfony Messenger 事件中而整合到完整的 Symfony 堆疊應用程式中。這些事件透過偵聽器分派,提供了一種方便的回應方式。

透過事件管理排程訊息

策略性事件處理

目標是提供在決定何時採取行動方面的彈性,同時保持解耦。已引入三種主要事件類型

  • 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
36
37
38
39
// src/Scheduler/SaleTaskProvider.php
namespace App\Scheduler;

#[AsSchedule('uptoyou')]
class SaleTaskProvider implements ScheduleProviderInterface
{
    public function __construct(private EventDispatcherInterface $dispatcher)
    {
    }

    public function getSchedule(): Schedule
    {
        $this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());

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

                // can access the schedule
                $schedule = $event->getSchedule()->getSchedule();

                // can target directly the RecurringMessage being processed
                $schedule->removeById($messageContext->id);

                // allow to call the ShouldCancel() and avoid the message to be handled
                $event->shouldCancel(true);
            })
            ->after(function(PostRunEvent $event) {
                // Do what you want
            })
            ->onFailure(function(FailureEvent $event) {
                // Do what you want
            });
    }
}

排程器事件

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();

    // do something with the schedule, context or message

    // and/or cancel message
    $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();

    // do something with the schedule, context or message
}

執行此命令以找出哪些偵聽器已針對此事件註冊及其優先順序

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();

    // do something with the schedule, context, message or error (logging, ...)

    // and/or ignore failure event
    $event->shouldIgnore(true);
}

執行此命令以找出哪些偵聽器已針對此事件註冊及其優先順序

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

消費訊息

Scheduler 組件提供兩種使用訊息的方式,具體取決於您的需求:使用 messenger:consume 命令或以程式設計方式建立 worker。當在完整的 Symfony 堆疊應用程式的環境中使用 Scheduler 組件時,建議使用第一種解決方案;當將 Scheduler 組件作為獨立組件使用時,第二種解決方案更適合。

執行 Worker

在定義週期性訊息並將其附加到排程後,您需要一種機制來根據其定義的頻率產生和使用訊息。為此,Scheduler 組件使用 Messenger 組件的 messenger:consume 命令

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

# use -vv if you need details about what's happening
$ php bin/console messenger:consume scheduler_nameofyourschedule -vv
Symfony Scheduler - generate and consume

提示

根據您的部署情境,您可能偏好使用 cron、Supervisor 或 systemd 等工具自動化 Messenger worker 程序的執行。這可確保 worker 持續執行。如需更多詳細資訊,請參閱 Messenger 組件文件中的 部署到生產環境 章節。

以程式方式建立消費者

前述解決方案的替代方案是建立並呼叫將使用訊息的 worker。此組件隨附一個隨時可用的 worker,名為 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(),
    // add more handlers if you have more message types
], schedules: [
    $schedule,
    // the scheduler can take as many schedules as you need
]);

// finally, run the scheduler once it's ready
$scheduler->run();

注意

當將 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
  -------

    ------------------- ------------------------- ----------------------
    Trigger             Provider                  Next Run
    ------------------- ------------------------- ----------------------
    every 2 days        App\Messenger\Foo(0:17..)  Sun, 03 Dec 2023 ...
    15 4 */3 * *        App\Messenger\Foo(0:17..)  Mon, 18 Dec 2023 ...
   -------------------- -------------------------- ---------------------

# you can also specify a date to use for the next run date:
$ php bin/console debug:scheduler --date=2025-10-18

# you can also specify a date to use for the next run date for a schedule:
$ php bin/console debug:scheduler name_of_schedule --date=2025-10-18

# use the --all option to also display the terminated recurring messages
$ php bin/console debug:scheduler --all

使用 Symfony 排程器進行有效率的管理

當 worker 重新啟動或關閉一段時間時,Scheduler 傳輸將無法產生訊息(因為它們是由排程器傳輸即時建立的)。這表示在 worker 非活動期間排程傳送的任何訊息都不會傳送,並且 Scheduler 將遺失最後處理訊息的追蹤。重新啟動後,它將重新計算從那時起要產生的訊息。

為了說明,請考慮設定為每 3 天傳送一次的週期性訊息。如果 worker 在第 2 天重新啟動,則訊息將在重新啟動後 3 天,即第 5 天傳送。

雖然此行為可能不一定會造成問題,但有可能不符合您尋求的內容。

這就是排程器允許透過 stateful 選項(和 Cache 組件)記住訊息的上次執行日期的原因。這允許系統保留排程的狀態,確保 worker 重新啟動時,它會從上次停止的位置繼續。

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)
    }
}

使用 stateful 選項,所有錯過的訊息都將被處理。如果您只需要處理一次訊息,則可以使用 processOnlyLastMissedRun 選項

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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)
            ->processOnlyLastMissedRun(true)
    }
}

7.2

processOnlyLastMissedRun 選項是在 Symfony 7.2 中引入的。

為了更有效地擴展您的排程,您可以使用多個 worker。在這種情況下,良好的實務是新增 lock,以防止同一個任務多次執行

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(
                // ...
            )
            ->lock($this->lockFactory->createLock('my-lock'));
    }
}

提示

訊息的處理時間很重要。如果花費很長時間,則所有後續訊息處理都可能會延遲。因此,良好的實務是預期到這一點,並規劃大於訊息處理時間的頻率。

此外,為了更好地擴展您的排程,您可以選擇將訊息包裝在 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 附加到訊息,以幫助您在需要時識別這些訊息。

這項工作,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本