Repository navigation
Build a cron expression a field at a time - #14
Open
KalimeroMK wants to merge 2 commits into
Open
KalimeroMK wants to merge 2 commits into
KalimeroMK wants to merge 2 commits into
Conversation
A cron expression says when a task runs in five positional fields, which is
terse to write and slow to read. `CronBuilder` sets them one at a time, opened
by named constructors on `RecurringTask`:
RecurringTask::daily()->at('05:00')->run('report:daily');
RecurringTask::everyFiveMinutes()->between('08:00', '17:00')->run('cache:warm');
RecurringTask::weekly()->on('monday')->at('09:30')->run('report:weekly');
The chain ends with `run()` for a task, `toTrigger()` for a trigger alone, or
`getExpression()` for the expression as a string. Every step returns a new
builder, so a half-built expression can be finished in more than one way. A
task built this way is the one the same expression passed to
`RecurringTask::cron()` gives, identifier included, so an existing schedule can
be rewritten into the builder without a task being treated as new.
A time window is compiled into the expression rather than checked when the task
is about to run, so `schedule:list` and the next run date of the trigger account
for it. That is what limits `between()` to whole hours: a cron field cannot say
"from 08:30".
Each step guards the frequency the step before it set, so that no chain quietly
runs more or less often than both of its calls asked for:
- `between()` and `unlessBetween()` narrow the hours the expression already runs
at rather than replacing them, so `everyHours(6)->between('08:00', '17:00')`
keeps the one run of the four falling in the window instead of becoming an
hourly task. A window leaving nothing of the expression is refused.
- `at()` replaces both the hour and the minute, so it needs a chain that already
runs once a day; on `hourly()` or `everyFiveMinutes()` it would drop the
repetition, and is refused in favour of `atMinutes()` and `atHours()`.
- A day of the week and a day of the month cannot both be set, because cron runs
a task on a day matching either field once both are narrowed:
`weekly()->onLastDayOfMonth()` would run on every Monday as well as on the
last day of the month. An expression passed to the constructor can still say
that.
- An interval has to divide its unit evenly, so that the pair of runs either
side of an hour or a day is an interval apart like every other pair.
A timezone is checked where it is given rather than where the trigger is built,
so a typo reports at the step that introduced it and as this package's own
exception.
The lock file is not committed, so CI resolves the development dependencies afresh: phpstan/phpstan 2.3.4 bundles phpstan/phpdoc-parser 2.4.0 in its phar while rector/rector 2.8.2 bundles 2.3, both unprefixed. The parser from one copy then builds nodes for the class of the other, and every file fails with CallableTypeNode::__construct() being given a FullyQualifiedNameNode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A cron expression says when a task runs in five positional fields, which is terse to write and slow to read.
CronBuildersets them one at a time, opened by named constructors onRecurringTask:The chain starts with how often the task runs —
everyMinute(),everyMinutes(),everyFiveMinutes(),everyTenMinutes(),everyFifteenMinutes(),everyThirtyMinutes(),hourly(),everyHours(),daily(),weekly(),monthly(),quarterly(),yearly()— and narrows it withat(),atHours(),atMinutes(),on(),weekdays(),weekends(),onDays(),onLastDayOfMonth(),inMonths(),between(),unlessBetween()andtimezone(). It ends withrun()for a task,toTrigger()for a trigger alone, orgetExpression()for the expression as a string.Every step returns a new builder, so a half-built expression can be kept and finished in more than one way. A task built this way is the one the same expression passed to
RecurringTask::cron()gives, identifier included, so an existing schedule can be rewritten into the builder without a task being treated as new.A time window is compiled into the expression rather than checked when the task is about to run, so
schedule:listand the next run date of the trigger account for it. That is what limitsbetween()to whole hours: a cron field cannot say "from 08:30".Steps that guard each other
A builder whose steps overwrite one another silently is worse than the raw expression it replaces, so each step refuses to change how often the step before it runs:
between()andunlessBetween()narrow the hours the expression already runs at rather than replacing them.everyHours(6)->between('08:00', '17:00')keeps the one run of the four falling in the window instead of becoming an hourly task, and a window leaving nothing of the expression is refused rather than given hours nobody asked for.at()replaces both the hour and the minute, so it needs a chain that already runs once a day. Onhourly()oreveryFiveMinutes()it would drop the repetition without saying so, and is refused in favour ofatMinutes()andatHours().weekly()->onLastDayOfMonth()would run on every Monday as well as on the last day of the month. An expression passed to the constructor can still say that.everyMinutes()takes 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 or 30 andeveryHours()takes 1, 2, 3, 4, 6, 8 or 12 — so that the pair of runs either side of an hour or a day is an interval apart like every other pair.A timezone is checked where it is given rather than where the trigger is built, so a typo reports at the step that introduced it and as this package's own exception.
Notes
CronExpressionTrigger, so it does not touch the ground Draft: publish command/task object model #12 covers.weekly()runs on Mondays, the dayevery('1 week')runs on as well.timezone()still needwithId()to share a schedule. That is the behaviour documented under "Task identifiers" today; changing it would move the identifier of every cron task with an explicit timezone, so it belongs in its own pull request.