Skip to content

Rewrite contributte/scheduler on top of symfony/scheduler #32

@ohmyfelix

Description

@ohmyfelix

Goal

Rewrite contributte/scheduler to be a real Nette integration for symfony/scheduler, following the same overall concept that contributte/messenger uses for symfony/messenger.

Why

The current package is still a custom cron/callback runner with its own runtime model (IJob, IScheduler, scheduler:run, lock files, callback jobs).
That works, but it is not aligned with Symfony Scheduler's architecture and misses the richer ecosystem around schedules, recurring messages, Messenger workers, runtime events, stateful execution, and locks.

contributte/messenger is already a strong example of how Contributte integrates a Symfony component into Nette:

  • one DI extension as the public entry point
  • internal compiler passes by concern
  • native Symfony runtime/services exposed in Nette
  • good docs and focused DI/E2E tests

contributte/scheduler should move in the same direction.

Current situation

Today contributte/scheduler is centered around:

  • IJob and IScheduler
  • Scheduler / LockingScheduler
  • config-driven scheduler.jobs
  • callback jobs and custom job classes
  • custom commands:
    • scheduler:run
    • scheduler:list
    • scheduler:force-run
    • scheduler:help

That model is fundamentally different from Symfony Scheduler, which is based on:

  • ScheduleProviderInterface
  • Schedule
  • RecurringMessage
  • Messenger-backed scheduler transports
  • messenger:consume scheduler_<name>
  • debug:scheduler
  • optional runtime events, stateful cache, and lock integration

Proposed direction

Rebuild the package as a thin Nette bridge over Symfony Scheduler, similar in spirit to contributte/messenger.

Core design

  • Keep a single SchedulerExtension as public DI entry point.
  • Split integration into small passes, e.g.:
    • ProviderPass
    • TaskPass
    • TransportPass
    • EventPass
    • ConsolePass
    • DebugPass
  • Make Symfony Scheduler concepts the primary API:
    • ScheduleProviderInterface
    • Schedule
    • RecurringMessage
    • #[AsSchedule]
  • Support Symfony task attributes as an upper layer:
    • #[AsCronTask]
    • #[AsPeriodicTask]

Runtime integration

  • Register a schedule-provider locator.
  • Register SchedulerTransportFactory.
  • Create one Messenger transport per schedule:
    • scheduler_<name> -> schedule://<name>
  • Reuse Messenger worker flow:
    • messenger:consume scheduler_<name>
  • Register scheduler event bridge:
    • DispatchSchedulerEventListener
  • Register ServiceCallMessageHandler for service/method-based task definitions.
  • Expose native scheduler debug tooling:
    • debug:scheduler

Dependencies

Likely runtime direction:

  • php >=8.2
  • nette/di
  • symfony/scheduler
  • symfony/messenger
  • symfony/console

Optional integrations:

  • dragonmantank/cron-expression
  • symfony/cache
  • symfony/lock
  • symfony/event-dispatcher

Migration strategy

This is not a small refactor; it is a model change.

Recommended compatibility layer

Keep the scheduler extension name, but introduce a transition layer that compiles legacy config into Symfony schedules where possible.

Preserve temporarily:

  • scheduler.jobs
  • callback-based jobs
  • service-based jobs
  • scheduler.path semantics via a lock adapter

Deprecate:

  • IJob
  • IScheduler
  • Scheduler
  • LockingScheduler
  • custom imperative runtime access
  • legacy custom commands that do not map cleanly to Symfony Scheduler

Command migration

Keep/review:

  • debug:scheduler as first-class

Deprecate or replace:

  • scheduler:run -> use messenger:consume scheduler_<name>
  • scheduler:list -> use debug:scheduler
  • scheduler:force-run -> likely no direct equivalent; either drop or reintroduce only as an explicit compatibility tool
  • scheduler:help -> not needed once docs and native tooling are in place

Iteration plan

Iteration 1 - Symfony-native MVP

Implement the clean core first:

  • explicit ScheduleProviderInterface services
  • schedule provider discovery
  • scheduler transports
  • messenger:consume scheduler_<name>
  • debug:scheduler
  • minimal docs and tests

This gives the cleanest architecture and proves the bridge works.

Iteration 2 - Compatibility layer

Add migration support for current users:

  • compile legacy scheduler.jobs into recurring messages
  • support callback/service-based task definitions
  • map scheduler.path to Symfony lock support
  • emit deprecations for old API/commands

This reduces upgrade pain.

Iteration 3 - High-level ergonomics

Add developer-friendly features:

  • #[AsSchedule]
  • #[AsCronTask]
  • #[AsPeriodicTask]
  • config-defined tasks/messages
  • optional stateful cache support
  • optional event-dispatcher integration
  • improved examples and cookbook docs

Testing plan

Add real coverage, not only config parsing:

  • DI compile tests
  • provider discovery tests
  • legacy config migration tests
  • attribute-based task tests
  • lock integration tests
  • stateful schedule tests
  • console command tests
  • E2E worker/schedule scenarios

Docs plan

Update docs to include:

  • installation
  • Messenger dependency/integration story
  • minimal setup
  • schedule providers
  • task attributes
  • locks and stateful mode
  • consuming schedules
  • debugging
  • migration guide from legacy scheduler.jobs

Open questions

  • Should contributte/messenger become a hard dependency, or should contributte/scheduler wire symfony/messenger directly?
  • How much of the old scheduler.jobs config should be preserved during transition?
  • Should scheduler:force-run exist in the new world at all?
  • Is this rewrite released as the next major version only?

Acceptance criteria

  • Package is centered on symfony/scheduler, not a custom in-process scheduler loop.
  • Nette users can define schedules/providers and consume them via Messenger workers.
  • debug:scheduler works in Nette apps.
  • There is a documented upgrade path from current contributte/scheduler.
  • Tests cover DI, runtime wiring, and realistic schedule execution scenarios.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions