Passed
Push — master ( 7c044f...6abd51 )
by Kevin
02:07
created

Task::dailyBetween()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
namespace Zenstruck\ScheduleBundle\Schedule;
4
5
use Zenstruck\ScheduleBundle\Event\BeforeTaskEvent;
6
use Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask;
7
use Zenstruck\ScheduleBundle\Schedule\Extension\BetweenTimeExtension;
8
use Zenstruck\ScheduleBundle\Schedule\Extension\CallbackExtension;
9
use Zenstruck\ScheduleBundle\Schedule\Extension\EmailExtension;
10
use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
11
use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
12
use Zenstruck\ScheduleBundle\Schedule\Extension\WithoutOverlappingExtension;
13
14
/**
15
 * @author Taylor Otwell <[email protected]>
16
 * @author Kevin Bond <[email protected]>
17
 */
18
abstract class Task
19
{
20
    private const DEFAULT_EXPRESSION = '* * * * *';
21
22
    private $description;
23
    private $expression = self::DEFAULT_EXPRESSION;
24
    private $timezone;
25
26
    /** @var Extension[] */
27
    private $extensions = [];
28
29 167
    public function __construct(string $description)
30
    {
31 167
        $this->description = $description;
32 167
    }
33
34 30
    final public function __toString(): string
35
    {
36 30
        return $this->getDescription();
37
    }
38
39 27
    public function getType(): string
40
    {
41 27
        return (new \ReflectionClass($this))->getShortName();
42
    }
43
44 7
    final public function getId(): string
45
    {
46 7
        return \sha1(\get_class($this).$this->getExpression().$this->getDescription());
47
    }
48
49 115
    final public function getDescription(): string
50
    {
51 115
        return $this->description;
52
    }
53
54 107
    final public function getExpression(): CronExpression
55
    {
56 107
        return new CronExpression($this->expression, $this->getDescription());
57
    }
58
59 113
    final public function getTimezone(): ?\DateTimeZone
60
    {
61 113
        return $this->timezone;
62
    }
63
64 61
    final public function getNextRun(): \DateTimeInterface
65
    {
66 61
        return $this->getExpression()->getNextRun($this->getTimezoneValue());
67
    }
68
69 33
    final public function isDue(): bool
70
    {
71 33
        return $this->getExpression()->isDue($this->getTimezoneValue());
72
    }
73
74
    /**
75
     * @return Extension[]
76
     */
77 69
    final public function getExtensions(): array
78
    {
79 69
        return $this->extensions;
80
    }
81
82 48
    final public function addExtension(Extension $extension): self
83
    {
84 48
        $this->extensions[] = $extension;
85
86 48
        return $this;
87
    }
88
89
    /**
90
     * Set a unique description for this task.
91
     */
92 10
    final public function description(string $description): self
93
    {
94 10
        $this->description = $description;
95
96 10
        return $this;
97
    }
98
99
    /**
100
     * The timezone this task should run in.
101
     *
102
     * @param string|\DateTimeZone $value
103
     */
104 6
    final public function timezone($value): self
105
    {
106 6
        if (!$value instanceof \DateTimeZone) {
107 6
            $value = new \DateTimeZone($value);
108
        }
109
110 6
        $this->timezone = $value;
111
112 6
        return $this;
113
    }
114
115
    /**
116
     * Prevent task from running if callback throws \Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask.
117
     *
118
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
119
     */
120 12
    final public function filter(callable $callback): self
121
    {
122 12
        return $this->addExtension(CallbackExtension::taskFilter($callback));
123
    }
124
125
    /**
126
     * Only run task if true.
127
     *
128
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
129
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
130
     */
131 4
    final public function when(string $description, $callback): self
132
    {
133
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
134 2
            return (bool) $callback;
135 4
        };
136
137
        return $this->filter(function (BeforeTaskEvent $event) use ($callback, $description) {
138 4
            if (!$callback($event)) {
139 2
                throw new SkipTask($description);
140
            }
141 4
        });
142
    }
143
144
    /**
145
     * Skip task if true.
146
     *
147
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
148
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
149
     */
150 5
    final public function skip(string $description, $callback): self
151
    {
152
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
153 3
            return (bool) $callback;
154 5
        };
155
156
        return $this->filter(function (BeforeTaskEvent $event) use ($callback, $description) {
157 5
            if ($callback($event)) {
158 3
                throw new SkipTask($description);
159
            }
160 5
        });
161
    }
162
163
    /**
164
     * Execute callback before task runs.
165
     *
166
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
167
     */
168 3
    final public function before(callable $callback): self
169
    {
170 3
        return $this->addExtension(CallbackExtension::taskBefore($callback));
171
    }
172
173
    /**
174
     * Execute callback after task has run (will not execute if skipped).
175
     *
176
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
177
     */
178 3
    final public function after(callable $callback): self
179
    {
180 3
        return $this->addExtension(CallbackExtension::taskAfter($callback));
181
    }
182
183
    /**
184
     * Alias for after().
185
     */
186 1
    final public function then(callable $callback): self
187
    {
188 1
        return $this->after($callback);
189
    }
190
191
    /**
192
     * Execute callback if task was successful (will not execute if skipped).
193
     *
194
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
195
     */
196 3
    final public function onSuccess(callable $callback): self
197
    {
198 3
        return $this->addExtension(CallbackExtension::taskSuccess($callback));
199
    }
200
201
    /**
202
     * Execute callback if task failed (will not execute if skipped).
203
     *
204
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
205
     */
206 3
    final public function onFailure(callable $callback): self
207
    {
208 3
        return $this->addExtension(CallbackExtension::taskFailure($callback));
209
    }
210
211
    /**
212
     * Ping a webhook before task runs (will not ping if task was skipped).
213
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
214
     *
215
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
216
     */
217 4
    final public function pingBefore(string $url, string $method = 'GET', array $options = []): self
218
    {
219 4
        return $this->addExtension(PingExtension::taskBefore($url, $method, $options));
220
    }
221
222
    /**
223
     * Ping a webhook after task has run (will not ping if task was skipped).
224
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
225
     *
226
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
227
     */
228 4
    final public function pingAfter(string $url, string $method = 'GET', array $options = []): self
229
    {
230 4
        return $this->addExtension(PingExtension::taskAfter($url, $method, $options));
231
    }
232
233
    /**
234
     * Alias for pingAfter().
235
     */
236 1
    final public function thenPing(string $url, string $method = 'GET', array $options = []): self
237
    {
238 1
        return $this->pingAfter($url, $method, $options);
239
    }
240
241
    /**
242
     * Ping a webhook if task was successful (will not ping if task was skipped).
243
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
244
     *
245
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
246
     */
247 4
    final public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
248
    {
249 4
        return $this->addExtension(PingExtension::taskSuccess($url, $method, $options));
250
    }
251
252
    /**
253
     * Ping a webhook if task failed (will not ping if task was skipped).
254
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
255
     *
256
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
257
     */
258 6
    final public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
259
    {
260 6
        return $this->addExtension(PingExtension::taskFailure($url, $method, $options));
261
    }
262
263
    /**
264
     * Email task detail after run (on success or failure, not if skipped).
265
     * Be sure to configure `zenstruck_schedule.email_handler`.
266
     *
267
     * @param string|string[] $to       Email address(es)
268
     * @param callable|null   $callback Add your own headers etc
269
     *                                  Receives an instance of \Symfony\Component\Mime\Email
270
     */
271 6
    final public function emailAfter($to = null, string $subject = null, callable $callback = null): self
272
    {
273 6
        return $this->addExtension(EmailExtension::taskAfter($to, $subject, $callback));
274
    }
275
276
    /**
277
     * Alias for emailAfter().
278
     */
279 1
    final public function thenEmail($to = null, string $subject = null, callable $callback = null): self
280
    {
281 1
        return $this->emailAfter($to, $subject, $callback);
282
    }
283
284
    /**
285
     * Email task/failure details if failed (not if skipped).
286
     * Be sure to configure `zenstruck_schedule.email_handler`.
287
     *
288
     * @param string|string[] $to       Email address(es)
289
     * @param callable|null   $callback Add your own headers etc
290
     *                                  Receives an instance of \Symfony\Component\Mime\Email
291
     */
