|
| 1 | +Scheduler |
| 2 | +========= |
| 3 | + |
| 4 | +.. versionadded:: 6.3 |
| 5 | + |
| 6 | + The Scheduler component was introduced in Symfony 6.3 |
| 7 | + |
| 8 | +The Symfony Scheduler is a powerful and flexible component designed to manage tasks scheduling within your PHP application. |
| 9 | + |
| 10 | +This document focuses on using the Scheduler component in the context of a full stack Symfony application. |
| 11 | + |
| 12 | +Installation |
| 13 | +------------ |
| 14 | + |
| 15 | +In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to |
| 16 | +install the scheduler component: |
| 17 | + |
| 18 | +.. code-block:: terminal |
| 19 | +
|
| 20 | + $ composer require symfony/scheduler |
| 21 | +
|
| 22 | +Introduction to the case: |
| 23 | +------------------------- |
| 24 | + |
| 25 | +Embarking on a task is one thing, but often, the need to repeat that task looms large. While one could resort to issuing commands and scheduling them with cron jobs, this approach involves external tools and additional configuration. The Symfony Scheduler component emerges as a solution, allowing us to retain control, configuration, and maintenance of task scheduling within our PHP application. |
| 26 | + |
| 27 | +At its core, the principle is straightforward: a task, considered as a message needs to be managed by a service, and this cycle must be repeated. |
| 28 | +Does this sound familiar? Think :doc:`Symfony Messenger docs </components/messenger>`. |
| 29 | + |
| 30 | +But while the system of Symfony Messenger proves very useful in various scenarios, there are instances where its capabilities fall short, particularly when dealing with repetitive tasks at regular intervals. |
| 31 | + |
| 32 | +Let's dive into a practical example within the context of a sales company. |
| 33 | + |
| 34 | +Imagine the company's goal is to send diverse sales reports to customers based on the specific reports each customer chooses to receive. In constructing the schedule for this scenario, the following steps are taken: |
| 35 | + |
| 36 | +#. Iterate over reports stored in the database and create a recurring task for each report, considering its unique properties. This task, however, should not be generated during holiday periods. |
| 37 | +#. Furthermore, we encounter another critical task that needs scheduling: the periodic cleanup of outdated files that are no longer relevant. |
| 38 | + |
| 39 | +On the basis of a case study in the context of a full stack Symfony application, let's dive in and explore how we can set up our system. |
| 40 | + |
| 41 | +Symfony Scheduler basics: |
| 42 | +------------------------- |
| 43 | + |
| 44 | +Differences and parallels between Symfony Messenger and Symfony Scheduler. |
| 45 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 46 | + |
| 47 | +The primary goal is to generate and process reports generation while also handling the removal of outdated reports at specified intervals. |
| 48 | + |
| 49 | +As mentioned, this component, even if it's an independent component, it draws its foundation and inspiration from the Symfony Messenger component. |
| 50 | + |
| 51 | +On one hand, it adopts well-established concepts from Symfony Messenger (such as message, handler, bus, transport, etc.). For example, the task of creating a report is considered as a message that will be directed, and processed by the corresponding handler. |
| 52 | + |
| 53 | +However, unlike Symfony Messenger, the messages will not be dispatched in the first instance. Instead, the aim is to create them based on a predefined frequency. |
| 54 | + |
| 55 | +This is where the specific transport in Symfony Scheduler, known as the :class:`Symfony\\Component\\Scheduler\\Messenger\\SchedulerTransport`, plays a crucial role. The transport autonomously generates directly various messages according to the assigned frequencies. |
| 56 | + |
| 57 | +From (Symfony Messenger cycle): |
| 58 | + |
| 59 | +.. image:: /_images/components/messenger/basic_cycle.png |
| 60 | + :alt: Symfony Messenger basic cycle |
| 61 | + |
| 62 | +To (Symfony Scheduler cycle): |
| 63 | + |
| 64 | +.. image:: /_images/components/scheduler/scheduler_cycle.png |
| 65 | + :alt: Symfony Scheduler basic cycle |
| 66 | + |
| 67 | +In essence, it is crucial to precisely define the message to be conveyed and processed. In Symfony Scheduler, the concept of a message takes on a very particular characteristic; it should be recurrent: It's a :class:`Symfony\\Component\\Scheduler\\RecurringMessage`. |
| 68 | + |
| 69 | +Clarifying the need to define and attach Recurring Messages to a Schedule. |
| 70 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 71 | + |
| 72 | +In order to generate various messages based on their defined frequencies, configuration is necessary. |
| 73 | +The heart of the scheduling process and its configuration resides in a class that must extend the :class:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface`. |
| 74 | + |
| 75 | +The purpose of this provider is to return a schedule through the method ``getSchedule()`` containing our different recurringMessages. |
| 76 | + |
| 77 | +The class attribute ``AsSchedule``, which by default references the ``default`` named schedule, allows us to register on a particular schedule:: |
| 78 | + |
| 79 | + // src/Scheduler/MyScheduleProvider.php |
| 80 | + namespace App\Scheduler; |
| 81 | + |
| 82 | + class MyScheduleProvider implements ScheduleProviderInterface |
| 83 | + { |
| 84 | + public function getSchedule(): Schedule |
| 85 | + { |
| 86 | + ... |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | +.. tip:: |
| 91 | + |
| 92 | + This becomes important when initiating the ``messenger:consume`` command, especially when specifying one or more specific transports. |
| 93 | + In Symfony Scheduler, the transport is named ``scheduler_nameofyourschedule``. |
| 94 | + |
| 95 | +The Concept of Recurring Message in Symfony Scheduler |
| 96 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 97 | + |
| 98 | +A message associated with a Trigger |
| 99 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 100 | + |
| 101 | +First and foremost, a RecurringMessage is a message that will be associated with a trigger. |
| 102 | + |
| 103 | +The trigger is what allows configuring the recurrence frequency of your message. Several options are available to us: |
| 104 | + |
| 105 | +#. It can be a cron expression trigger: |
| 106 | + |
| 107 | +.. configuration-block:: |
| 108 | + |
| 109 | + .. code-block:: php |
| 110 | +
|
| 111 | + RecurringMessage::cron(‘*****’, new Message()) |
| 112 | + RecurringMessage::cron('#daily', $msg); |
| 113 | +
|
| 114 | +.. tip:: |
| 115 | + A hashed expressions is a string representing the schedule for a particular command to execute. |
| 116 | + Supported hashed expression: |
| 117 | + |
| 118 | + * ``#yearly``, ``#annually`` - Run once a year, midnight, Jan. 1 - ``0 0 1 1 *`` |
| 119 | + * ``#monthly`` - Run once a month, midnight, first of month - ``0 0 1 * *`` |
| 120 | + * ``#weekly`` - Run once a week, midnight on Sun - ``0 0 * * 0`` |
| 121 | + * ``#daily``, ``#midnight`` - Run once a day, midnight - ``0 0 * * *`` |
| 122 | + * ``#hourly`` - Run once an hour, first minute - ``0 * * * *`` |
| 123 | + |
| 124 | +.. tip:: |
| 125 | + |
| 126 | + It's possible to add and define a timezone as a 3rd argument |
| 127 | + |
| 128 | +#. It can be a periodicalTrigger through various frequency formats (string / integer / DateInterval) |
| 129 | + |
| 130 | +.. configuration-block:: |
| 131 | + |
| 132 | + .. code-block:: php |
| 133 | +
|
| 134 | + RecurringMessage::every('10 seconds', new Message()) |
| 135 | + RecurringMessage::every('3 weeks', new Message()) |
| 136 | + RecurringMessage::every('first Monday of next month', new Message()) |
| 137 | +
|
| 138 | + $from = new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris')); |
| 139 | + $until = '2023-06-12'; |
| 140 | + RecurringMessage::every('first Monday of next month', new Message(), $from, $until) |
| 141 | +
|
| 142 | +#. It can be a custom trigger implementing :class:`Symfony\\Component\\Scheduler\\TriggerInterface` |
| 143 | + |
| 144 | +If we go back to our scenario regarding reports generation based on our customer preferences. |
| 145 | +If the basic frequency is set to a daily basis, we will need to implement a custom trigger due to the specific requirement of not generating reports during public holiday periods:: |
| 146 | + |
| 147 | + // src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php |
| 148 | + namespace App\Scheduler\Trigger; |
| 149 | + |
| 150 | + class ExcludeHolidaysTrigger implements TriggerInterface |
| 151 | + { |
| 152 | + public function __construct(private TriggerInterface $inner) |
| 153 | + { |
| 154 | + } |
| 155 | + |
| 156 | + public function __toString(): string |
| 157 | + { |
| 158 | + return $this->inner.' (except holidays)'; |
| 159 | + } |
| 160 | + |
| 161 | + public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable |
| 162 | + { |
| 163 | + if (!$nextRun = $this->inner->getNextRunDate($run)) { |
| 164 | + return null; |
| 165 | + } |
| 166 | + |
| 167 | + while (!$this->isHoliday($nextRun) { // loop until we get the next run date that is not a holiday |
| 168 | + $nextRun = $this->inner->getNextRunDate($nextRun); |
| 169 | + } |
| 170 | + |
| 171 | + return $nextRun; |
| 172 | + } |
| 173 | + |
| 174 | + private function isHoliday(\DateTimeImmutable $timestamp): bool |
| 175 | + { |
| 176 | + // app specific logic to determine if $timestamp is on a holiday |
| 177 | + // returns true if holiday, false otherwise |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | +Then, we would have to define our RecurringMessage |
| 182 | + |
| 183 | +.. configuration-block:: |
| 184 | + |
| 185 | + .. code-block:: php |
| 186 | +
|
| 187 | + RecurringMessage::trigger( |
| 188 | + new ExcludeHolidaysTrigger( // our custom trigger wrapper |
| 189 | + CronExpressionTrigger::fromSpec('@daily'), |
| 190 | + ), |
| 191 | + new SendDailySalesReports(), |
| 192 | + ); |
| 193 | +
|
| 194 | +The RecurringMessages must be attached to a Schedule:: |
| 195 | + |
| 196 | + // src/Scheduler/MyScheduleProvider.php |
| 197 | + namespace App\Scheduler; |
| 198 | + |
| 199 | + #[AsSchedule('uptoyou')] |
| 200 | + class MyScheduleProvider implements ScheduleProviderInterface |
| 201 | + { |
| 202 | + public function getSchedule(): Schedule |
| 203 | + { |
| 204 | + return $this->schedule ??= (new Schedule()) |
| 205 | + ->with( |
| 206 | + RecurringMessage::trigger( |
| 207 | + new ExcludeHolidaysTrigger( // our custom trigger wrapper |
| 208 | + CronExpressionTrigger::fromSpec('@daily'), |
| 209 | + ), |
| 210 | + new SendDailySalesReports()), |
| 211 | + RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport()) |
| 212 | + |
| 213 | + ); |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | +So, this recurring message will encompass both the trigger, defining the generation frequency of the message, and the message itself, the one to be processed by a specific handler. |
| 218 | +But what is interesting to know is that it also provides us with the ability to generate our message dynamically. |
| 219 | + |
| 220 | +A dynamic vision for the messages generated |
| 221 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 222 | + |
| 223 | +This proves particularly useful when the message depends on data stored in databases or third-party services. |
| 224 | + |
| 225 | +Taking our example of reports generation, it depends on customer requests. Depending on the specific demands, any number of reports may need to be generated at a defined frequency. For these dynamic scenarios, it gives us the capability to dynamically define our message instead of statically. This is achieved by wrapping it in a :class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`. |
| 226 | + |
| 227 | +Essentially, this means we can dynamically define our message(s) through a callback that gets executed each time the transport checks for messages to be generated:: |
| 228 | + |
| 229 | + // src/Scheduler/MyScheduleProvider.php |
| 230 | + namespace App\Scheduler; |
| 231 | + |
| 232 | + class MyScheduleProvider implements ScheduleProviderInterface |
| 233 | + { |
| 234 | + public function getSchedule(): Schedule |
| 235 | + { |
| 236 | + return $this->schedule ??= (new Schedule()) |
| 237 | + ->with( |
| 238 | + RecurringMessage::trigger( |
| 239 | + new ExcludeHolidaysTrigger( // our custom trigger wrapper |
| 240 | + CronExpressionTrigger::fromSpec('@daily'), |
| 241 | + ), |
| 242 | + // instead of being static as in the previous example |
| 243 | + new CallbackMessageProvider([$this, 'generateReports'], 'foo')), |
| 244 | + RecurringMessage::cron(‘3 8 * * 1’, new CleanUpOldSalesReport()) |
| 245 | + |
| 246 | + ); |
| 247 | + } |
| 248 | + |
| 249 | + public function generateReports(MessageContext $context) |
| 250 | + { |
| 251 | + // ... |
| 252 | + yield new ReportSomething(); |
| 253 | + yield new ReportSomethingReportSomethingElse(); |
| 254 | + .... |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | +Exploring alternatives for crafting your Recurring Messages. |
| 259 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 260 | + |
| 261 | +- Explore alternative methods for creating recurring messages (via attributes). |
| 262 | + |
| 263 | + |
| 264 | +Managing Scheduled Messages: |
| 265 | +---------------------------- |
| 266 | + |
| 267 | +Modifying Scheduled Messages in real time |
| 268 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 269 | + |
| 270 | +- Emphasize the importance of dynamically modifying scheduled messages. |
| 271 | + |
| 272 | +Exploring Strategies for adding, removing, and modifying entries within the Schedule |
| 273 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 274 | + |
| 275 | +- Discuss approaches to add, remove, or modify messages within the schedule. |
| 276 | +- Explore actions within a handler and introduce the concept of events in Symfony. |
| 277 | + |
| 278 | +Managing Scheduled Messages via Events: |
| 279 | +--------------------------------------- |
| 280 | + |
| 281 | +A strategic event handling |
| 282 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 283 | + |
| 284 | +- Explain the different types of events in Symfony (PRE_RUN_EVENT / POST_RUN_EVENT / FAILURE_EVENT) |
| 285 | +- Mention the distinctive feature of the PRE_RUN_EVENT to prevent a message from being handled by its designated handler. |
| 286 | + |
| 287 | +Efficient management with Symfony Scheduler: |
| 288 | +-------------------------------------------- |
| 289 | + |
| 290 | +- Detail the special considerations for efficient management, including caching, locking, and the use of multiple schedules (possibility to scale up your schedules). |
| 291 | +- Demonstrate the possibility of redispatching a message. |
| 292 | + |
0 commit comments