Passed
Push — master ( 3ce531...7a6df5 )
by Kevin
02:21
created

Task::getContext()   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 0
crap 1
1
<?php
2
3
namespace Zenstruck\ScheduleBundle\Schedule;
4
5
use Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask;
6
use Zenstruck\ScheduleBundle\Schedule\Extension\BetweenTimeExtension;
7
use Zenstruck\ScheduleBundle\Schedule\Extension\CallbackExtension;
8
use Zenstruck\ScheduleBundle\Schedule\Extension\EmailExtension;
9
use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
10
use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
11
use Zenstruck\ScheduleBundle\Schedule\Extension\WithoutOverlappingExtension;
12
use Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext;
13
14
/**
15
 * @author Taylor Otwell <[email protected]>
16
 * @author Kevin Bond <[email protected]>
17
 */
18
abstract class Task
19
{
20
    use HasExtensions;
21
22
    private const DEFAULT_EXPRESSION = '* * * * *';
23
24
    private $description;
25
    private $expression = self::DEFAULT_EXPRESSION;
26
    private $timezone;
27
28 183
    public function __construct(string $description)
29
    {
30 183
        $this->description = $description;
31 183
    }
32
33 24
    final public function __toString(): string
34
    {
35 24
        return "{$this->getType()}: {$this->getDescription()}";
36
    }
37
38 37
    public function getType(): string
39
    {
40 37
        return (new \ReflectionClass($this))->getShortName();
41
    }
42
43 48
    final public function getId(): string
44
    {
45 48
        return \sha1(\get_class($this).$this->getExpression().$this->getDescription());
46
    }
47
48 126
    final public function getDescription(): string
49
    {
50 126
        return $this->description;
51
    }
52
53 2
    public function getContext(): array
54
    {
55 2
        return [];
56
    }
57
58 116
    final public function getExpression(): CronExpression
59
    {
60 116
        return new CronExpression($this->expression, $this->getDescription());
61
    }
62
63 117
    final public function getTimezone(): ?\DateTimeZone
64
    {
65 117
        return $this->timezone;
66
    }
67
68 63
    final public function getNextRun(): \DateTimeInterface
69
    {
70 63
        return $this->getExpression()->getNextRun($this->getTimezoneValue());
71
    }
72
73 35
    final public function isDue(\DateTimeInterface $timestamp): bool
74
    {
75 35
        return $this->getExpression()->isDue($timestamp, $this->getTimezoneValue());
76
    }
77
78
    /**
79
     * Set a unique description for this task.
80
     */
81 10
    final public function description(string $description): self
82
    {
83 10
        $this->description = $description;
84
85 10
        return $this;
86
    }
87
88
    /**
89
     * The timezone this task should run in.
90
     *
91
     * @param string|\DateTimeZone $value
92
     */
93 5
    final public function timezone($value): self
94
    {
95 5
        if (!$value instanceof \DateTimeZone) {
96 5
            $value = new \DateTimeZone($value);
97
        }
98
99 5
        $this->timezone = $value;
100
101 5
        return $this;
102
    }
103
104
    /**
105
     * Prevent task from running if callback throws \Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask.
106
     *
107
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
108
     */
109 12
    final public function filter(callable $callback): self
110
    {
111 12
        return $this->addExtension(CallbackExtension::taskFilter($callback));
112
    }
113
114
    /**
115
     * Only run task if true.
116
     *
117
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
118
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
119
     */
120 4
    final public function when(string $description, $callback): self
121
    {
122
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
123 2
            return (bool) $callback;
124 4
        };
125
126
        return $this->filter(function (TaskRunContext $context) use ($callback, $description) {
127 4
            if (!$callback($context)) {
128 2
                throw new SkipTask($description);
129
            }
130 4
        });
131
    }
132
133
    /**
134
     * Skip task if true.
135
     *
136
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
137
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
138
     */
139 5
    final public function skip(string $description, $callback): self
140
    {
141
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
142 3
            return (bool) $callback;
143 5
        };
144
145
        return $this->filter(function (TaskRunContext $context) use ($callback, $description) {
146 5
            if ($callback($context)) {
147 3
                throw new SkipTask($description);
148
            }
149 5
        });
150
    }
151
152
    /**
153
     * Execute callback before task runs.
154
     *
155
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
156
     */
157 3
    final public function before(callable $callback): self
158
    {
159 3
        return $this->addExtension(CallbackExtension::taskBefore($callback));
160
    }
161
162
    /**
163
     * Execute callback after task has run (will not execute if skipped).
164
     *
165
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
166
     */
