aleblanc/simple-cron-scheduler
Composer 安装命令:
composer require aleblanc/simple-cron-scheduler
包简介
Symfony bundle to orchestrate cron through a single crontab entry + a declarative schedule (PHP attributes or YAML), with subprocess isolation. No Messenger, no long-running daemon.
README 文档
README
Symfony bundle to orchestrate cron through a single crontab entry + a declarative schedule (PHP attributes or YAML), with subprocess isolation by default. No Messenger, no long-running daemon. MIT.
Targets Symfony 7.2 → 10, PHP 8.4+.
* * * * * php /var/www/app/bin/console scheduler:run
Everything else lives in your application code.
Why this bundle
It is an alternative to symfony/scheduler that needs neither Messenger nor Supervisor.
symfony/scheduler dispatches its tasks through Messenger: you need a transport and a
messenger:consume worker kept alive by a process supervisor (Supervisor, systemd…) that you deploy
and monitor. simple-cron-scheduler removes all of that: the cadence comes from the OS cron
(* * * * *), which wakes a fresh process every minute. No worker to keep alive, no transport,
nothing to supervise — a crash has nothing to restart.
In spirit it is close to Laravel's scheduler (a single crontab line, a declarative schedule in
code, @daily macros, between windows, environment restriction) — brought to Symfony with a
repeatable #[AsCronTask] attribute, native shell tasks, and per-task memory isolation via
subprocess on top.
symfony/scheduler |
simple-cron-scheduler | |
|---|---|---|
| Long-running worker | required (messenger:consume) |
❌ none |
| Supervisor / systemd | required to keep the worker alive | ❌ not needed |
| Messenger / transport | required | ❌ none |
| Trigger | the worker | OS cron (* * * * *) |
| Memory isolation | depends on the transport | ✅ subprocess by default |
| Native shell tasks | no | ✅ yes |
Installation (automatic, with Flex)
The package ships a Flex recipe: the quickest way is to point your app at this repo as a custom recipe endpoint, then require the package — the bundle and config are wired automatically.
1. Add the recipe endpoint to your app's composer.json:
{
"extra": {
"symfony": {
"allow-contrib": true,
"endpoint": [
"https://raw.githubusercontent.com/aleblanc/simple-cron-scheduler/main/index.json",
"flex://defaults"
]
}
}
}
2. Require the package — the recipe runs on install:
composer require aleblanc/simple-cron-scheduler
This registers the bundle in config/bundles.php, creates config/packages/simple_cron_scheduler.yaml,
and prints a post-install reminder. The bundle's configuration alias is simple_cron_scheduler.
3. Add the single crontab entry on the server (the only manual step — Flex does not touch the crontab):
* * * * * php /var/www/app/bin/console scheduler:run >> /var/log/scheduler.log 2>&1
That's it — no worker, no Supervisor. Check your tasks with php bin/console scheduler:run --list.
Without the recipe (or before tagging a release), do it by hand: add
SimpleCronScheduler\SimpleCronSchedulerBundle::class => ['all' => true]toconfig/bundles.php, createconfig/packages/simple_cron_scheduler.yaml(a minimalsimple_cron_scheduler: { timezone: Europe/Paris }is enough), then add the crontab line. Recipe details:recipes/README.md.
Declaring tasks with an attribute (#[AsCronTask])
The attribute is repeatable: stack several schedules on the same command, with different arguments.
use SimpleCronScheduler\Attribute\AsCronTask; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; #[AsCommand(name: 'app:sync-feed')] #[AsCronTask('*/2 * * * *', between: ['7:00', '22:00'], skipMinutes: [2, 32], description: 'Daytime sync')] #[AsCronTask('*/15 * * * *', unlessBetween: ['7:00', '22:00'], skipMinutes: [2, 32], description: 'Nighttime sync')] #[AsCronTask('@daily', args: ['--full'], env: ['prod'])] // Hourly, weekdays only (Mon–Fri), but never between midnight and 6am, never in December: #[AsCronTask('0 * * * *', days: [1, 2, 3, 4, 5], skipHours: [0, 1, 2, 3, 4, 5], skipMonths: [12])] final class SyncFeedCommand extends Command { /* ... */ }
#[AsCronTask] fields
| Field | Type | Default | Description |
|---|---|---|---|
expression |
string |
required | 5-field cron or a macro (@daily, @hourly, @weekly, @monthly, @yearly). |
name |
?string |
null |
Unique name. Derived from the command name when absent. |
description |
?string |
null |
Human-readable label shown by scheduler:run --list. |
args |
string[] |
[] |
CLI arguments passed to the command. |
runner |
?string |
null |
process | in_process. null = global default. |
timeout |
?int |
null |
Max seconds (process mode). |
skipMinutes |
int[] |
[] |
Minutes to exclude (0–59). |
skipHours |
int[] |
[] |
Hours to exclude (0–23). |
skipDays |
int[] |
[] |
Weekdays to exclude (0=Sunday … 6=Saturday). |
skipMonths |
int[] |
[] |
Months to exclude (1–12). |
days |
int[] |
[] |
Allowlist of weekdays (0=Sunday … 6=Saturday). Empty = every day. |
between |
[from, to] |
null |
Run ONLY within this time window. |
unlessBetween |
[from, to] |
null |
Do NOT run within this window (handles the midnight wrap). |
env |
string[] |
[] |
Allowed environments (%kernel.environment%). Empty = all. |
when |
?string |
null |
Service id of a CronCondition implementation. |
group |
?string |
null |
Filtering via --group. |
disabled |
bool |
false |
Disable without removing the attribute. |
Declaring tasks in YAML (shell / command / service)
config/packages/simple_cron_scheduler.yaml:
simple_cron_scheduler: timezone: Europe/Paris default_runner: process # process | in_process default_timeout: 600 # seconds log_channel: scheduler log_output_max_bytes: 8192 lock_runner: true # anti-overlap on scheduler:run tasks: clear-old-cache: schedule: '0 5 * * 1' shell: 'rm -rf var/cache/old' import-feed-a: schedule: '1,31 0-6 * * *' command: 'app:import:feed' args: ['partner-a'] prune-orphans: schedule: '@daily' service: 'App\Cron\OrphanPruner' # __invoke(\DateTimeImmutable $now): void env: ['prod']
Each entry has exactly one of command:, shell: or service: — otherwise it fails at boot.
All runtime fields (skipMinutes, skipHours, skipDays, skipMonths, days, between,
unlessBetween, env, when, description, group, disabled, runner, timeout) are available
in YAML too:
business-hours-report: schedule: '0 * * * *' # every hour… command: 'app:report:hourly' days: [1, 2, 3, 4, 5] # …on weekdays only (0=Sun … 6=Sat) skipHours: [0, 1, 2, 3, 4, 5] # …never during the night skipMonths: [12] # …and never in December
A full example lives in doc/examples/simple_cron_scheduler.yaml.
Calendar conditions
Beyond the cron expression itself, these additive filters refine when a task runs (all of them must
pass; any match on a skip* list excludes the run):
| Field | Excludes / restricts | Values |
|---|---|---|
skipMinutes |
minutes to skip | 0–59 |
skipHours |
hours to skip | 0–23 |
skipDays |
weekdays to skip | 0=Sunday … 6=Saturday |
skipMonths |
months to skip | 1–12 |
days |
allowlist of weekdays (runs only on these) | 0=Sunday … 6=Saturday |
between / unlessBetween |
time-of-day window | ['HH:MM', 'HH:MM'] |
They combine freely — e.g. days: [1..5] + skipHours: [0..5] = weekday business-hours only. When a
task is ruled out, a TaskSkippedEvent is dispatched with the matching reason
(skipHours, days, skipDays, skipMonths, …).
Execution modes (runners)
process(default): each command runs in aphp bin/consolesubprocess. An OOM, a segfault or anexit(255)does not affect the other tasks.memory_limitis reset for each task.in_process: runs in the current process (zero overhead, but no memory isolation). Good for short, predictable tasks and tests.shell:Process::fromShellCommandline()→ pipes,&&, redirections. No PHP involved.service: invokes a service__invoke(\DateTimeImmutable $now)in the current process.
Global via default_runner, overridable per task via runner:.
when conditions
Create a service implementing CronCondition (auto-tagged — no config needed):
namespace App\Cron; use SimpleCronScheduler\Condition\CronCondition; final class NotPublicHoliday implements CronCondition { public function isAllowed(\DateTimeImmutable $now): bool { return !$this->holidays->contains($now); } }
Reference it by its service id (= FQCN with default autowiring):
#[AsCronTask('0 9 * * 1-5', when: \App\Cron\NotPublicHoliday::class)]
Observability — events
Each run dispatches Symfony events. Wire your own listeners (alerts, email, metrics):
| Event | When | Payload |
|---|---|---|
TaskStartingEvent |
before execution | task, now |
TaskFinishedEvent |
after execution (success or failure) | task, result |
TaskFailedEvent |
exit ≠ 0 or exception | task, result, exception |
TaskSkippedEvent |
task ruled out by a condition | task, reason |
Example — email the output on failure (Laravel's emailOutputOnFailure equivalent):
use SimpleCronScheduler\Event\TaskFailedEvent; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] final class MailOnTaskFailure { public function __construct(private MailerInterface $mailer) {} public function __invoke(TaskFailedEvent $event): void { $this->mailer->send((new Email()) ->to('ops@example.com') ->subject(sprintf('[cron] %s failed (exit %d)', $event->task->name, $event->result->exitCode)) ->text($event->result->output)); } }
Anti-overlap
scheduler:run uses LockableTrait (symfony/lock + FlockStore), enabled by default
(lock_runner: true). If a tick runs longer than a minute, the next tick is skipped cleanly (exit 0).
Optional belt-and-suspenders at the crontab level for slow Symfony boots:
* * * * * /usr/bin/flock -n /var/lock/scheduler.lock php /var/www/app/bin/console scheduler:run
Commands
| Command | Role |
|---|---|
scheduler:run |
Crontab entry — runs the tasks that are due. Always returns 0. |
scheduler:run --list |
List all tasks (name, expression, type, description). |
scheduler:run --dry-run |
Show what would run without executing. |
scheduler:run --only=NAME |
Run only the named task. |
scheduler:run --group=G |
Run only the tasks of that group. |
scheduler:list |
Alias of scheduler:run --list. |
Tests & quality
composer install composer test # PHPUnit (42 tests) composer stan # PHPStan level 8 composer cs # PHP-CS-Fixer (dry-run, @Symfony + @PSR12) composer cs-fix # PHP-CS-Fixer (apply fixes) composer qa # cs + stan + test
Configs: phpstan.dist.neon (level 8, src + tests) and
.php-cs-fixer.dist.php.
License
MIT
统计信息
- 总下载量: 1
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-24