292 8
    final public function emailOnFailure($to = null, string $subject = null, callable $callback = null): self
293
    {
294 8
        return $this->addExtension(EmailExtension::taskFailure($to, $subject, $callback));
295
    }
296
297
    /**
298
     * Prevent task from running if still running from previous run.
299
     *
300
     * @param int $ttl Maximum expected lock duration in seconds
301
     */
302 5
    final public function withoutOverlapping(int $ttl = WithoutOverlappingExtension::DEFAULT_TTL): self
303
    {
304 5
        return $this->addExtension(new WithoutOverlappingExtension($ttl));
305
    }
306
307
    /**
308
     * Restrict running of schedule to a single server.
309
     * Be sure to configure `zenstruck_schedule.single_server_handler`.
310
     *
311
     * @param int $ttl Maximum expected lock duration in seconds
312
     */
313 4
    final public function onSingleServer(int $ttl = SingleServerExtension::DEFAULT_TTL): self
314
    {
315 4
        return $this->addExtension(new SingleServerExtension($ttl));
316
    }
317
318
    /**
319
     * Only run between given times.
320
     *
321
     * @param string $startTime "HH:MM" (ie "09:00")
322
     * @param string $endTime   "HH:MM" (ie "14:30")
323
     * @param bool   $inclusive Whether to include the start and end time
324
     */
325 7
    final public function onlyBetween(string $startTime, string $endTime, bool $inclusive = true): self
326
    {
327 7
        return $this->addExtension(BetweenTimeExtension::whenWithin($startTime, $endTime, $inclusive));
328
    }
329
330
    /**
331
     * Skip when between given times.
332
     *
333
     * @param string $startTime "HH:MM" (ie "09:00")
334
     * @param string $endTime   "HH:MM" (ie "14:30")
335
     * @param bool   $inclusive Whether to include the start and end time
336
     */
337 7
    final public function unlessBetween(string $startTime, string $endTime, bool $inclusive = true): self
338
    {
339 7
        return $this->addExtension(BetweenTimeExtension::unlessWithin($startTime, $endTime, $inclusive));
340
    }
341
342
    /**
343
     * Set your own cron expression (ie "15 3 * * 1,4").
344
     */
345 71
    final public function cron(string $expression): self
346
    {
347 71
        $this->expression = $expression;
348
349 71
        return $this;
350
    }
351
352
    /**
353
     * Resets the expression to "* * * * *".
354
     */
355 2
    final public function everyMinute(): self
356
    {
357 2
        return $this->cron(self::DEFAULT_EXPRESSION);
358
    }
359
360
    /**
361
     * Resets the expression to "<*>/5 * * * *".
362
     */
363 1
    final public function everyFiveMinutes(): self
364
    {
365 1
        return $this->cron('*/5 * * * *');
366
    }
367
368
    /**
369
     * Resets the expression to "<*>/10 * * * *".
370
     */
371 1
    final public function everyTenMinutes(): self
372
    {
373 1
        return $this->cron('*/10 * * * *');
374
    }
375
376
    /**
377
     * Resets the expression to "<*>/15 * * * *".
378
     */
379 1
    final public function everyFifteenMinutes(): self
380
    {
381 1
        return $this->cron('*/15 * * * *');
382
    }
383
384
    /**
385
     * Resets the expression to "<*>/20 * * * *".
386
     */
387 1
    final public function everyTwentyMinutes(): self
388
    {
389 1
        return $this->cron('*/20 * * * *');
390
    }
391
392
    /**
393
     * Resets the expression to "0,30 * * * *".
394
     */
395 1
    final public function everyThirtyMinutes(): self
396
    {
397 1
        return $this->cron('0,30 * * * *');
398
    }
399
400
    /**
401
     * Resets the expression to "0 * * * *".
402
     */
403 3
    final public function hourly(): self
404
    {
405 3
        return $this->cron('0 * * * *');
406
    }
407
408
    /**
409
     * Resets the expression to "X * * * *" with X being the passed minute(s).
410
     *
411
     * @param int|string $minute     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
412
     * @param int|string ...$minutes Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
413
     */
414 2
    final public function hourlyAt($minute, ...$minutes): self
