Passed
Pull Request — master (#15)
by Kevin
17:16
created

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