Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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/scheduler

Scheduler 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.

ExpressionWhen
* * * * *Every minute
*/5 * * * *Every five minutes
0 * * * *At the start of every hour
0 3 * * *Every day at 3:00
0 9 * * 1On 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_default

On a server it is kept running under Supervisor or systemd — like a queue worker.

Viewing the schedule

php bin/console debug:scheduler

The 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.

Thanks for reading!

Donate to the site — we work hard for you.