415
    {
416 2
        return $this->hourly()->minutes($minute, ...$minutes);
417
    }
418
419
    /**
420
     * Resets the expression to "0 0 * * *".
421
     */
422 9
    final public function daily(): self
423
    {
424 9
        return $this->cron('0 0 * * *');
425
    }
426
427
    /**
428
     * Resets the expression to "0 X * * *" with X being the passed hour(s).
429
     *
430
     * @param int|string $hour     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
431
     * @param int|string ...$hours Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
432
     */
433 3
    final public function dailyOn($hour, ...$hours): self
434
    {
435 3
        return $this->daily()->hours($hour, ...$hours);
436
    }
437
438
    /**
439
     * Resets the expression to "0 X-Y * * *" with X and Y being the passed start and end hours.
440
     *
441
     * @param int $firstHour  0-23
442
     * @param int $secondHour 0-23
443
     */
444 1
    final public function dailyBetween(int $firstHour, int $secondHour): self
445
    {
446 1
        return $this->daily()->hours("{$firstHour}-{$secondHour}");
447
    }
448
449
    /**
450
     * Resets the expression to "0 X,Y * * *" with X and Y being the passed hours.
451
     *
452
     * @param int $firstHour  0-23
453
     * @param int $secondHour 0-23
454
     */
455 2
    final public function twiceDaily(int $firstHour = 1, int $secondHour = 13): self
456
    {
457 2
        return $this->dailyOn($firstHour, $secondHour);
458
    }
459
460
    /**
461
     * Shortcut for ->daily()->at($time).
462
     *
463
     * @param string $time Integer for just the hour (ie 2) or "HH:MM" for hour and minute (ie "14:30")
464
     */
465 2
    final public function dailyAt(string $time): self
466
    {
467 2
        return $this->daily()->at($time);
468
    }
469
470
    /**
471
     * Resets the expression to "0 0 * * 0".
472
     */
473 14
    final public function weekly(): self
474
    {
475 14
        return $this->cron('0 0 * * 0');
476
    }
477
478
    /**
479
     * Resets the expression to "0 0 * * X" with X being the passed day(s) of week.
480
     *
481
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
482
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
483
     */
484 1
    final public function weeklyOn($day, ...$days): self
485
    {
486 1
        return $this->weekly()->daysOfWeek($day, ...$days);
487
    }
488
489
    /**
490
     * Resets the expression to "0 0 1 * *".
491
     */
492 5
    final public function monthly(): self
493
    {
494 5
        return $this->cron('0 0 1 * *');
495
    }
496
497
    /**
498
     * Resets the expression to "0 0 X * *" with X being the passed day(s) of month.
499
     *
500
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
501
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
502
     */
503 3
    final public function monthlyOn($day, ...$days): self
504
    {
505 3
        return $this->monthly()->daysOfMonth($day, ...$days);
506
    }
507
508
    /**
509
     * Resets the expression to "0 0 X,Y * *" with X and Y being the passed days of the month.
510
     *
511
     * @param int $firstDay  1-31
512
     * @param int $secondDay 1-31
513
     */
514 2
    final public function twiceMonthly(int $firstDay = 1, int $secondDay = 16): self
515
    {
516 2
        return $this->monthlyOn($firstDay, $secondDay);
517
    }
518
519
    /**
520
     * Resets the expression to "0 0 1 1 *".
521
     */
522 2
    final public function yearly(): self
523
    {
524 2
        return $this->cron('0 0 1 1 *');
525
    }
526
527
    /**
528
     * Resets the expression to "0 0 1 1-12/3 *".
529
     */
530 1
    final public function quarterly(): self
531
    {
532 1
        return $this->cron('0 0 1 */3 *');
533
    }
534
535
    /**
536
     * Set just the "minute" field.
537
     *
538
     * @param int|string $minute     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
539
     * @param int|string ...$minutes Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
540
     */
541 34
    final public function minutes($minute, ...$minutes): self
542
    {
543 34
        return $this->spliceIntoPosition(CronExpression::MINUTE, $minute, ...$minutes);
544
    }
545
546
    /**
547
     * Set just the "hour" field.
548
     *
549
     * @param int|string $hour     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
550
     * @param int|string ...$hours Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
551
     */