167 3
    final public function after(callable $callback): self
168
    {
169 3
        return $this->addExtension(CallbackExtension::taskAfter($callback));
170
    }
171
172
    /**
173
     * Alias for after().
174
     */
175 1
    final public function then(callable $callback): self
176
    {
177 1
        return $this->after($callback);
178
    }
179
180
    /**
181
     * Execute callback if task was successful (will not execute if skipped).
182
     *
183
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
184
     */
185 3
    final public function onSuccess(callable $callback): self
186
    {
187 3
        return $this->addExtension(CallbackExtension::taskSuccess($callback));
188
    }
189
190
    /**
191
     * Execute callback if task failed (will not execute if skipped).
192
     *
193
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
194
     */
195 3
    final public function onFailure(callable $callback): self
196
    {
197 3
        return $this->addExtension(CallbackExtension::taskFailure($callback));
198
    }
199
200
    /**
201
     * Ping a webhook before task runs (will not ping if task was skipped).
202
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
203
     *
204
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
205
     */
206 4
    final public function pingBefore(string $url, string $method = 'GET', array $options = []): self
207
    {
208 4
        return $this->addExtension(PingExtension::taskBefore($url, $method, $options));
209
    }
210
211
    /**
212
     * Ping a webhook after task has run (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 pingAfter(string $url, string $method = 'GET', array $options = []): self
218
    {
219 4
        return $this->addExtension(PingExtension::taskAfter($url, $method, $options));
220
    }
221
222
    /**
223
     * Alias for pingAfter().
224
     */
225 1
    final public function thenPing(string $url, string $method = 'GET', array $options = []): self
226
    {
227 1
        return $this->pingAfter($url, $method, $options);
228
    }
229
230
    /**
231
     * Ping a webhook if task was successful (will not ping if task was skipped).
232
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
233
     *
234
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
235
     */
236 4
    final public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
237
    {
238 4
        return $this->addExtension(PingExtension::taskSuccess($url, $method, $options));
239
    }
240
241
    /**
242
     * Ping a webhook if task failed (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 6
    final public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
248
    {
249 6
        return $this->addExtension(PingExtension::taskFailure($url, $method, $options));
250
    }
251
252
    /**
253
     * Email task detail after run (on success or failure, not if skipped).
254
     * Be sure to configure `zenstruck_schedule.email_handler`.
255
     *
256
     * @param string|string[] $to       Email address(es)
257
     * @param callable|null   $callback Add your own headers etc
258
     *                                  Receives an instance of \Symfony\Component\Mime\Email
259
     */
260 7
    final public function emailAfter($to = null, string $subject = null, callable $callback = null): self
261
    {
262 7
        return $this->addExtension(EmailExtension::taskAfter($to, $subject, $callback));
263
    }
264
265
    /**
266
     * Alias for emailAfter().
267
     */
268 1
    final public function thenEmail($to = null, string $subject = null, callable $callback = null): self
269
    {
270 1
        return $this->emailAfter($to, $subject, $callback);
271
    }
272
273
    /**
274
     * Email task/failure details if failed (not if skipped).
275
     * Be sure to configure `zenstruck_schedule.email_handler`.
276
     *
277
     * @param string|string[] $to       Email address(es)
278
     * @param callable|null   $callback Add your own headers etc
279
     *                                  Receives an instance of \Symfony\Component\Mime\Email
280
     */
281 8
    final public function emailOnFailure($to = null, string $subject = null, callable $callback = null): self
282
    {
283 8
        return $this->addExtension(EmailExtension::taskFailure($to, $subject, $callback));
284
    }
285
286
    /**
287
     * Prevent task from running if still running from previous run.
288
     *
289
     * @param int $ttl Maximum expected lock duration in seconds
290
     */
291 5
    final public function withoutOverlapping(int $ttl = WithoutOverlappingExtension::DEFAULT_TTL): self
292
    {
293 5
        return $this->addExtension(new WithoutOverlappingExtension($ttl));
294
    }
295
296
    /**
297
     * Restrict running of schedule to a single server.
298
     * Be sure to configure `zenstruck_schedule.single_server_handler`.
299
     *
300
     * @param int $ttl Maximum expected lock duration in seconds
301
     */
302 5
    final public function onSingleServer(int $ttl = SingleServerExtension::DEFAULT_TTL): self
303
    {
304 5
        return $this->addExtension(new SingleServerExtension($ttl));
305
    }
306
307
    /**
308
     * Only run between given times.
309
     *
310
     * @param string $startTime "HH:MM" (ie "09:00")
311
     * @param string $endTime   "HH:MM" (ie "14:30")
312
     * @param bool   $inclusive Whether to include the start and end time
313
     */
