Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.65% covered (success)
99.65%
282 / 283
98.11% covered (success)
98.11%
52 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cron
99.65% covered (success)
99.65%
282 / 283
98.11% covered (success)
98.11%
52 / 53
110
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setGracePeriod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getGracePeriod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasGracePeriod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSeconds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSeconds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMinutes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHours
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDaysOfTheMonth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMonths
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDaysOfTheWeek
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 schedule
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 getSchedule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSchedule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateSchedule
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
14
 everySecond
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 every5Seconds
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 every10Seconds
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 every15Seconds
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 every20Seconds
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 every30Seconds
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 seconds
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 everyMinute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 every5Minutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 every10Minutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 every15Minutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 every20Minutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 every30Minutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 minutes
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 hours
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 hourly
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 daily
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dailyAt
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 weekly
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 monthly
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 quarterly
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 yearly
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 weekdays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 weekends
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 sundays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 mondays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 tuesdays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 wednesdays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 thursdays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 fridays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 saturdays
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 between
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 evaluate
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
12
 fieldPasses
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evaluateExpression
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Queue\Process;
16
17/**
18 * Cron class
19 *
20 * @category   Pop
21 * @package    Pop\Queue
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    3.0.0
26 */
27class Cron
28{
29
30    /**
31     * Schedule string
32     * @var ?string
33     */
34    protected ?string $schedule = null;
35
36    /**
37     * Seconds
38     *  - Not a standard cron unit of time. The smallest time interval supported by cron is 1 minute.
39     *    This is to support time intervals less than minute and down to 1 second.
40     * @var array
41     */
42    protected array $seconds = [];
43
44    /**
45     * Minutes
46     * @var array
47     */
48    protected array $minutes = [];
49
50    /**
51     * Hours
52     * @var array
53     */
54    protected array $hours = [];
55
56    /**
57     * Days of the month
58     * @var array
59     */
60    protected array $daysOfTheMonth = [];
61
62    /**
63     * Months
64     * @var array
65     */
66    protected array $months = [];
67
68    /**
69     * Days of the week
70     * @var array
71     */
72    protected array $daysOfTheWeek = [];
73
74    /**
75     * Grace period, in seconds, allowed between the scheduled time and the
76     * evaluation of it. Defaults to -1, i.e. the seconds value is disregarded
77     * and the schedule is due for the whole of its matching minute.
78     * @var int
79     */
80    protected int $gracePeriod = -1;
81
82    /**
83     * Constructor
84     *
85     * Instantiate the cron  object
86     *
87     * @param  ?string $schedule
88     * @param  int     $gracePeriod
89     */
90    public function __construct(?string $schedule = null, int $gracePeriod = -1)
91    {
92        if ($schedule !== null) {
93            $this->schedule($schedule);
94        }
95        $this->setGracePeriod($gracePeriod);
96    }
97
98    /**
99     * Factory
100     *
101     * @param  ?string $schedule
102     * @return Cron
103     */
104    public static function create(?string $schedule = null): Cron
105    {
106        return new self($schedule);
107    }
108
109    /**
110     * Set grace period
111     *
112     * @param  int $gracePeriod
113     * @return Cron
114     */
115    public function setGracePeriod(int $gracePeriod): Cron
116    {
117        $this->gracePeriod = $gracePeriod;
118        return $this;
119    }
120
121    /**
122     * Get grace period
123     *
124     * @return int
125     */
126    public function getGracePeriod(): int
127    {
128        return $this->gracePeriod;
129    }
130
131    /**
132     * Has grace period
133     *
134     * Only a grace period of exactly 0 - strict evaluation to the 00 second -
135     * grants no grace. A negative value is the loosest setting there is, not
136     * the absence of one.
137     *
138     * @return bool
139     */
140    public function hasGracePeriod(): bool
141    {
142        return ($this->gracePeriod !== 0);
143    }
144
145    /**
146     * Has seconds
147     *
148     * @return bool
149     */
150    public function hasSeconds(): bool
151    {
152        return !empty($this->seconds);
153    }
154
155    /**
156     * Get seconds
157     *
158     * @return array
159     */
160    public function getSeconds(): array
161    {
162        return $this->seconds;
163    }
164
165    /**
166     * Get minutes
167     *
168     * @return array
169     */
170    public function getMinutes(): array
171    {
172        return $this->minutes;
173    }
174
175    /**
176     * Get hours
177     *
178     * @return array
179     */
180    public function getHours(): array
181    {
182        return $this->hours;
183    }
184
185    /**
186     * Get days of the month
187     *
188     * @return array
189     */
190    public function getDaysOfTheMonth(): array
191    {
192        return $this->daysOfTheMonth;
193    }
194
195    /**
196     * Get months
197     *
198     * @return array
199     */
200    public function getMonths(): array
201    {
202        return $this->months;
203    }
204
205    /**
206     * Get days of the week
207     *
208     * @return array
209     */
210    public function getDaysOfTheWeek(): array
211    {
212        return $this->daysOfTheWeek;
213    }
214
215    /**
216     * Set cron schedule
217     *
218     *   min  hour  dom  month  dow
219     *    *    *     *     *     *
220     *
221     *      - OR non-standard -
222     *
223     *    sec  min  hour  dom  month  dow
224     *     *    *    *     *     *     *
225     *
226     * @param  string $schedule
227     * @return Cron
228     */
229    public function schedule(string $schedule): Cron
230    {
231        $schedule = preg_replace('!\s+!', ' ', trim($schedule));
232
233        if (substr_count($schedule, ' ') >= 4) {
234            $this->schedule = $schedule;
235
236            if (substr_count($schedule, ' ') == 5) {
237                list($sec, $min, $hour, $dom, $month, $dow) = explode(' ', $this->schedule);
238                $this->seconds = [$sec];
239            } else {
240                list($min, $hour, $dom, $month, $dow) = explode(' ', $this->schedule);
241            }
242
243            $this->minutes        = [$min];
244            $this->hours          = [$hour];
245            $this->daysOfTheMonth = [$dom];
246            $this->months         = [$month];
247            $this->daysOfTheWeek  = [$dow];
248        }
249
250        return $this;
251    }
252
253    /**
254     * Get schedule string
255     *
256     * @return ?string
257     */
258    public function getSchedule(): ?string
259    {
260        return $this->schedule;
261    }
262
263    /**
264     * Has schedule string
265     *
266     * @return bool
267     */
268    public function hasSchedule(): bool
269    {
270        return ($this->schedule !== null);
271    }
272
273    /**
274     * Update cron schedule
275     * @return Cron
276     */
277    public function updateSchedule(): Cron
278    {
279        $schedule = [];
280
281        // Minutes
282        if (count($this->seconds) > 1) {
283            $schedule[] = implode(',', $this->seconds);
284        } else if (isset($this->seconds[0])) {
285            $schedule[] = $this->seconds[0];
286        }
287
288        // Minutes
289        if (count($this->minutes) > 1) {
290            $schedule[] = implode(',', $this->minutes);
291        } else if (isset($this->minutes[0])) {
292            $schedule[] = $this->minutes[0];
293        }
294
295        // Hours
296        if (count($this->hours) > 1) {
297            $schedule[] = implode(',', $this->hours);
298        } else if (isset($this->hours[0])) {
299            $schedule[] = $this->hours[0];
300        }
301
302        // DOM
303        if (count($this->daysOfTheMonth) > 1) {
304            $schedule[] = implode(',', $this->daysOfTheMonth);
305        } else if (isset($this->daysOfTheMonth[0])) {
306            $schedule[] = $this->daysOfTheMonth[0];
307        }
308
309        // Months
310        if (count($this->months) > 1) {
311            $schedule[] = implode(',', $this->months);
312        } else if (isset($this->months[0])) {
313            $schedule[] = $this->months[0];
314        }
315
316        // DOW
317        if (count($this->daysOfTheWeek) > 1) {
318            $schedule[] = implode(',', $this->daysOfTheWeek);
319        } else if (isset($this->daysOfTheWeek[0])) {
320            $schedule[] = $this->daysOfTheWeek[0];
321        }
322
323        if (empty($schedule)) {
324            throw new Exception('Error: The cron schedule has not been set.');
325        }
326
327        $this->schedule = implode(' ', $schedule);
328        return $this;
329    }
330
331    /**
332     * Set job schedule to every second
333     *
334     * @return Cron
335     */
336    public function everySecond(): Cron
337    {
338        $this->seconds        = ['*'];
339        $this->minutes        = ['*'];
340        $this->hours          = ['*'];
341        $this->daysOfTheMonth = ['*'];
342        $this->months         = ['*'];
343        $this->daysOfTheWeek  = ['*'];
344
345        return $this->updateSchedule();
346    }
347
348    /**
349     * Set job schedule to every 5 seconds
350     *
351     * @return Cron
352     */
353    public function every5Seconds(): Cron
354    {
355        $this->seconds        = ['*/5'];
356        $this->minutes        = ['*'];
357        $this->hours          = ['*'];
358        $this->daysOfTheMonth = ['*'];
359        $this->months         = ['*'];
360        $this->daysOfTheWeek  = ['*'];
361
362        return $this->updateSchedule();
363    }
364
365    /**
366     * Set job schedule to every 10 seconds
367     *
368     * @return Cron
369     */
370    public function every10Seconds(): Cron
371    {
372        $this->seconds        = ['*/10'];
373        $this->minutes        = ['*'];
374        $this->hours          = ['*'];
375        $this->daysOfTheMonth = ['*'];
376        $this->months         = ['*'];
377        $this->daysOfTheWeek  = ['*'];
378
379        return $this->updateSchedule();
380    }
381
382    /**
383     * Set job schedule to every 15 seconds
384     *
385     * @return Cron
386     */
387    public function every15Seconds(): Cron
388    {
389        $this->seconds        = ['*/15'];
390        $this->minutes        = ['*'];
391        $this->hours          = ['*'];
392        $this->daysOfTheMonth = ['*'];
393        $this->months         = ['*'];
394        $this->daysOfTheWeek  = ['*'];
395
396        return $this->updateSchedule();
397    }
398
399    /**
400     * Set job schedule to every 20 seconds
401     *
402     * @return Cron
403     */
404    public function every20Seconds(): Cron
405    {
406        $this->seconds        = ['*/20'];
407        $this->minutes        = ['*'];
408        $this->hours          = ['*'];
409        $this->daysOfTheMonth = ['*'];
410        $this->months         = ['*'];
411        $this->daysOfTheWeek  = ['*'];
412
413        return $this->updateSchedule();
414    }
415
416    /**
417     * Set job schedule to every 30 seconds
418     *
419     * @return Cron
420     */
421    public function every30Seconds(): Cron
422    {
423        $this->seconds        = ['*/30'];
424        $this->minutes        = ['*'];
425        $this->hours          = ['*'];
426        $this->daysOfTheMonth = ['*'];
427        $this->months         = ['*'];
428        $this->daysOfTheWeek  = ['*'];
429
430        return $this->updateSchedule();
431    }
432
433    /**
434     * Set job schedule to by specific seconds
435     *
436     * @param  mixed $seconds
437     * @return Cron
438     */
439    public function seconds(mixed $seconds): Cron
440    {
441        if (is_string($seconds) && (str_contains($seconds, ','))) {
442            $seconds = explode(',' , $seconds);
443        } else if (is_numeric($seconds)) {
444            $seconds = [(int)$seconds];
445        } else {
446            $seconds = [$seconds];
447        }
448
449        $this->seconds        = array_map('trim', $seconds);
450        $this->minutes        = ['*'];
451        $this->hours          = ['*'];
452        $this->daysOfTheMonth = ['*'];
453        $this->months         = ['*'];
454        $this->daysOfTheWeek  = ['*'];
455
456        return $this->updateSchedule();
457    }
458
459    /**
460     * Set job schedule to every minute
461     *
462     * @return Cron
463     */
464    public function everyMinute(): Cron
465    {
466        $this->minutes        = ['*'];
467        $this->hours          = ['*'];
468        $this->daysOfTheMonth = ['*'];
469        $this->months         = ['*'];
470        $this->daysOfTheWeek  = ['*'];
471
472        return $this->updateSchedule();
473    }
474
475    /**
476     * Set job schedule to every 5 minutes
477     *
478     * @return Cron
479     */
480    public function every5Minutes(): Cron
481    {
482        $this->minutes        = ['*/5'];
483        $this->hours          = ['*'];
484        $this->daysOfTheMonth = ['*'];
485        $this->months         = ['*'];
486        $this->daysOfTheWeek  = ['*'];
487
488        return $this->updateSchedule();
489    }
490
491    /**
492     * Set job schedule to every 10 minutes
493     *
494     * @return Cron
495     */
496    public function every10Minutes(): Cron
497    {
498        $this->minutes        = ['*/10'];
499        $this->hours          = ['*'];
500        $this->daysOfTheMonth = ['*'];
501        $this->months         = ['*'];
502        $this->daysOfTheWeek  = ['*'];
503
504        return $this->updateSchedule();
505    }
506
507    /**
508     * Set job schedule to every 15 minutes
509     *
510     * @return Cron
511     */
512    public function every15Minutes(): Cron
513    {
514        $this->minutes        = ['*/15'];
515        $this->hours          = ['*'];
516        $this->daysOfTheMonth = ['*'];
517        $this->months         = ['*'];
518        $this->daysOfTheWeek  = ['*'];
519
520        return $this->updateSchedule();
521    }
522
523    /**
524     * Set job schedule to every 20 minutes
525     *
526     * @return Cron
527     */
528    public function every20Minutes(): Cron
529    {
530        $this->minutes        = ['*/20'];
531        $this->hours          = ['*'];
532        $this->daysOfTheMonth = ['*'];
533        $this->months         = ['*'];
534        $this->daysOfTheWeek  = ['*'];
535
536        return $this->updateSchedule();
537    }
538
539    /**
540     * Set job schedule to every 30 minutes
541     *
542     * @return Cron
543     */
544    public function every30Minutes(): Cron
545    {
546        $this->minutes        = ['*/30'];
547        $this->hours          = ['*'];
548        $this->daysOfTheMonth = ['*'];
549        $this->months         = ['*'];
550        $this->daysOfTheWeek  = ['*'];
551
552        return $this->updateSchedule();
553    }
554
555    /**
556     * Set job schedule to by specific minutes
557     *
558     * @param  mixed $minutes
559     * @return Cron
560     */
561    public function minutes(mixed $minutes): Cron
562    {
563        if (is_string($minutes) && (str_contains($minutes, ','))) {
564            $minutes = explode(',' , $minutes);
565        } else if (is_numeric($minutes)) {
566            $minutes = [(int)$minutes];
567        } else {
568            $minutes = [$minutes];
569        }
570
571        $this->minutes        = array_map('trim', $minutes);
572        $this->hours          = ['*'];
573        $this->daysOfTheMonth = ['*'];
574        $this->months         = ['*'];
575        $this->daysOfTheWeek  = ['*'];
576
577        return $this->updateSchedule();
578    }
579
580    /**
581     * Set job schedule to by specific hours
582     *
583     * @param  mixed $hours
584     * @param  mixed $minutes
585     * @return Cron
586     */
587    public function hours(mixed $hours, mixed $minutes = null): Cron
588    {
589        if ($minutes !== null) {
590            $this->minutes($minutes);
591        } else {
592            $this->minutes = [0];
593        }
594
595        if (is_string($hours) && (str_contains($hours, ','))) {
596            $hours = explode(',' , $hours);
597        } else if (is_numeric($hours)) {
598            $hours = [(int)$hours];
599        } else {
600            $hours = [$hours];
601        }
602
603        $this->hours          = array_map('trim', $hours);
604        $this->daysOfTheMonth = ['*'];
605        $this->months         = ['*'];
606        $this->daysOfTheWeek  = ['*'];
607
608        return $this->updateSchedule();
609    }
610
611    /**
612     * Set job schedule to hourly
613     *
614     * @param  mixed $minutes
615     * @return Cron
616     */
617    public function hourly(mixed $minutes = null): Cron
618    {
619        if ($minutes !== null) {
620            $this->minutes($minutes);
621        } else {
622            $this->minutes = [0];
623        }
624
625        $this->hours          = ['*'];
626        $this->daysOfTheMonth = ['*'];
627        $this->months         = ['*'];
628        $this->daysOfTheWeek  = ['*'];
629
630        return $this->updateSchedule();
631    }
632
633    /**
634     * Set job schedule to daily (alias to hours)
635     *
636     * @param  mixed $hours
637     * @param  mixed $minutes
638     * @return Cron
639     */
640    public function daily(mixed $hours, mixed $minutes = null): Cron
641    {
642        return $this->hours($hours, $minutes);
643    }
644
645    /**
646     * Set job schedule to daily at specific time, i.e. 14:30
647     *
648     * @param  string $time
649     * @return Cron
650     */
651    public function dailyAt(string $time): Cron
652    {
653        list($hour, $minute) = explode(':', $time);
654        $this->daily($hour, $minute);
655        return $this;
656    }
657
658    /**
659     * Set job schedule to weekly
660     *
661     * @param  mixed $day
662     * @param  mixed $hours
663     * @param  mixed $minutes
664     * @return Cron
665     */
666    public function weekly(mixed $day, mixed $hours = null, mixed $minutes = null): Cron
667    {
668        if ($minutes !== null) {
669            $this->minutes($minutes);
670        } else {
671            $this->minutes = [0];
672        }
673
674        if ($hours !== null) {
675            $this->hours = [$hours];
676        } else {
677            $this->hours = [0];
678        }
679
680        $this->daysOfTheMonth = ['*'];
681        $this->months         = ['*'];
682        $this->daysOfTheWeek  = [$day];
683
684        return $this->updateSchedule();
685    }
686
687    /**
688     * Set job schedule to monthly
689     *
690     * @param  mixed $day
691     * @param  mixed $hours
692     * @param  mixed $minutes
693     * @return Cron
694     */
695    public function monthly(mixed $day, mixed $hours = null, mixed $minutes = null): Cron
696    {
697        if ($minutes !== null) {
698            $this->minutes($minutes);
699        } else {
700            $this->minutes = [0];
701        }
702
703        if ($hours !== null) {
704            $this->hours = [$hours];
705        } else {
706            $this->hours = [0];
707        }
708
709        $this->daysOfTheMonth = [$day];
710        $this->months         = ['*'];
711        $this->daysOfTheWeek  = ['*'];
712
713        return $this->updateSchedule();
714    }
715
716    /**
717     * Set job schedule to quarterly
718     *
719     * @param  mixed $hours
720     * @param  mixed $minutes
721     * @return Cron
722     */
723    public function quarterly(mixed $hours = null, mixed $minutes = null): Cron
724    {
725        if ($minutes !== null) {
726            $this->minutes($minutes);
727        } else {
728            $this->minutes = [0];
729        }
730
731        if ($hours !== null) {
732            $this->hours = [$hours];
733        } else {
734            $this->hours = [0];
735        }
736
737        $this->daysOfTheMonth = ['1'];
738        $this->months         = [1,4,7,10];
739        $this->daysOfTheWeek  = ['*'];
740
741        return $this->updateSchedule();
742    }
743
744    /**
745     * Set job schedule to yearly
746     *
747     * @param  bool $endOfYear
748     * @param  mixed $hours
749     * @param  mixed $minutes
750     * @return Cron
751     */
752    public function yearly(bool $endOfYear = false, mixed $hours = null, mixed $minutes = null): Cron
753    {
754        if ($minutes !== null) {
755            $this->minutes($minutes);
756        } else {
757            $this->minutes = [0];
758        }
759
760        if ($hours !== null) {
761            $this->hours = [$hours];
762        } else {
763            $this->hours = [0];
764        }
765
766        $this->daysOfTheMonth = ($endOfYear) ? ['31'] : ['1'];
767        $this->months         = ($endOfYear) ? ['12'] : ['1'];
768        $this->daysOfTheWeek  = ['*'];
769
770        return $this->updateSchedule();
771    }
772
773    /**
774     * Set job schedule to weekdays
775     *
776     * @return Cron
777     */
778    public function weekdays(): Cron
779    {
780        $this->daysOfTheWeek = ['1', '2', '3', '4', '5'];
781        return $this->updateSchedule();
782    }
783
784    /**
785     * Set job schedule to weekends
786     *
787     * @return Cron
788     */
789    public function weekends(): Cron
790    {
791        $this->daysOfTheWeek = ['0', '6'];
792        return $this->updateSchedule();
793    }
794
795    /**
796     * Set job schedule to Sundays
797     *
798     * @return Cron
799     */
800    public function sundays(): Cron
801    {
802        $this->daysOfTheWeek = ['0'];
803        return $this->updateSchedule();
804    }
805
806    /**
807     * Set job schedule to Mondays
808     *
809     * @return Cron
810     */
811    public function mondays(): Cron
812    {
813        $this->daysOfTheWeek = ['1'];
814        return $this->updateSchedule();
815    }
816
817    /**
818     * Set job schedule to Tuesdays
819     *
820     * @return Cron
821     */
822    public function tuesdays(): Cron
823    {
824        $this->daysOfTheWeek = ['2'];
825        return $this->updateSchedule();
826    }
827
828    /**
829     * Set job schedule to Wednesdays
830     *
831     * @return Cron
832     */
833    public function wednesdays(): Cron
834    {
835        $this->daysOfTheWeek = ['3'];
836        return $this->updateSchedule();
837    }
838
839    /**
840     * Set job schedule to Thursdays
841     *
842     * @return Cron
843     */
844    public function thursdays(): Cron
845    {
846        $this->daysOfTheWeek = ['4'];
847        return $this->updateSchedule();
848    }
849
850    /**
851     * Set job schedule to Fridays
852     *
853     * @return Cron
854     */
855    public function fridays(): Cron
856    {
857        $this->daysOfTheWeek = ['5'];
858        return $this->updateSchedule();
859    }
860
861    /**
862     * Set job schedule to Saturdays
863     *
864     * @return Cron
865     */
866    public function saturdays(): Cron
867    {
868        $this->daysOfTheWeek = ['6'];
869        return $this->updateSchedule();
870    }
871
872    /**
873     * Set job schedule to between two hours
874     *
875     * @param  int $start
876     * @param  int $end
877     * @return Cron
878     */
879    public function between(int $start, int $end): Cron
880    {
881        $this->hours = [$start . '-' . $end];
882        return $this->updateSchedule();
883    }
884
885    /**
886     * Render the cron schedule string
887     *
888     * @return string
889     */
890    public function render(): string
891    {
892        if (empty($this->schedule)) {
893            $this->updateSchedule();
894        }
895        return $this->schedule;
896    }
897
898    /**
899     * Evaluate the set cron schedule value against a time value
900     *
901     * The grace period governs how late an evaluation may be and still count
902     * as due. It applies only to minute-granularity schedules; a schedule with
903     * a seconds field is always evaluated exactly.
904     *
905     * $gracePeriod = -1;    disregards the seconds value - due for the whole
906     *                       of the matching minute (the default)
907     * $gracePeriod = 1-59;  due within that many seconds of the scheduled time
908     * $gracePeriod = 0;     strict evaluation to the 00 second
909     *
910     * Note that a missed window is not made up later - there is no catch-up.
911     *
912     * @param  mixed $time
913     * @param  ?int  $gracePeriod
914     * @throws Exception
915     * @return bool
916     */
917    public function evaluate(mixed $time = null, ?int $gracePeriod = null): bool
918    {
919        if ($time === null) {
920            $time = time();
921        } else if (is_string($time)) {
922            $time = strtotime($time);
923            if ($time === false) {
924                throw new Exception('Error: That time value is not valid.');
925            }
926        }
927
928        if ($gracePeriod !== null) {
929            $this->setGracePeriod($gracePeriod);
930        }
931
932        // One getdate() rather than six separate date() calls: it returns every
933        // field this method needs from a single timestamp conversion. Queue::run()
934        // calls this once per task per second inside its sub-minute tick loop, so
935        // this is the hottest path in the component and the six-fold saving lands
936        // squarely on it.
937        $parts  = getdate($time);
938        $second = $parts['seconds'];
939
940        // Short-circuited, coarsest field first. The result is an AND across every
941        // field, so stopping at the first failure cannot change the answer - and
942        // the coarse fields (day-of-week, month, day-of-month) are the ones that
943        // rule a schedule out most often, so testing them first means a task that
944        // isn't due today costs one field test instead of six.
945        //
946        // The seconds field is deliberately excluded from this chain: on a
947        // minute-granularity schedule it is not part of the decision at all (the
948        // grace period governs the seconds instead), which is why hasSeconds()
949        // gates it below rather than it being tested inline here.
950        if ((!$this->fieldPasses($this->daysOfTheWeek, $parts['wday'])) ||
951            (!$this->fieldPasses($this->months, $parts['mon'])) ||
952            (!$this->fieldPasses($this->daysOfTheMonth, $parts['mday'])) ||
953            (!$this->fieldPasses($this->hours, $parts['hours'])) ||
954            (!$this->fieldPasses($this->minutes, $parts['minutes']))) {
955            return false;
956        }
957
958        if ($this->hasSeconds()) {
959            return $this->fieldPasses($this->seconds, $second);
960        }
961
962        // Reached only when every minute-granularity field has already matched,
963        // which is exactly the condition the old explicit '* * * * *' special case
964        // tested for - an all-wildcard schedule passes every field above, so it
965        // arrives here and gets the same grace-period-only answer it always did,
966        // without needing its own branch.
967        return (($this->gracePeriod < 0) || ($second <= $this->gracePeriod));
968    }
969
970    /**
971     * Determine whether one schedule field matches the corresponding value from
972     * the time being evaluated.
973     *
974     * This is the per-field test that evaluate() used to inline six times over.
975     * The three cases, in the order the original checked them: the wildcard '*',
976     * a literal value present in the field, and - only for a single-element
977     * string field - a compound expression (a comma list, a step, or a range)
978     * handed off to evaluateExpression().
979     *
980     * The loose in_array() comparison is intentional and load-bearing: field
981     * values arrive as strings when parsed out of a schedule string but as ints
982     * when set through the fluent helpers (hourly(), daily(), and friends all
983     * assign ints), so both have to compare equal to the int taken from the
984     * timestamp.
985     *
986     * @param  array $field
987     * @param  int   $value
988     * @return bool
989     */
990    protected function fieldPasses(array $field, int $value): bool
991    {
992        // Checked before in_array() rather than after it, as the original did:
993        // the answer is identical either way (no integer is loosely equal to
994        // '*'), and the wildcard is overwhelmingly the most common field, so
995        // it is the one worth answering first.
996        if ($field == ['*']) {
997            return true;
998        }
999
1000        if (in_array($value, $field)) {
1001            return true;
1002        }
1003
1004        return ((count($field) == 1) && is_string($field[0]) && $this->evaluateExpression($field[0], $value));
1005    }
1006
1007    /**
1008     * To string method
1009     *
1010     * @return string
1011     */
1012    public function __toString(): string
1013    {
1014        return $this->render();
1015    }
1016
1017    /**
1018     * Determine if the value satisfies the schedule expression
1019     *
1020     * @param  string $expression
1021     * @param  mixed  $value
1022     * @return bool
1023     */
1024    protected function evaluateExpression(string $expression, mixed $value): bool
1025    {
1026        if (str_contains($expression, ',')) {
1027            $values = array_map('trim', explode(',', $expression));
1028            return in_array($value, $values);
1029        } else if (str_contains($expression, '/')) {
1030            $step = (int)substr($expression, (strpos($expression, '/') + 1));
1031            return (($value % $step) == 0);
1032        } else if (str_contains($expression, '-')) {
1033            list($min, $max) = explode('-', $expression);
1034            return (($value >= $min) && ($value <= $max));
1035        }
1036
1037        return false;
1038    }
1039
1040}