Task::weekdays()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Zenstruck\ScheduleBundle\Schedule;
4
5
use Symfony\Component\Mime\Address;
6
use Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask;
7
use Zenstruck\ScheduleBundle\Schedule\Extension\BetweenTimeExtension;
8
use Zenstruck\ScheduleBundle\Schedule\Extension\CallbackExtension;
9
use Zenstruck\ScheduleBundle\Schedule\Extension\EmailExtension;
10
use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
11
use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
12
use Zenstruck\ScheduleBundle\Schedule\Extension\WithoutOverlappingExtension;
13
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
    /** @var string */
32
    private $description;
33
34 195
    /** @var string */
35
    private $expression = self::DEFAULT_EXPRESSION;
36 195
37 195
    /** @var \DateTimeZone|null */
38
    private $timezone;
39 27
40
    public function __construct(string $description)
41 27
    {
42
        $this->description = $description;
43
    }
44 37
45
    final public function __toString(): string
46 37
    {
47
        return "{$this->getType()}: {$this->getDescription()}";
48
    }
49 81
50
    public function getType(): string
51 81
    {
52
        return (new \ReflectionClass($this))->getShortName();
53
    }
54 160
55
    final public function getId(): string
56 160
    {
57
        return \sha1(static::class.$this->getExpression().$this->getDescription());
58
    }
59 2
60
    final public function getDescription(): string
61 2
    {
62
        return $this->description;
63
    }
64 149
65
    /**
66 149
     * @return array<string,string>
67
     */
68
    public function getContext(): array
69 138
    {
70
        return [];
71 138
    }
72
73
    final public function getExpression(): CronExpression
74 63
    {
75
        return new CronExpression($this->expression, $this->getDescription());
76 63
    }
77
78
    final public function getTimezone(): ?\DateTimeZone
79 68
    {
80
        return $this->timezone;
81 68
    }
82
83
    final public function getNextRun(): \DateTimeInterface
84
    {
85
        return $this->getExpression()->getNextRun($this->getTimezoneValue());
86
    }
87 9
88
    final public function isDue(\DateTimeInterface $timestamp): bool
89 9
    {
90
        return $this->getExpression()->isDue($timestamp, $this->getTimezoneValue());
91 9
    }
92
93
    /**
94
     * Set a unique description for this task.
95
     */
96
    final public function description(string $description): self
97
    {
98
        $this->description = $description;
99 5
100
        return $this;
101 5
    }
102 5
103
    /**
104
     * The timezone this task should run in.
105 5
     *
106
     * @param string|\DateTimeZone $value
107 5
     */
108
    final public function timezone($value): self
109
    {
110
        if (!$value instanceof \DateTimeZone) {
111
            $value = new \DateTimeZone($value);
112
        }
113
114
        $this->timezone = $value;
115 12
116
        return $this;
117 12
    }
118
119
    /**
120
     * Prevent task from running if callback throws \Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask.
121
     *
122
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
123
     */
124
    final public function filter(callable $callback): self
125
    {
126 4
        return $this->addExtension(CallbackExtension::taskFilter($callback));
127
    }
128
129 2
    /**
130 4
     * Only run task if true.
131
     *
132
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
133 4
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
134 2
     */
135
    final public function when(string $description, $callback): self
136 4
    {
137
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
138
            return (bool) $callback;
139
        };
140
141
        return $this->filter(function(TaskRunContext $context) use ($callback, $description) {
142
            if (!$callback($context)) {
143
                throw new SkipTask($description);
144
            }
145 5
        });
146
    }
147
148 3
    /**
149 5
     * Skip task if true.
150
     *
151
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
152 5
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
153 3
     */
154
    final public function skip(string $description, $callback): self
155 5
    {
156
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
157
            return (bool) $callback;
158
        };
159
160
        return $this->filter(function(TaskRunContext $context) use ($callback, $description) {
161
            if ($callback($context)) {
162
                throw new SkipTask($description);
163 3
            }
164
        });
165 3
    }
166
167
    /**
168
     * Execute callback before task runs.
169
     *
170
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
171
     */
172
    final public function before(callable $callback): self
173 3
    {
174
        return $this->addExtension(CallbackExtension::taskBefore($callback));
175 3
    }
176
177
    /**
178
     * Execute callback after task has run (will not execute if skipped).
179
     *
180
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
181 3
     */
182
    final public function after(callable $callback): self
183 3
    {
184
        return $this->addExtension(CallbackExtension::taskAfter($callback));
185
    }
186
187
    /**
188
     * Alias for after().
189
     */
190
    final public function then(callable $callback): self
191 3
    {
192
        return $this->after($callback);
193 3
    }
194
195
    /**
196
     * Execute callback if task was successful (will not execute if skipped).
197
     *
198
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
199
     */
200
    final public function onSuccess(callable $callback): self
201 3
    {
202
        return $this->addExtension(CallbackExtension::taskSuccess($callback));
203 3
    }
204
205
    /**
206
     * Execute callback if task failed (will not execute if skipped).
207
     *
208
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\Task\TaskRunContext
209
     */
210
    final public function onFailure(callable $callback): self
211
    {
212 3
        return $this->addExtension(CallbackExtension::taskFailure($callback));
213
    }
214 3
215
    /**
216
     * Ping a webhook before task runs (will not ping if task was skipped).
217
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
218
     *
219
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
220
     */
221
    final public function pingBefore(string $url, string $method = 'GET', array $options = []): self
222
    {
223 3
        return $this->addExtension(PingExtension::taskBefore($url, $method, $options));
224
    }
225 3
226
    /**
227
     * Ping a webhook after task has run (will not ping if task was skipped).
228
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
229
     *
230
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
231
     */
232
    final public function pingAfter(string $url, string $method = 'GET', array $options = []): self
233
    {
234
        return $this->addExtension(PingExtension::taskAfter($url, $method, $options));
235
    }
236
237
    /**
238
     * Alias for pingAfter().
239
     */
240
    final public function thenPing(string $url, string $method = 'GET', array $options = []): self
241
    {
242 3
        return $this->pingAfter($url, $method, $options);
243
    }
244 3
245
    /**
246
     * Ping a webhook if task was successful (will not ping if task was skipped).
247
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
248
     *
249
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
250
     */
251
    final public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
252
    {
253 5
        return $this->addExtension(PingExtension::taskSuccess($url, $method, $options));
254
    }
255 5
256
    /**
257
     * Ping a webhook if task failed (will not ping if task was skipped).
258
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
259
     *
260
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
261
     */
262
    final public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
263
    {
264
        return $this->addExtension(PingExtension::taskFailure($url, $method, $options));
265
    }
266 7
267
    /**
268 7
     * Email task detail after run (on success or failure, not if skipped).
269
     * Be sure to configure `zenstruck_schedule.mailer`.
270
     *
271
     * @param string|Address|string[]|Address[]|null $to       Email address(es)
272
     * @param callable|null                          $callback Add your own headers etc
273
     *                                                         Receives an instance of \Symfony\Component\Mime\Email
274 1
     */
275
    final public function emailAfter($to = null, ?string $subject = null, ?callable $callback = null): self
276 1
    {
277
        return $this->addExtension(EmailExtension::taskAfter($to, $subject, $callback));
278
    }
279
280
    /**
281
     * Alias for emailAfter().
282
     *
283
     * @param string|Address|string[]|Address[]|null $to Email address(es)
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|Address|string[]|Address[]|null $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 131
        if (5 !== \count($segments)) { // reset if set to alias or invalid
692
            $this->expression = self::DEFAULT_EXPRESSION;
693 131
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