314 7
    final public function onlyBetween(string $startTime, string $endTime, bool $inclusive = true): self
315
    {
316 7
        return $this->addExtension(BetweenTimeExtension::whenWithin($startTime, $endTime, $inclusive));
317
    }
318
319
    /**
320
     * Skip when between given times.
321
     *
322
     * @param string $startTime "HH:MM" (ie "09:00")
323
     * @param string $endTime   "HH:MM" (ie "14:30")
324
     * @param bool   $inclusive Whether to include the start and end time
325
     */
326 7
    final public function unlessBetween(string $startTime, string $endTime, bool $inclusive = true): self
327
    {
328 7
        return $this->addExtension(BetweenTimeExtension::unlessWithin($startTime, $endTime, $inclusive));
329
    }
330
331
    /**
332
     * Set your own cron expression (ie "15 3 * * 1,4").
333
     */
334 73
    final public function cron(string $expression): self
335
    {
336 73
        $this->expression = $expression;
337
338 73
        return $this;
339
    }
340
341
    /**
342
     * Resets the expression to "* * * * *".
343
     */
344 2
    final public function everyMinute(): self
345
    {
346 2
        return $this->cron(self::DEFAULT_EXPRESSION);
347
    }
348
349
    /**
350
     * Resets the expression to "<*>/5 * * * *".
351
     */
352 1
    final public function everyFiveMinutes(): self
353
    {
354 1
        return $this->cron('*/5 * * * *');
355
    }
356
357
    /**
358
     * Resets the expression to "<*>/10 * * * *".
359
     */
360 1
    final public function everyTenMinutes(): self
361
    {
362 1
        return $this->cron('*/10 * * * *');
363
    }
364
365
    /**
366
     * Resets the expression to "<*>/15 * * * *".
367
     */
368 1
    final public function everyFifteenMinutes(): self
369
    {
370 1
        return $this->cron('*/15 * * * *');
371
    }
372
373
    /**
374
     * Resets the expression to "<*>/20 * * * *".
375
     */
376 1
    final public function everyTwentyMinutes(): self
377
    {
378 1
        return $this->cron('*/20 * * * *');
379
    }
380
381
    /**
382
     * Resets the expression to "0,30 * * * *".
383
     */
384 1
    final public function everyThirtyMinutes(): self
385
    {
386 1
        return $this->cron('0,30 * * * *');
387
    }
388
389
    /**
390
     * Resets the expression to "0 * * * *".
391
     */
392 3
    final public function hourly(): self
393
    {
394 3
        return $this->cron('0 * * * *');
395
    }
396
397
    /**
398
     * Resets the expression to "X * * * *" with X being the passed minute(s).
399
     *
400
     * @param int|string $minute     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
401
     * @param int|string ...$minutes Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (20/2)
402
     */
403 2
    final public function hourlyAt($minute, ...$minutes): self
404
    {
405 2
        return $this->hourly()->minutes($minute, ...$minutes);
406
    }
407
408
    /**
409
     * Resets the expression to "0 0 * * *".
410
     */
411 9
    final public function daily(): self
412
    {
413 9
        return $this->cron('0 0 * * *');
414
    }
415
416
    /**
417
     * Resets the expression to "0 X * * *" with X being the passed hour(s).
418
     *
419
     * @param int|string $hour     Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
420
     * @param int|string ...$hours Single value (ie 1), multiple values (ie 1,3), range (ie 1-3), or step values (1-5/2)
421
     */
422 3
    final public function dailyOn($hour, ...$hours): self
423
    {
424 3
        return $this->daily()->hours($hour, ...$hours);
425
    }
426
427
    /**
428
     * Resets the expression to "0 X-Y * * *" with X and Y being the passed start and end hours.
429
     *
430
     * @param int $firstHour  0-23
431
     * @param int $secondHour 0-23
432
     */
433 1
    final public function dailyBetween(int $firstHour, int $secondHour): self
434
    {
435 1
        return $this->daily()->hours("{$firstHour}-{$secondHour}");
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 34
    final public function minutes($minute, ...$minutes): self
531
    {
532 34
        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 12
    final public function hours($hour, ...$hours): self
542
    {
543 12
        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 17
    final public function daysOfWeek($day, ...$days): self
575
    {
576 17
        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 6
    final public function mondays(): self
599
    {
600 6
        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 98
    private function getTimezoneValue(): ?string
686
    {
687 98
        return $this->getTimezone() ? $this->getTimezone()->getName() : null;
688
    }
689
}
690