1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* CRON expression parser that can determine whether or not a CRON expression is |
5
|
|
|
* due to run, the next run date and previous run date of a CRON expression. |
6
|
|
|
* The determinations made by this class are accurate if checked run once per |
7
|
|
|
* minute (seconds are dropped from date time comparisons). |
8
|
|
|
* |
9
|
|
|
* Schedule parts must map to: |
10
|
|
|
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week |
11
|
|
|
* [1-7|MON-SUN], and an optional year. |
12
|
|
|
* |
13
|
|
|
* @author Michael Dowling <[email protected]> |
14
|
|
|
* @link http://en.wikipedia.org/wiki/Cron |
15
|
|
|
*/ |
16
|
|
|
class CronExpression |
17
|
|
|
{ |
18
|
|
|
const MINUTE = 0; |
19
|
|
|
const HOUR = 1; |
20
|
|
|
const DAY = 2; |
21
|
|
|
const MONTH = 3; |
22
|
|
|
const WEEKDAY = 4; |
23
|
|
|
const YEAR = 5; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array CRON expression parts |
27
|
|
|
*/ |
28
|
|
|
private $cronParts; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var CronExpression_FieldFactory CRON field factory |
32
|
|
|
*/ |
33
|
|
|
private $fieldFactory; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var array Order in which to test of cron parts |
37
|
|
|
*/ |
38
|
|
|
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Factory method to create a new CronExpression. |
42
|
|
|
* |
43
|
|
|
* @param string $expression The CRON expression to create. There are |
44
|
|
|
* several special predefined values which can be used to substitute the |
45
|
|
|
* CRON expression: |
46
|
|
|
* |
47
|
|
|
* @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 * |
48
|
|
|
* @monthly - Run once a month, midnight, first of month - 0 0 1 * * |
49
|
|
|
* @weekly - Run once a week, midnight on Sun - 0 0 * * 0 |
50
|
|
|
* @daily - Run once a day, midnight - 0 0 * * * |
51
|
|
|
* @hourly - Run once an hour, first minute - 0 * * * * |
52
|
|
|
* |
53
|
|
|
*@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use |
54
|
|
|
* |
55
|
|
|
* @return CronExpression |
56
|
|
|
*/ |
57
|
|
|
public static function factory($expression, CronExpression_FieldFactory $fieldFactory = null) |
58
|
|
|
{ |
59
|
|
|
$mappings = array( |
60
|
|
|
'@yearly' => '0 0 1 1 *', |
61
|
|
|
'@annually' => '0 0 1 1 *', |
62
|
|
|
'@monthly' => '0 0 1 * *', |
63
|
|
|
'@weekly' => '0 0 * * 0', |
64
|
|
|
'@daily' => '0 0 * * *', |
65
|
|
|
'@hourly' => '0 * * * *' |
66
|
|
|
); |
67
|
|
|
|
68
|
|
|
if (isset($mappings[$expression])) { |
69
|
|
|
$expression = $mappings[$expression]; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory()); |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Parse a CRON expression |
77
|
|
|
* |
78
|
|
|
* @param string $expression CRON expression (e.g. '8 * * * *') |
79
|
|
|
* @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields |
80
|
|
|
*/ |
81
|
|
|
public function __construct($expression, CronExpression_FieldFactory $fieldFactory) |
82
|
|
|
{ |
83
|
|
|
$this->fieldFactory = $fieldFactory; |
84
|
|
|
$this->setExpression($expression); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Set or change the CRON expression |
89
|
|
|
* |
90
|
|
|
* @param string $value CRON expression (e.g. 8 * * * *) |
91
|
|
|
* |
92
|
|
|
* @return CronExpression |
93
|
|
|
* @throws InvalidArgumentException if not a valid CRON expression |
94
|
|
|
*/ |
95
|
|
|
public function setExpression($value) |
96
|
|
|
{ |
97
|
|
|
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); |
|
|
|
|
98
|
|
|
if (count($this->cronParts) < 5) { |
|
|
|
|
99
|
|
|
throw new InvalidArgumentException( |
100
|
|
|
$value . ' is not a valid CRON expression' |
101
|
|
|
); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
foreach ($this->cronParts as $position => $part) { |
105
|
|
|
$this->setPart($position, $part); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
return $this; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Set part of the CRON expression |
113
|
|
|
* |
114
|
|
|
* @param int $position The position of the CRON expression to set |
115
|
|
|
* @param string $value The value to set |
116
|
|
|
* |
117
|
|
|
* @return CronExpression |
118
|
|
|
* @throws InvalidArgumentException if the value is not valid for the part |
119
|
|
|
*/ |
120
|
|
|
public function setPart($position, $value) |
121
|
|
|
{ |
122
|
|
|
if (!$this->fieldFactory->getField($position)->validate($value)) { |
123
|
|
|
throw new InvalidArgumentException( |
124
|
|
|
'Invalid CRON field value ' . $value . ' as position ' . $position |
125
|
|
|
); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
$this->cronParts[$position] = $value; |
129
|
|
|
|
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Get a next run date relative to the current date or a specific date |
135
|
|
|
* |
136
|
|
|
* @param string|DateTime $currentTime (optional) Relative calculation date |
137
|
|
|
* @param int $nth (optional) Number of matches to skip before returning a |
138
|
|
|
* matching next run date. 0, the default, will return the current |
139
|
|
|
* date and time if the next run date falls on the current date and |
140
|
|
|
* time. Setting this value to 1 will skip the first match and go to |
141
|
|
|
* the second match. Setting this value to 2 will skip the first 2 |
142
|
|
|
* matches and so on. |
143
|
|
|
* @param bool $allowCurrentDate (optional) Set to TRUE to return the |
144
|
|
|
* current date if it matches the cron expression |
145
|
|
|
* |
146
|
|
|
* @return DateTime |
147
|
|
|
* @throws RuntimeException on too many iterations |
148
|
|
|
*/ |
149
|
|
|
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) |
150
|
|
|
{ |
151
|
|
|
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Get a previous run date relative to the current date or a specific date |
156
|
|
|
* |
157
|
|
|
* @param string|DateTime $currentTime (optional) Relative calculation date |
158
|
|
|
* @param int $nth (optional) Number of matches to skip before returning |
159
|
|
|
* @param bool $allowCurrentDate (optional) Set to TRUE to return the |
160
|
|
|
* current date if it matches the cron expression |
161
|
|
|
* |
162
|
|
|
* @return DateTime |
163
|
|
|
* @throws RuntimeException on too many iterations |
164
|
|
|
* @see CronExpression::getNextRunDate |
165
|
|
|
*/ |
166
|
|
|
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) |
167
|
|
|
{ |
168
|
|
|
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Get multiple run dates starting at the current date or a specific date |
173
|
|
|
* |
174
|
|
|
* @param int $total Set the total number of dates to calculate |
175
|
|
|
* @param string|DateTime $currentTime (optional) Relative calculation date |
176
|
|
|
* @param bool $invert (optional) Set to TRUE to retrieve previous dates |
177
|
|
|
* @param bool $allowCurrentDate (optional) Set to TRUE to return the |
178
|
|
|
* current date if it matches the cron expression |
179
|
|
|
* |
180
|
|
|
* @return array Returns an array of run dates |
181
|
|
|
*/ |
182
|
|
|
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false) |
183
|
|
|
{ |
184
|
|
|
$matches = array(); |
185
|
|
|
for ($i = 0; $i < max(0, $total); $i++) { |
186
|
|
|
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
return $matches; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Get all or part of the CRON expression |
194
|
|
|
* |
195
|
|
|
* @param string $part (optional) Specify the part to retrieve or NULL to |
196
|
|
|
* get the full cron schedule string. |
197
|
|
|
* |
198
|
|
|
* @return string|null Returns the CRON expression, a part of the |
199
|
|
|
* CRON expression, or NULL if the part was specified but not found |
200
|
|
|
*/ |
201
|
|
|
public function getExpression($part = null) |
202
|
|
|
{ |
203
|
|
|
if (null === $part) { |
204
|
|
|
return implode(' ', $this->cronParts); |
205
|
|
|
} elseif (array_key_exists($part, $this->cronParts)) { |
206
|
|
|
return $this->cronParts[$part]; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
return null; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Helper method to output the full expression. |
214
|
|
|
* |
215
|
|
|
* @return string Full CRON expression |
216
|
|
|
*/ |
217
|
|
|
public function __toString() |
218
|
|
|
{ |
219
|
|
|
return $this->getExpression(); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Determine if the cron is due to run based on the current date or a |
224
|
|
|
* specific date. This method assumes that the current number of |
225
|
|
|
* seconds are irrelevant, and should be called once per minute. |
226
|
|
|
* |
227
|
|
|
* @param string|DateTime $currentTime (optional) Relative calculation date |
228
|
|
|
* |
229
|
|
|
* @return bool Returns TRUE if the cron is due to run or FALSE if not |
230
|
|
|
*/ |
231
|
|
|
public function isDue($currentTime = 'now') |
232
|
|
|
{ |
233
|
|
|
if ('now' === $currentTime) { |
234
|
|
|
$currentDate = date('Y-m-d H:i'); |
235
|
|
|
$currentTime = strtotime($currentDate); |
236
|
|
|
} elseif ($currentTime instanceof DateTime) { |
237
|
|
|
$currentDate = $currentTime->format('Y-m-d H:i'); |
238
|
|
|
$currentTime = strtotime($currentDate); |
239
|
|
|
} else { |
240
|
|
|
$currentTime = new DateTime($currentTime); |
241
|
|
|
$currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); |
|
|
|
|
242
|
|
|
$currentDate = $currentTime->format('Y-m-d H:i'); |
243
|
|
|
$currentTime = (int)($currentTime->getTimestamp()); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Get the next or previous run date of the expression relative to a date |
251
|
|
|
* |
252
|
|
|
* @param string|DateTime $currentTime (optional) Relative calculation date |
253
|
|
|
* @param int $nth (optional) Number of matches to skip before returning |
254
|
|
|
* @param bool $invert (optional) Set to TRUE to go backwards in time |
255
|
|
|
* @param bool $allowCurrentDate (optional) Set to TRUE to return the |
256
|
|
|
* current date if it matches the cron expression |
257
|
|
|
* |
258
|
|
|
* @return DateTime |
259
|
|
|
* @throws RuntimeException on too many iterations |
260
|
|
|
*/ |
261
|
|
|
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false) |
262
|
|
|
{ |
263
|
|
|
if ($currentTime instanceof DateTime) { |
264
|
|
|
$currentDate = $currentTime; |
265
|
|
|
} else { |
266
|
|
|
$currentDate = new DateTime($currentTime ? $currentTime : 'now'); |
267
|
|
|
$currentDate->setTimezone(new DateTimeZone(date_default_timezone_get())); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); |
|
|
|
|
271
|
|
|
$nextRun = clone $currentDate; |
272
|
|
|
$nth = (int) $nth; |
273
|
|
|
|
274
|
|
|
// Set a hard limit to bail on an impossible date |
275
|
|
|
for ($i = 0; $i < 1000; $i++) { |
276
|
|
|
|
277
|
|
|
foreach (self::$order as $position) { |
278
|
|
|
$part = $this->getExpression($position); |
279
|
|
|
if (null === $part) { |
280
|
|
|
continue; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
$satisfied = false; |
284
|
|
|
// Get the field object used to validate this part |
285
|
|
|
$field = $this->fieldFactory->getField($position); |
286
|
|
|
// Check if this is singular or a list |
287
|
|
|
if (strpos($part, ',') === false) { |
288
|
|
|
$satisfied = $field->isSatisfiedBy($nextRun, $part); |
289
|
|
|
} else { |
290
|
|
|
foreach (array_map('trim', explode(',', $part)) as $listPart) { |
291
|
|
|
if ($field->isSatisfiedBy($nextRun, $listPart)) { |
292
|
|
|
$satisfied = true; |
293
|
|
|
break; |
294
|
|
|
} |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
// If the field is not satisfied, then start over |
299
|
|
|
if (!$satisfied) { |
300
|
|
|
$field->increment($nextRun, $invert); |
301
|
|
|
continue 2; |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
// Skip this match if needed |
306
|
|
|
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { |
307
|
|
|
$this->fieldFactory->getField(0)->increment($nextRun, $invert); |
308
|
|
|
continue; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
return $nextRun; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
// @codeCoverageIgnoreStart |
315
|
|
|
throw new RuntimeException('Impossible CRON expression'); |
316
|
|
|
// @codeCoverageIgnoreEnd |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.