Passed
Push — master ( 76636e...a7cce5 )
by Kevin
03:12
created

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