Passed
Push — master ( c11449...8bc349 )
by Kevin
02:01
created

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