rewieer/taskschedulerbundle
TaskSchedulerBundle makes it easy to schedule recurring PHP tasks with cron-style expressions. Register tasks as services, run them through a single console command, and manage background jobs cleanly in Symfony applications without extra infrastructure.
TaskSchedulerBundle is a Symfony bundle that lets you declare scheduled tasks as services (each with a cron expression) and run them from a single long-lived runner command, instead of scattering cron entries across servers.
Caveat before you install: this is a Symfony bundle, not a Laravel package. If you are on Laravel, the same concepts live in Illuminate\Console\Scheduling\Schedule (->cron(), ->withoutOverlapping(), ->runInBackground()), and that is what you should use day-to-day. Read this package when you are maintaining a Symfony service alongside your Laravel apps, or when you want a reference model for the daemon-plus-locks pattern.
Minimal steps:
composer require ancyrweb/task-scheduler-bundle and register the bundle in config/bundles.php (Symfony) — pin the version (0.13.*) since this is still pre-1.0 and minor bumps can carry behaviour changes.*/5 * * * * expression, then invoke the runner command manually with verbose output and confirm it fires once, logs, and exits cleanly.Do not start by migrating all your existing cron entries. Get one task running under the runner and observed for a day first.
One task per class, thin and idempotent. Keep the scheduled class a wrapper that resolves dependencies via constructor injection and delegates to a real service. That makes the task runnable by hand in a test or a console command without the scheduler in the loop.
Daemon + loop, not per-task cron. The intended production shape is a single supervisor/systemd-managed process that the bundle keeps alive, evaluating "what is due now" on each tick. This is the same trade-off as Laravel's schedule:work versus a crontab entry calling schedule:run every minute: the daemon is cheaper and gives you in-process locking, but it can silently die — always pair it with a supervisor restart policy and a heartbeat check.
Scheduling metadata in one place. Prefer defining frequencies next to the task (attribute or service tag) but keep environment-specific overrides (disable a noisy task in dev, stagger times in prod) in config rather than hard-coded expressions.
Bridge pattern for Laravel shops. If you need this bundle's semantics in a Laravel codebase, don't try to install it. Reimplement: a queued/daemonised loop that reads a task registry (a container tag or a config array), resolves each task from the container, checks a cache lock (Cache::lock()), and dispatches. The locking and "is it due" logic is the only part worth copying.
Testing. Call the task class directly with mocked collaborators — the scheduling layer adds nothing to the assertion. For the runner itself, test with a frozen clock and a single due task.
0.13.0 changed time semantics — this is the one to internalise. The current time is now captured before the first task runs and reused for the run. Previously each due-check could read a fresh clock. Consequences:
0.13.0 also tightened code style. This is contributor-facing (fixers/CI) and has no runtime effect — but if you vendor-patch or maintain a fork, expect your local changes to fail their style job, and rebase onto the new formatting rather than merging around it.
Pre-1.0 versioning. Treat every 0.x minor as potentially breaking. Read the changelog even for "style" releases, because 0.13.0 bundled a behavioural change under a second bullet.
Overlap protection is on you. The daemon model means a slow task and the next tick can collide. Always wrap non-reentrant tasks in a lock with an expiry longer than the worst-case runtime, and log lock contention rather than swallowing it — a silently skipped task looks identical to a task that did nothing.
Timezone and DST. Cron expressions are evaluated against whatever timezone the runner process has. Set it explicitly; ambiguous local times around DST transitions can skip or double-fire a task.
Debugging a task that "didn't run": check in this order — is the daemon alive, is the task registered in the container/tag, is the expression valid, is the lock held, and only then look at the task body. Verbose runner output on a manual invocation resolves the first four immediately.
Extension points. The task registry (container tag / attribute loading) and the runner loop are the seams to customise — a custom task loader if you store schedules in a database, or a decorated runner if you need metrics. Keep customisations outside the vendor tree; the API is not stable at 0.x.
How can I help you explore Laravel packages today?