Skip to content

Commit 5865129

Browse files
committed
Merge branch '7.0' into 7.1
* 7.0: Tweaks and rewords of the new Scheduler docs [Scheduler] Proposal - initial structure for Symfony Scheduler documentation
2 parents 6ce725d + 97bfa5a commit 5865129

File tree

4 files changed

+379
-0
lines changed

4 files changed

+379
-0
lines changed
89.3 KB
Loading
Loading
Loading

scheduler.rst

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
Scheduler
2+
=========
3+
4+
.. versionadded:: 6.3
5+
6+
The Scheduler component was introduced in Symfony 6.3
7+
8+
The scheduler component manages task scheduling within your PHP application, like
9+
running a task each night at 3 AM, every two weeks except for holidays or any
10+
other custom schedule you might need.
11+
12+
This component is useful to schedule tasks like maintenance (database cleanup,
13+
cache clearing, etc.), background processing (queue handling, data synchronization,
14+
etc.), periodic data updates, scheduled notifications (emails, alerts), and more.
15+
16+
This document focuses on using the Scheduler component in the context of a full
17+
stack Symfony application.
18+
19+
Installation
20+
------------
21+
22+
In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to
23+
install the scheduler component:
24+
25+
.. code-block:: terminal
26+
27+
$ composer require symfony/scheduler
28+
29+
Symfony Scheduler Basics
30+
------------------------
31+
32+
The main benefit of using this component is that automation is managed by your
33+
application, which gives you a lot of flexibility that is not possible with cron
34+
jobs (e.g. dynamic schedules based on certain conditions).
35+
36+
At its core, the Scheduler component allows you to create a task (called a message)
37+
that is executed by a service and repeated on some schedule. It has some similarities
38+
with the :doc:`Symfony Messenger </components/messenger>` component (such as message,
39+
handler, bus, transport, etc.), but the main difference is that Messenger can't
40+
deal with repetitive tasks at regular intervals.
41+
42+
Consider the following example of an application that sends some reports to
43+
customers on a scheduled basis. First, create a Scheduler message that represents
44+
the task of creating a report::
45+
46+
// src/Scheduler/Message/SendDailySalesReports.php
47+
namespace App\Scheduler\Message;
48+
49+
class SendDailySalesReports
50+
{
51+
public function __construct(private string $id) {}
52+
53+
public function getId(): int
54+
{
55+
return $this->id;
56+
}
57+
}
58+
59+
Next, create the handler that processes that kind of message::
60+
61+
// src/Scheduler/Handler/SendDailySalesReportsHandler.php
62+
namespace App\Scheduler\Handler;
63+
64+
#[AsMessageHandler]
65+
class SendDailySalesReportsHandler
66+
{
67+
public function __invoke(SendDailySalesReports $message)
68+
{
69+
// ... do some work to send the report to the customers
70+
}
71+
}
72+
73+
Instead of sending these messages immediately (as in the Messenger component),
74+
the goal is to create them based on a predefined frequency. This is possible
75+
thanks to :class:`Symfony\\Component\\Scheduler\\Messenger\\SchedulerTransport`,
76+
a special transport for Scheduler messages.
77+
78+
The transport generates, autonomously, various messages according to the assigned
79+
frequencies. The following images illustrate the differences between the
80+
processing of messages in Messenger and Scheduler components:
81+
82+
In Messenger:
83+
84+
.. image:: /_images/components/messenger/basic_cycle.png
85+
:alt: Symfony Messenger basic cycle
86+
87+
In Scheduler:
88+
89+
.. image:: /_images/components/scheduler/scheduler_cycle.png
90+
:alt: Symfony Scheduler basic cycle
91+
92+
Another important difference is that messages in the Scheduler component are
93+
recurring. They are represented via the :class:`Symfony\\Component\\Scheduler\\RecurringMessage`
94+
class.
95+
96+
Attaching Recurring Messages to a Schedule
97+
------------------------------------------
98+
99+
The configuration of the message frequency is stored in a class that implements
100+
:class:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface`. This provider
101+
uses the method :method:`Symfony\\Component\\Scheduler\\ScheduleProviderInterface::getSchedule`
102+
to return a schedule containing the different recurring messages.
103+
104+
The :class:`Symfony\\Component\\Scheduler\\Attribute\\AsSchedule` attribute,
105+
which by default references the schedule named ``default``, allows you to register
106+
on a particular schedule::
107+
108+
// src/Scheduler/MyScheduleProvider.php
109+
namespace App\Scheduler;
110+
111+
#[AsSchedule]
112+
class SaleTaskProvider implements ScheduleProviderInterface
113+
{
114+
public function getSchedule(): Schedule
115+
{
116+
// ...
117+
}
118+
}
119+
120+
.. tip::
121+
122+
By default, the schedule name is ``default`` and the transport name follows
123+
the syntax: ``scheduler_nameofyourschedule`` (e.g. ``scheduler_default``).
124+
125+
.. tip::
126+
127+
`Memoizing`_ your schedule is a good practice to prevent unnecessary reconstruction
128+
if the ``getSchedule()`` method is checked by another service.
129+
130+
Scheduling Recurring Messages
131+
-----------------------------
132+
133+
A ``RecurringMessage`` is a message associated with a trigger, which configures
134+
the frequency of the message. Symfony provides different types of triggers:
135+
136+
Cron Expression Triggers
137+
~~~~~~~~~~~~~~~~~~~~~~~~
138+
139+
It uses the same syntax as the `cron command-line utility`_::
140+
141+
RecurringMessage::cron('* * * * *', new Message());
142+
143+
Before using it, you must install the following dependency:
144+
145+
.. code-block:: terminal
146+
147+
composer require dragonmantank/cron-expression
148+
149+
.. tip::
150+
151+
Check out the `crontab.guru website`_ if you need help to construct/understand
152+
cron expressions.
153+
154+
.. versionadded:: 6.4
155+
156+
Since version 6.4, it is now possible to add and define a timezone as a 3rd argument
157+
158+
Periodical Triggers
159+
~~~~~~~~~~~~~~~~~~~
160+
161+
These triggers allows to configure the frequency using different data types
162+
(``string``, ``integer``, ``DateInterval``). They also support the `relative formats`_
163+
defined by PHP datetime functions::
164+
165+
RecurringMessage::every('10 seconds', new Message());
166+
RecurringMessage::every('3 weeks', new Message());
167+
RecurringMessage::every('first Monday of next month', new Message());
168+
169+
$from = new \DateTimeImmutable('13:47', new \DateTimeZone('Europe/Paris'));
170+
$until = '2023-06-12';
171+
RecurringMessage::every('first Monday of next month', new Message(), $from, $until);
172+
173+
Custom Triggers
174+
~~~~~~~~~~~~~~~
175+
176+
Custom triggers allow to configure any frequency dynamically. They are created
177+
as services that implement :class:`Symfony\\Component\\Scheduler\\TriggerInterface`.
178+
179+
For example, if you want to send customer reports daily except for holiday periods::
180+
181+
// src/Scheduler/Trigger/NewUserWelcomeEmailHandler.php
182+
namespace App\Scheduler\Trigger;
183+
184+
class ExcludeHolidaysTrigger implements TriggerInterface
185+
{
186+
public function __construct(private TriggerInterface $inner)
187+
{
188+
}
189+
190+
public function __toString(): string
191+
{
192+
return $this->inner.' (except holidays)';
193+
}
194+
195+
public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable
196+
{
197+
if (!$nextRun = $this->inner->getNextRunDate($run)) {
198+
return null;
199+
}
200+
201+
// loop until you get the next run date that is not a holiday
202+
while (!$this->isHoliday($nextRun) {
203+
$nextRun = $this->inner->getNextRunDate($nextRun);
204+
}
205+
206+
return $nextRun;
207+
}
208+
209+
private function isHoliday(\DateTimeImmutable $timestamp): bool
210+
{
211+
// add some logic to determine if the given $timestamp is a holiday
212+
// return true if holiday, false otherwise
213+
}
214+
}
215+
216+
Then, define your recurring message::
217+
218+
RecurringMessage::trigger(
219+
new ExcludeHolidaysTrigger(
220+
CronExpressionTrigger::fromSpec('@daily'),
221+
),
222+
new SendDailySalesReports('...'),
223+
);
224+
225+
Finally, the recurring messages must be attached to a schedule::
226+
227+
// src/Scheduler/MyScheduleProvider.php
228+
namespace App\Scheduler;
229+
230+
#[AsSchedule('uptoyou')]
231+
class SaleTaskProvider implements ScheduleProviderInterface
232+
{
233+
public function getSchedule(): Schedule
234+
{
235+
return $this->schedule ??= (new Schedule())
236+
->with(
237+
RecurringMessage::trigger(
238+
new ExcludeHolidaysTrigger(
239+
CronExpressionTrigger::fromSpec('@daily'),
240+
),
241+
new SendDailySalesReports()
242+
),
243+
RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport())
244+
);
245+
}
246+
}
247+
248+
Consuming Messages (Running the Worker)
249+
---------------------------------------
250+
251+
After defining and attaching your recurring messages to a schedule, you'll need
252+
a mechanism to generate and consume the messages according to their defined frequencies.
253+
To do that, the Scheduler component uses the ``messenger:consume`` command from
254+
the Messenger component:
255+
256+
.. code-block:: terminal
257+
258+
$ php bin/console messenger:consume scheduler_nameofyourschedule
259+
260+
# use -vv if you need details about what's happening
261+
$ php bin/console messenger:consume scheduler_nameofyourschedule -vv
262+
263+
.. image:: /_images/components/scheduler/generate_consume.png
264+
:alt: Symfony Scheduler - generate and consume
265+
266+
.. versionadded:: 6.4
267+
268+
Since version 6.4, you can define your messages via a ``callback`` via the
269+
:class:`Symfony\\Component\\Scheduler\\Trigger\\CallbackMessageProvider`.
270+
271+
Debugging the Schedule
272+
----------------------
273+
274+
The ``debug:scheduler`` command provides a list of schedules along with their
275+
recurring messages. You can narrow down the list to a specific schedule:
276+
277+
.. code-block:: terminal
278+
279+
$ php bin/console debug:scheduler
280+
281+
Scheduler
282+
=========
283+
284+
default
285+
-------
286+
287+
------------------- ------------------------- ----------------------
288+
Trigger Provider Next Run
289+
------------------- ------------------------- ----------------------
290+
every 2 days App\Messenger\Foo(0:17..) Sun, 03 Dec 2023 ...
291+
15 4 */3 * * App\Messenger\Foo(0:17..) Mon, 18 Dec 2023 ...
292+
-------------------- -------------------------- ---------------------
293+
294+
.. versionadded:: 6.4
295+
296+
Since version 6.4, you can even specify a date to determine the next run date
297+
using the ``--date`` option. Additionally, you have the option to display
298+
terminated recurring messages using the ``--all`` option.
299+
300+
Efficient management with Symfony Scheduler
301+
-------------------------------------------
302+
303+
If a worker becomes idle, the recurring messages won't be generated (because they
304+
are created on-the-fly by the scheduler transport).
305+
306+
That's why the scheduler allows to remember the last execution date of a message
307+
via the ``stateful`` option (and the :doc:`Cache component </components/cache>`).
308+
This way, when it wakes up again, it looks at all the dates and can catch up on
309+
what it missed::
310+
311+
// src/Scheduler/MyScheduleProvider.php
312+
namespace App\Scheduler;
313+
314+
#[AsSchedule('uptoyou')]
315+
class SaleTaskProvider implements ScheduleProviderInterface
316+
{
317+
public function getSchedule(): Schedule
318+
{
319+
$this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());
320+
321+
return $this->schedule ??= (new Schedule())
322+
->with(
323+
// ...
324+
);
325+
->stateful($this->cache)
326+
}
327+
}
328+
329+
To scale your schedules more effectively, you can use multiple workers. In such
330+
cases, a good practice is to add a :doc:`lock </components/lock>` to prevent the
331+
same task more than once::
332+
333+
// src/Scheduler/MyScheduleProvider.php
334+
namespace App\Scheduler;
335+
336+
#[AsSchedule('uptoyou')]
337+
class SaleTaskProvider implements ScheduleProviderInterface
338+
{
339+
public function getSchedule(): Schedule
340+
{
341+
$this->removeOldReports = RecurringMessage::cron('3 8 * * 1', new CleanUpOldSalesReport());
342+
343+
return $this->schedule ??= (new Schedule())
344+
->with(
345+
// ...
346+
);
347+
->lock($this->lockFactory->createLock('my-lock')
348+
}
349+
}
350+
351+
.. tip::
352+
353+
The processing time of a message matters. If it takes a long time, all subsequent
354+
message processing may be delayed. So, it's a good practice to anticipate this
355+
and plan for frequencies greater than the processing time of a message.
356+
357+
Additionally, for better scaling of your schedules, you have the option to wrap
358+
your message in a :class:`Symfony\\Component\\Messenger\\Message\\RedispatchMessage`.
359+
This allows you to specify a transport on which your message will be redispatched
360+
before being further redispatched to its corresponding handler::
361+
362+
// src/Scheduler/MyScheduleProvider.php
363+
namespace App\Scheduler;
364+
365+
#[AsSchedule('uptoyou')]
366+
class SaleTaskProvider implements ScheduleProviderInterface
367+
{
368+
public function getSchedule(): Schedule
369+
{
370+
return $this->schedule ??= (new Schedule())
371+
->with(RecurringMessage::every('5 seconds'), new RedispatchMessage(new Message(), 'async'))
372+
);
373+
}
374+
}
375+
376+
.. _`Memoizing`: https://en.wikipedia.org/wiki/Memoization
377+
.. _`cron command-line utility`: https://en.wikipedia.org/wiki/Cron
378+
.. _`crontab.guru website`: https://crontab.guru/
379+
.. _`relative formats`: https://www.php.net/manual/en/datetime.formats.php#datetime.formats.relative

0 commit comments

Comments
 (0)