Passed
Push — master ( 8b3ece...7c044f )
by Kevin
01:49
created

Task::onlyBetween()   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
rs 10
cc 1
nc 1
nop 3
ccs 2
cts 2
cp 1
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 166
    public function __construct(string $description)
30
    {
31 166
        $this->description = $description;
32 166
    }
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 114
    final public function getDescription(): string
50
    {
51 114
        return $this->description;
52
    }
53
54 106
    final public function getExpression(): CronExpression
55
    {
56 106
        return new CronExpression($this->expression, $this->getDescription());
57
    }
58
59 112
    final public function getTimezone(): ?\DateTimeZone
60
    {
61 112
        return $this->timezone;
62
    }
63
64 60
    final public function getNextRun(): \DateTimeInterface
65
    {
66 60
        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 70
    final public function cron(string $expression): self
346
    {
347 70
        $this->expression = $expression;
348
349 70
        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 8
    final public function daily(): self
423
    {
424 8
        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 hours.
440
     *
441
     * @param int $firstHour  0-23
442
     * @param int $secondHour 0-23
443
     */
444 2
    final public function twiceDaily(int $firstHour = 1, int $secondHour = 13): self
445
    {
446 2
        return $this->dailyOn($firstHour, $secondHour);
447
    }
448
449
    /**
450
     * Shortcut for ->daily()->at($time).
451
     *
452
     * @param string $time Integer for just the hour (ie 2) or "HH:MM" for hour and minute (ie "14:30")
453
     */
454 2
    final public function dailyAt(string $time): self
455
    {
456 2
        return $this->daily()->at($time);
457
    }
458
459
    /**
460
     * Resets the expression to "0 0 * * 0".
461
     */
462 14
    final public function weekly(): self
463
    {
464 14
        return $this->cron('0 0 * * 0');
465
    }
466
467
    /**
468
     * Resets the expression to "0 0 * * X" with X being the passed day(s) of week.
469
     *
470
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
471
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
472
     */
473 1
    final public function weeklyOn($day, ...$days): self
474
    {
475 1
        return $this->weekly()->daysOfWeek($day, ...$days);
476
    }
477
478
    /**
479
     * Resets the expression to "0 0 1 * *".
480
     */
481 5
    final public function monthly(): self
482
    {
483 5
        return $this->cron('0 0 1 * *');
484
    }
485
486
    /**
487
     * Resets the expression to "0 0 X * *" with X being the passed day(s) of month.
488
     *
489
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
490
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
491
     */
492 3
    final public function monthlyOn($day, ...$days): self
493
    {
494 3
        return $this->monthly()->daysOfMonth($day, ...$days);
495
    }
496
497
    /**
498
     * Resets the expression to "0 0 X,Y * *" with X and Y being the passed days of the month.
499
     *
500
     * @param int $firstDay  1-31
501
     * @param int $secondDay 1-31
502
     */
503 2
    final public function twiceMonthly(int $firstDay = 1, int $secondDay = 16): self
504
    {
505 2
        return $this->monthlyOn($firstDay, $secondDay);
506
    }
507
508
    /**
509
     * Resets the expression to "0 0 1 1 *".
510
     */
511 2
    final public function yearly(): self
512
    {
513 2
        return $this->cron('0 0 1 1 *');
514
    }
515
516
    /**
517
     * Resets the expression to "0 0 1 1-12/3 *".
518
     */
519 1
    final public function quarterly(): self
520
    {
521 1
        return $this->cron('0 0 1 */3 *');
522
    }
523
524
    /**
525
     * Set just the "minute" field.
526
     *
527
     * @param int|string $minute     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
528
     * @param int|string ...$minutes Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
529
     */
530 33
    final public function minutes($minute, ...$minutes): self
531
    {
532 33
        return $this->spliceIntoPosition(CronExpression::MINUTE, $minute, ...$minutes);
533
    }
534
535
    /**
536
     * Set just the "hour" field.
537
     *
538
     * @param int|string $hour     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
539
     * @param int|string ...$hours Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
540
     */
541 11
    final public function hours($hour, ...$hours): self
542
    {
543 11
        return $this->spliceIntoPosition(CronExpression::HOUR, $hour, ...$hours);
544
    }
545
546
    /**
547
     * Set just the "day of month" field.
548
     *
549
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
550
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
551
     */
552 4
    final public function daysOfMonth($day, ...$days): self
553
    {
554 4
        return $this->spliceIntoPosition(CronExpression::DOM, $day, ...$days);
555
    }
556
557
    /**
558
     * Set just the "month" field.
559
     *
560
     * @param int|string $month     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-12/3)
561
     * @param int|string ...$months Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-12/3)
562
     */
563 1
    final public function months($month, ...$months): self
564
    {
565 1
        return $this->spliceIntoPosition(CronExpression::MONTH, $month, ...$months);
566
    }
567
568
    /**
569
     * Set just the "day of week" field.
570
     *
571
     * @param int|string $day     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
572
     * @param int|string ...$days Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
573
     */
574 18
    final public function daysOfWeek($day, ...$days): self
575
    {
576 18
        return $this->spliceIntoPosition(CronExpression::DOW, $day, ...$days);
577
    }
578
579
    /**
580
     * Set just the "day of week" field.
581
     */
582 1
    final public function weekdays(): self
583
    {
584 1
        return $this->daysOfWeek('1-5');
585
    }
586
587
    /**
588
     * Set just the "day of week" field.
589
     */
590 1
    final public function weekends(): self
591
    {
592 1
        return $this->daysOfWeek(0, 6);
593
    }
594
595
    /**
596
     * Set just the "day of week" field.
597
     */
598 7
    final public function mondays(): self
599
    {
600 7
        return $this->daysOfWeek(1);
601
    }
602
603
    /**
604
     * Set just the "day of week" field.
605
     */
606 2
    final public function tuesdays(): self
607
    {
608 2
        return $this->daysOfWeek(2);
609
    }
610
611
    /**
612
     * Set just the "day of week" field.
613
     */
614 1
    final public function wednesdays(): self
615
    {
616 1
        return $this->daysOfWeek(3);
617
    }
618
619
    /**
620
     * Set just the "day of week" field.
621
     */
622 1
    final public function thursdays(): self
623
    {
624 1
        return $this->daysOfWeek(4);
625
    }
626
627
    /**
628
     * Set just the "day of week" field.
629
     */
630 1
    final public function fridays(): self
631
    {
632 1
        return $this->daysOfWeek(5);
633
    }
634
635
    /**
636
     * Set just the "day of week" field.
637
     */
638 1
    final public function saturdays(): self
639
    {
640 1
        return $this->daysOfWeek(6);
641
    }
642
643
    /**
644
     * Set just the "day of week" field.
645
     */
646 3
    final public function sundays(): self
647
    {
648 3
        return $this->daysOfWeek(0);
649
    }
650
651
    /**
652
     * Set just the "hour" and optionally the "minute" field(s).
653
     *
654
     * @param string $time Integer for just the hour (ie 2) or "HH:MM" for hour and minute (ie "14:30")
655
     */
656 7
    final public function at(string $time): self
657
    {
658 7
        $segments = \explode(':', $time);
659
660
        return $this
661 7
            ->hours($segments[0])
662 7
            ->minutes(2 === \count($segments) ? $segments[1] : 0)
663
        ;
664
    }
665
666
    /**
667
     * @param int|string $value
668
     * @param int|string ...$values
669
     */
670 41
    private function spliceIntoPosition(int $position, $value, ...$values): self
671
    {
672 41
        $segments = \explode(' ', $this->expression);
673
674 41
        if (5 !== \count($segments)) { // reset if set to alias or invalid
675 1
            $this->expression = self::DEFAULT_EXPRESSION;
676
677 1
            return $this->spliceIntoPosition($position, $value);
678
        }
679
680 41
        $segments[$position] = \implode(',', \array_merge([$value], $values));
681
682 41
        return $this->cron(\implode(' ', $segments));
683
    }
684
685 93
    private function getTimezoneValue(): ?string
686
    {
687 93
        return $this->getTimezone() ? $this->getTimezone()->getName() : null;
688
    }
689
}
690