Scheduled tasks
Clean up old records once a day, send a report every Monday, check payments every five minutes. Each such task used to need a line in the system cron. The Scheduler component keeps the schedule in the application code.
Installation
composer require symfony/schedulerScheduler works on top of Messenger: a schedule is a source of messages handled by a regular worker.
A task with a cron expression
namespace App\Scheduler;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCronTask('0 3 * * *')]
class CleanupOldSessions
{
public function __construct(private SessionRepository $sessions) {}
public function __invoke(): void
{
$this->sessions->deleteOlderThan(new \DateTimeImmutable('-30 days'));
}
}The class runs every day at 3:00. Cron expressions need the dragonmantank/cron-expression package.
The expression format
Five fields: minute, hour, day of the month, month, day of the week.
| Expression | When |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every five minutes |
0 * * * * | At the start of every hour |
0 3 * * * | Every day at 3:00 |
0 9 * * 1 | On Mondays at 9:00 |
0 0 1 * * | On the first day of every month |
A task at equal intervals
use Symfony\Component\Scheduler\Attribute\AsPeriodicTask;
#[AsPeriodicTask(frequency: '10 minutes')]
class CheckPendingPayments
{
public function __invoke(): void
{
// checking payments
}
}A command on a schedule
The same attribute can mark a console command:
#[AsCommand(name: 'app:send-report')]
#[AsCronTask('0 9 * * 1')]
class SendReportCommand extends Command
{
// …
}Running
A Messenger worker executes the schedule:
php bin/console messenger:consume scheduler_defaultOn a server it is kept running under Supervisor or systemd — like a queue worker.
Viewing the schedule
php bin/console debug:schedulerThe command shows every task and the time of its next run.
A custom schedule
When the schedule depends on settings or data, it is assembled in a class:
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
#[AsSchedule('reports')]
class ReportSchedule implements ScheduleProviderInterface
{
public function getSchedule(): Schedule
{
return (new Schedule())
->add(RecurringMessage::cron('0 9 * * 1', new SendWeeklyReport()))
->add(RecurringMessage::every('1 hour', new RefreshRates()));
}
}Such a schedule is run separately: messenger:consume scheduler_reports.
Common mistake
If two workers run the same schedule — on two servers, for instance — every task runs twice: the report is sent twice, the payment check runs in parallel. Keep one worker per schedule or add a lock with the lock() method of Schedule — then only one process takes a task.