排程器
排程器元件管理您的 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 中

在排程器中

另一個重要的區別是排程器元件中的訊息是週期性的。它們透過 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
判斷下一次執行日期的觸發器。
JitterTrigger 和 ExcludeTimeTrigger 是裝飾器,用於修改它們包裝的觸發器的行為。您可以透過呼叫 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 天。
週期性觸發器
這些觸發器允許使用不同的資料類型 (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);
提示
您也可以使用 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
的方法,可以透過將以下其中一個屬性新增至服務或命令來完成: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 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()
{
// ...
}
}
注意
from
和 until
選項是選填的。如果未定義,則任務將無限期執行。
#[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

提示
根據您的部署情境,您可能偏好使用 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 附加到訊息,以幫助您在需要時識別這些訊息。