aleblanc/simple-cron-scheduler 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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] to config/bundles.php, create config/packages/simple_cron_scheduler.yaml (a minimal simple_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 a php bin/console subprocess. An OOM, a segfault or an exit(255) does not affect the other tasks. memory_limit is 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
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-24