Skip to content

Symfony

since: 1.8

runwisp/runwisp runs your Symfony Scheduler schedules under RunWisp. #[AsCronTask], #[AsPeriodicTask] and your schedule providers stay as they are. bin/console runwisp:work replaces messenger:consume scheduler_default, and every run gets a log, an exit code and a duration in the web UI.

It works with Symfony 6.4 or later, on PHP 8.2 or later. See the PHP SDK for how the worker runs tasks.

Terminal window
composer require runwisp/runwisp

Register the bundle:

config/bundles.php
<?php
return [
// ...
RunWisp\Symfony\RunWispBundle::class => ['all' => true],
];

Start the worker:

Terminal window
bin/console runwisp:work

It starts RunWisp and prints where to find it:

RunWisp dashboard: http://127.0.0.1:9477, password: vendor/bin/runwisp password --data var/runwisp

RunWisp keeps its database, run logs and socket in var/runwisp. Keep the worker running the way you run messenger:consume: a systemd unit, a Supervisor program or your container’s command. bin/console runwisp:work --env=prod runs every task in the prod environment too.

Each recurring message of each schedule becomes a RunWisp task:

src/Command/SendDigestCommand.php
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCommand('app:send-digest')]
#[AsCronTask('0 8 * * *', timezone: 'Europe/Bratislava', jitter: 60)]
final class SendDigestCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Sending the daily digest');
return Command::SUCCESS;
}
}

Cron triggers need dragonmantank/cron-expression, as they do with messenger:consume:

Terminal window
composer require dragonmantank/cron-expression

This task is named app:send-digest, with cron 0 8 * * *, timezone Europe/Bratislava and jitter 60s.

Trigger Task
#[AsCronTask], cron() cron as written, and timezone when the trigger has one.
#[AsPeriodicTask], every() cron = "@every <n>s".
jitter, withJitter() jitter in seconds, on top of the trigger.
Other triggers Skipped, with a warning when the worker starts.

Intervals without a fixed length, such as 1 month, are skipped too. Use a cron expression for them.

The task name comes from the message:

Message Task name
A command (#[AsCronTask] on it) The command name: app:send-digest.
A service method Service-Class::method.
Any other message The message’s class name, without namespace.

A name in #[Task] comes first. Otherwise, a schedule other than default prefixes the name: reports.SendReport. Two messages that get the same name are told apart with -2, and the worker warns about it. A task keeps its run history under its name, so rename the message or its schedule with care.

Each run is a new bin/console runwisp:run <name> process. A command runs there with its output going to the run’s log. Any other message is handled there and then by your message handlers, as the scheduler’s consumer would. A message with transports is sent to those transports, as Symfony does.

A thrown error fails the run with exit code 1 and the stack trace in its log, and a command’s exit code is the run’s. Run a task by hand with the same command:

Terminal window
bin/console runwisp:run app:send-digest

Give a task the task keys Symfony has no word for with #[RunWisp\Task], next to its #[AsCronTask]. The keys are named as in runwisp.toml:

src/Command/SendDigestCommand.php
<?php
namespace App\Command;
use RunWisp\Task;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
#[AsCommand('app:send-digest')]
#[AsCronTask('0 8 * * *', timezone: 'Europe/Bratislava', jitter: 60)]
#[Task(retry_attempts: 3, timeout: '10m', notify: ['slack'])]
final class SendDigestCommand extends Command
{
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Sending the daily digest');
return Command::SUCCESS;
}
}

The first argument is the task name: #[Task('digest', retry_attempts: 3)]. For a service method, put the attribute on the method or its class; for any other message, on the message class. The schedule comes from Symfony, so cron and timezone are not accepted.

The bundle’s config holds the rest of a runwisp.toml, such as notifiers and defaults, with the same keys:

config/packages/runwisp.yaml
runwisp:
config:
notifiers:
slack:
type: slack
webhook_url: "%env(SLACK_WEBHOOK_URL)%"

bin/console runwisp:check lists the tasks runwisp:work would run, with each schedule, next run and RunWisp options, and checks the config with RunWisp, without starting anything:

Terminal window
bin/console runwisp:check

It exits with code 1 when RunWisp would reject the config, such as for a misspelled key, so you can run it in CI. See Check your tasks.

Every key is optional. A key you leave out falls back to its RUNWISP_* environment variable, as described in the PHP SDK.

config/packages/runwisp.yaml
runwisp:
data: "%kernel.project_dir%/var/runwisp"
port: 9477
password: "%env(RUNWISP_PASSWORD)%"
Key Default
app The name in composer.json
data RUNWISP_DATA, else var/runwisp
port RUNWISP_PORT, else 9477 or the next free port
host RUNWISP_HOST, else 127.0.0.1
password RUNWISP_PASSWORD
auth true; false turns the login off
binary The binary Composer installed, else runwisp on PATH
config The rest of a runwisp.toml