552 12
    final public function hours($hour, ...$hours): self
553
    {
554 12
        return $this->spliceIntoPosition(CronExpression::HOUR, $hour, ...$hours);
555
    }
556
557
    /**
558
     * Set just the "day of month" field.
559
     *
560
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
561
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
562
     */
563 4
    final public function daysOfMonth($day, ...$days): self
564
    {
565 4
        return $this->spliceIntoPosition(CronExpression::DOM, $day, ...$days);
566
    }
567
568
    /**
569
     * Set just the "month" field.
570
     *
571
     * @param int|string $month     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-12/3)
572
     * @param int|string ...$months Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-12/3)
573
     */
574 1
    final public function months($month, ...$months): self
575
    {
576 1
        return $this->spliceIntoPosition(CronExpression::MONTH, $month, ...$months);
577
    }
578
579
    /**
580
     * Set just the "day of week" field.
581
     *
582
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
583
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
584
     */
585 18
    final public function daysOfWeek($day, ...$days): self
586
    {
587 18
        return $this->spliceIntoPosition(CronExpression::DOW, $day, ...$days);
588
    }
589
590
    /**
591
     * Set just the "day of week" field.
592
     */
593 1
    final public function weekdays(): self
594
    {
595 1
        return $this->daysOfWeek('1-5');
596
    }
597
598
    /**
599
     * Set just the "day of week" field.
600
     */
601 1
    final public function weekends(): self
602
    {
603 1
        return $this->daysOfWeek(0, 6);
604
    }
605
606
    /**
607
     * Set just the "day of week" field.
608
     */
609 7
    final public function mondays(): self
610
    {
611 7
        return $this->daysOfWeek(1);
612
    }
613
614
    /**
615
     * Set just the "day of week" field.
616
     */
617 2
    final public function tuesdays(): self
618
    {
619 2
        return $this->daysOfWeek(2);
620
    }
621
622
    /**
623
     * Set just the "day of week" field.
624
     */
625 1
    final public function wednesdays(): self
626
    {
627 1
        return $this->daysOfWeek(3);
628
    }
629
630
    /**
631
     * Set just the "day of week" field.
632
     */
633 1
    final public function thursdays(): self
634
    {
635 1
        return $this->daysOfWeek(4);
636
    }
637
638
    /**
639
     * Set just the "day of week" field.
640
     */
641 1
    final public function fridays(): self
642
    {
643 1
        return $this->daysOfWeek(5);
644
    }
645
646
    /**
647
     * Set just the "day of week" field.
648
     */
649 1
    final public function saturdays(): self
650
    {
651 1
        return $this->daysOfWeek(6);
652
    }
653
654
    /**
655
     * Set just the "day of week" field.
656
     */
657 3
    final public function sundays(): self
658
    {
659 3
        return $this->daysOfWeek(0);
660
    }
661
662
    /**
663
     * Set just the "hour" and optionally the "minute" field(s).
664
     *
665
     * @param string $time Integer for just the hour (ie 2) or "HH:MM" for hour and minute (ie "14:30")
666
     */
667 7
    final public function at(string $time): self
668
    {
669 7
        $segments = \explode(':', $time);
670
671
        return $this
672 7
            ->hours($segments[0])
673 7
            ->minutes(2 === \count($segments) ? $segments[1] : 0)
674
        ;
675
    }
676
677
    /**
678
     * @param int|string $value
679
     * @param int|string ...$values
680
     */
681 42
    private function spliceIntoPosition(int $position, $value, ...$values): self
682
    {
683 42
        $segments = \explode(' ', $this->expression);
684
685 42
        if (5 !== \count($segments)) { // reset if set to alias or invalid
686 1
            $this->expression = self::DEFAULT_EXPRESSION;
687
688 1
            return $this->spliceIntoPosition($position, $value);
689
        }
690
691 42
        $segments[$position] = \implode(',', \array_merge([$value], $values));
692
693 42
        return $this->cron(\implode(' ', $segments));
694
    }
695
696 94
    private function getTimezoneValue(): ?string
697
    {
698 94
        return $this->getTimezone() ? $this->getTimezone()->getName() : null;
699
    }
700
}
701