All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
2.3.0 - 2026-04-20
- Maintenance mode - stop running jobs during deployments
MaintenanceCheckerinterface - implement to define when maintenance is activeMaintenanceManager- orchestrates maintenance checking and shutdown requests
- Run tracking
RunRegistry- tracks active runsFileRunRegistry- PID-based stale detection (verifies process is alive), JSON metadata in run filesLockPoolRunRegistry- lock refresh support to prevent TTL expiry during long runs
ActiveRun- value object with run ID, PID and start timestampActivityStatus- value object returned byManagedScheduler->getStatus()
StatusCommand(scheduler:status) - reports maintenance state and active runs--fail-when-not-ready-for-shutdownoption for deploy scripts
SchedulergetStatus()- returnsActivityStatuswith active runs and maintenance state (BC break)
ManagedScheduler- accepts optional
MaintenanceManager- enables maintenance mode with two-phase shutdown (graceful wait, then force-kill after configurable grace period) - accepts optional
RunRegistry- enables active run tracking
- accepts optional
RunCommand- accepts optional
MaintenanceManager- registers signal handlers for graceful shutdown - handles
SIGTERMandSIGINTsignals (requirespcntlextension, double-signal forces exit) - returns exit code
2when run was stopped due to maintenance
- accepts optional
WorkerCommand- handles
SIGTERMandSIGINTsignals for graceful stop (requirespcntlextension, double-signal forces exit)
- handles
BasicJobExecutor- supports maintenance mode (shutdown after current job finishes)
ProcessJobExecutor- supports maintenance mode (shuts down running jobs gracefully + forcefully after timeout)
JobLock->extendTo(float $seconds)- sets lock expiration to given seconds from nowJobInfogetExecutionId()- unique identifier per job execution, for pairing before/after callbacksgetJobId()- replaces deprecatedgetId()
PlannedJobInfogetJobId()- replaces deprecatedgetId()
JobResulthasLockExpiredEarly()- indicates whether the lock expired before the job finished
JobResultStatemaintenance()- for jobs skipped or terminated due to maintenance
RunSummaryisMaintenanceActive()- indicates whether run was affected by maintenance
- Multi-server protection via minute lock - prevents the same job from running twice within the same minute when scheduler runs on multiple servers with a distributed lock store
PrefixingLockFactory- wrapsLockFactoryto prefix lock keys, preventing collisions between applications sharing the same lock store
JobInfo,PlannedJobInfogetId()- usegetJobId()instead, will be removed in v3.0
JobInfoisForcedRun()- useisManualRun()instead, will be removed in v3.0
JobLockisAcquiredByCurrentProcess()- always returns true inside a job, will be removed in v3.0refresh()- useextendTo()instead, will be removed in v3.0
ManagedScheduleraddLockedJobCallback()- useaddAfterJobCallback()and check$result->getState() === JobResultState::lock(); the after-job callback now fires for every job outcome includinglock, making this callback redundant. Will be removed in v3.0
JobExecutor(BC break)runJobs()accepts optional?ShutdownCheck $shutdownCheckparameterrunJobs()accepts optional?Closure $onJobEventparameter
RunSummary- constructor accepts optional
bool $maintenanceActiveparameter
- constructor accepts optional
afterJobcallback fires for every job outcome —done,fail,lockandmaintenance(previously onlydoneandfail)- Under
ProcessJobExecutor,beforeJobandafterJobcallbacks now fire in the parent process — subprocesses stream framework events back to the parent, which dispatches callbacks with the subprocess'sJobInfo. Callback pairing via$info->getExecutionId()works transparently across the process boundary, including when subprocesses are force-killed during maintenance or crash after starting Schedulerinterface (BC break)runJob()accepts optional?Closure $onJobStartedand?Closure $onJobFinishedparameters — framework-internal hooks used byProcessJobExecutor's subprocess protocol
Scheduler::runJob()respects maintenance mode regardless of$force— when maintenance is active the returnedJobSummaryhas stateJobResultState::maintenance()(previously$force=truebypassed maintenance and$force=falsereturnednull)JobInfo::toArray()/RunParameters::toArray()expose the flag asmanualRun(wasforcedRun)RunParametersconstructor parameter and getter renamed:forcedRun→manualRun,isForcedRun()→isManualRun()(RunParametersis@internal)
2.2.2 - 2026-02-12
- Use ANSI colors in console commands to support environments without true color support
2.2.1 - 2025-09-08
WorkerCommand- run subprocess may write to output right before finishProcessJobExecutor,WorkerCommand- parameters for phpdbg are properly escaped
2.2.0 - 2025-06-28
ProcessJobExecutor,WorkerCommandPhpExecutableFinderis used instead ofPHP_BINARYto locate PHP binary (fixes running in environment wherePHP_BINARYis not available)
2.1.3 - 2024-12-29
- Composer
- Allow PHP 8.4
2.1.2 - 2024-07-03
ProcessJobExecutor- start and end times passed from job process to main process are not shifted by offset of current timezone from UTC
ListCommand- handle impossible due times (like 31st of February)
2.1.1 - 2024-06-21
- callbacks are marked as either
@param-later-invoked-callableor@param-immediately-invoked-callable - Allow PHP 8.3
- Allow symfony/console, symfony/lock and symfony/process ^7.0.0
2.1.0 - 2024-05-26
ManagedScheduler- warns in case lock was released before job finished (via optional logger)
ExplainCommand- explains cron expression syntax
ListCommand- adds
--explainoption to explain whole expression
- adds
SymfonyConsoleJobJobInfogetTimeZone()returns timezone job should run inisForcedRun()returns whether job was run via $scheduler->runJob() or scheduler:run-job command, ignoring the cron expression
PlannedJobInfogetTimeZone()
ProcessJobExecutor- subprocess errors include exit code
- logs unexpected stdout instead of triggering E_USER_NOTICE (via optional logger)
- logs unexpected stderr instead of throwing an exception (via optional logger)
JobInfogetExtendedExpression()includes seconds only if seconds are usedgetExtendedExpression()includes timezone if timezone is used
ListCommand- more compact render
- sort jobs by keys instead of generated names
WorkerCommand- requires interactive CLI
--forceoption to bypass interactive CLI requirement
ListCommand- on invalid input option - writes error to output instead of throwing exception
JobInfogetStart()includes correct timezone (in main process, when usingProcessJobExecutor)
JobResultgetEnd()includes correct timezone (in main process, when usingProcessJobExecutor)
2.0.0 - 2024-01-26
This release most notably contains:
- planning jobs by seconds
- timezones support
- locked job, before run and after run events
- job results are shown in console immediately
- stderr handling in subprocesses - causes an exception
- stdout handling in subprocesses - causes a notice, instead of an exception
- simplified job manager
SchedulerrunPromise()- allowsscheduler:runandscheduler:workcommands to output job result as soon as it is finished
SimpleScheduleraddJob()accepts parameterrepeatAfterSecondsaddJob()accepts parametertimeZoneaddLazyJob()replacesCallbackJobManager
ManagedScheduleraddLockedJobCallback()- executes given callback when job is lockedaddBeforeRunCallback()- executes given callback before run startsaddAfterRunCallback()- executes given callback when run finishes
JobInfogetRepeatAfterSeconds()- returns the seconds part of expressiongetExtendedExpression()- returns cron expression including secondsgetRunSecond()- returns for which second within a minute was job scheduled
JobSchedule- contains info about the scheduled jobSimpleJobManageraddJob()accepts parameterrepeatAfterSecondsaddJob()accepts parametertimeZoneaddLazyJob()replacesCallbackJobManager
ListCommand- prints
repeatAfterSecondsparameter - prints job's
timeZoneparameter - adds
--timezone(-tz) option to show execution times in specified timezone
- prints
RunJobCommand- stdout is caught in a buffer and printed to output in a standardized manner (to above job result by default and
into key
stdoutin case--jsonoption is used)
- stdout is caught in a buffer and printed to output in a standardized manner (to above job result by default and
into key
JobManagergetPair()->getJobSchedule()- returns
JobScheduleinstead of an array
- returns
getPairs()->getJobSchedules()- returns array of
JobScheduleinstead of an array of arrays
- returns array of
SchedulergetJobs()->getJobSchedules()- returns array of
JobScheduleinstead of an array of arrays
- returns array of
RunSummarygetJobs()->getJobSummaries()
JobInfogetStart()->getTimeZone()- returns timezone specified by the job
JobResultgetEnd()->getTimeZone()- returns timezone specified by the job
JobResultStateskip()renamed tolock()
JobExecutorrunJobs()accepts list ofJobSchedulegrouped by seconds instead of list of idsrunJobs()returnsGenerator<int, JobSummary, void, RunSummary>instead ofRunSummary
ProcessJobExecutor- uses microseconds instead of milliseconds for start and end times
- better exception message in case subprocess call failed
- handles stdout and stderr separately
- stderr output does not make the job processing fail
- if stderr output is produced, an exception is still thrown (explaining unexpected stderr instead of a job failure)
- stdout output is captured and converted to notice (with strict error handler it will still cause an exception, but will not block execution of other jobs)
ManagedScheduler- acquired job locks are scoped just to their id - changing run frequency or job name will not make process loose the lock
CronExpressionis cloned after being added to job manager or scheduler for job immutability
JobManagergetExpressions()- replaced bygetJobSchedules()
CallbackJobManager- useSimpleJobManager->addLazyJob()instead
ProcessJobExecutor- use existing CronExpression instance instead of creating new one to support inheritance
ListCommand- Fix numeric job ids in case option
--nextis used
- Fix numeric job ids in case option