Passed
Push — master ( 4d5f71...6b73cf )
by Kevin
02:12
created

Task::spliceIntoPosition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
namespace Zenstruck\ScheduleBundle\Schedule;
4
5
use Zenstruck\ScheduleBundle\Event\BeforeTaskEvent;
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
14
/**
15
 * @author Taylor Otwell <[email protected]>
16
 * @author Kevin Bond <[email protected]>
17
 */
18
abstract class Task
19
{
20
    private const DEFAULT_EXPRESSION = '* * * * *';
21
22
    private $description;
23
    private $expression = self::DEFAULT_EXPRESSION;
24
    private $timezone;
25
26
    /** @var Extension[] */
27
    private $extensions = [];
28
29 149
    public function __construct(string $description)
30
    {
31 149
        $this->description = $description;
32 149
    }
33
34 28
    final public function __toString(): string
35
    {
36 28
        return $this->getDescription();
37
    }
38
39 25
    public function getType(): string
40
    {
41 25
        return (new \ReflectionClass($this))->getShortName();
42
    }
43
44 7
    final public function getId(): string
45
    {
46 7
        return \sha1(\get_class($this).$this->getExpression().$this->getDescription());
47
    }
48
49 97
    final public function getDescription(): string
50
    {
51 97
        return $this->description;
52
    }
53
54 89
    final public function getExpression(): CronExpression
55
    {
56 89
        return new CronExpression($this->expression, $this->getDescription());
57
    }
58
59 95
    final public function getTimezone(): ?\DateTimeZone
60
    {
61 95
        return $this->timezone;
62
    }
63
64 44
    final public function getNextRun(): \DateTimeInterface
65
    {
66 44
        return $this->getExpression()->getNextRun($this->getTimezoneValue());
67
    }
68
69 32
    final public function isDue(): bool
70
    {
71 32
        return $this->getExpression()->isDue($this->getTimezoneValue());
72
    }
73
74
    /**
75
     * @return Extension[]
76
     */
77 66
    final public function getExtensions(): array
78
    {
79 66
        return $this->extensions;
80
    }
81
82 47
    final public function addExtension(Extension $extension): self
83
    {
84 47
        $this->extensions[] = $extension;
85
86 47
        return $this;
87
    }
88
89
    /**
90
     * Set a unique description for this task.
91
     */
92 9
    final public function description(string $description): self
93
    {
94 9
        $this->description = $description;
95
96 9
        return $this;
97
    }
98
99
    /**
100
     * The timezone this task should run in.
101
     *
102
     * @param string|\DateTimeZone $value
103
     */
104 5
    final public function timezone($value): self
105
    {
106 5
        if (!$value instanceof \DateTimeZone) {
107 5
            $value = new \DateTimeZone($value);
108
        }
109
110 5
        $this->timezone = $value;
111
112 5
        return $this;
113
    }
114
115
    /**
116
     * Prevent task from running if callback throws \Zenstruck\ScheduleBundle\Schedule\Exception\SkipTask.
117
     *
118
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
119
     */
120 12
    final public function filter(callable $callback): self
121
    {
122 12
        return $this->addExtension(CallbackExtension::taskFilter($callback));
123
    }
124
125
    /**
126
     * Only run task if true.
127
     *
128
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
129
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
130
     */
131 4
    final public function when(string $description, $callback): self
132
    {
133
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
134 2
            return (bool) $callback;
135 4
        };
136
137
        return $this->filter(function (BeforeTaskEvent $event) use ($callback, $description) {
138 4
            if (!$callback($event)) {
139 2
                throw new SkipTask($description);
140
            }
141 4
        });
142
    }
143
144
    /**
145
     * Skip task if true.
146
     *
147
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
148
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
149
     */
150 5
    final public function skip(string $description, $callback): self
151
    {
152
        $callback = \is_callable($callback) ? $callback : function () use ($callback) {
153 3
            return (bool) $callback;
154 5
        };
155
156
        return $this->filter(function (BeforeTaskEvent $event) use ($callback, $description) {
157 5
            if ($callback($event)) {
158 3
                throw new SkipTask($description);
159
            }
160 5
        });
161
    }
162
163
    /**
164
     * Execute callback before task runs.
165
     *
166
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\BeforeTaskEvent
167
     */
168 3
    final public function before(callable $callback): self
169
    {
170 3
        return $this->addExtension(CallbackExtension::taskBefore($callback));
171
    }
172
173
    /**
174
     * Execute callback after task has run (will not execute if skipped).
175
     *
176
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
177
     */
178 3
    final public function after(callable $callback): self
179
    {
180 3
        return $this->addExtension(CallbackExtension::taskAfter($callback));
181
    }
182
183
    /**
184
     * Alias for after().
185
     */
186 1
    final public function then(callable $callback): self
187
    {
188 1
        return $this->after($callback);
189
    }
190
191
    /**
192
     * Execute callback if task was successful (will not execute if skipped).
193
     *
194
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
195
     */
196 3
    final public function onSuccess(callable $callback): self
197
    {
198 3
        return $this->addExtension(CallbackExtension::taskSuccess($callback));
199
    }
200
201
    /**
202
     * Execute callback if task failed (will not execute if skipped).
203
     *
204
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Event\AfterTaskEvent
205
     */
206 3
    final public function onFailure(callable $callback): self
207
    {
208 3
        return $this->addExtension(CallbackExtension::taskFailure($callback));
209
    }
210
211
    /**
212
     * Ping a webhook before task runs (will not ping if task was skipped).
213
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
214
     *
215
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
216
     */
217 4
    final public function pingBefore(string $url, string $method = 'GET', array $options = []): self
218
    {
219 4
        return $this->addExtension(PingExtension::taskBefore($url, $method, $options));
220
    }
221
222
    /**
223
     * Ping a webhook after task has run (will not ping if task was skipped).
224
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
225
     *
226
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
227
     */
228 4
    final public function pingAfter(string $url, string $method = 'GET', array $options = []): self
229
    {
230 4
        return $this->addExtension(PingExtension::taskAfter($url, $method, $options));
231
    }
232
233
    /**
234
     * Alias for pingAfter().
235
     */
236 1
    final public function thenPing(string $url, string $method = 'GET', array $options = []): self
237
    {
238 1
        return $this->pingAfter($url, $method, $options);
239
    }
240
241
    /**
242
     * Ping a webhook if task was successful (will not ping if task was skipped).
243
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
244
     *
245
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
246
     */
247 4
    final public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
248
    {
249 4
        return $this->addExtension(PingExtension::taskSuccess($url, $method, $options));
250
    }
251
252
    /**
253
     * Ping a webhook if task failed (will not ping if task was skipped).
254
     * If you want to control the HttpClientInterface used, configure `zenstruck_schedule.ping_handler`.
255
     *
256
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
257
     */
258 6
    final public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
259
    {
260 6
        return $this->addExtension(PingExtension::taskFailure($url, $method, $options));
261
    }
262
263
    /**
264
     * Email task detail after run (on success or failure, not if skipped).
265
     * Be sure to configure `zenstruck_schedule.email_handler`.
266
     *
267
     * @param string|string[] $to       Email address(es)
268
     * @param callable|null   $callback Add your own headers etc
269
     *                                  Receives an instance of \Symfony\Component\Mime\Email
270
     */
271 6
    final public function emailAfter($to = null, string $subject = null, callable $callback = null): self
272
    {
273 6
        return $this->addExtension(EmailExtension::taskAfter($to, $subject, $callback));
274
    }
275
276
    /**
277
     * Alias for emailAfter().
278
     */
279 1
    final public function thenEmail($to = null, string $subject = null, callable $callback = null): self
280
    {
281 1
        return $this->emailAfter($to, $subject, $callback);
282
    }
283
284
    /**
285
     * Email task/failure details if failed (not if skipped).
286
     * Be sure to configure `zenstruck_schedule.email_handler`.
287
     *
288
     * @param string|string[] $to       Email address(es)
289
     * @param callable|null   $callback Add your own headers etc
290
     *                                  Receives an instance of \Symfony\Component\Mime\Email
291
     */
292 8
    final public function emailOnFailure($to = null, string $subject = null, callable $callback = null): self
293
    {
294 8
        return $this->addExtension(EmailExtension::taskFailure($to, $subject, $callback));
295
    }
296
297
    /**
298
     * Prevent task from running if still running from previous run.
299
     *
300
     * @param int $ttl Maximum expected lock duration in seconds
301
     */
302 5
    final public function withoutOverlapping(int $ttl = WithoutOverlappingExtension::DEFAULT_TTL): self
303
    {
304 5
        return $this->addExtension(new WithoutOverlappingExtension($ttl));
305
    }
306
307
    /**
308
     * Restrict running of schedule to a single server.
309
     * Be sure to configure `zenstruck_schedule.single_server_handler`.
310
     *
311
     * @param int $ttl Maximum expected lock duration in seconds
312
     */
313 3
    final public function onSingleServer(int $ttl = SingleServerExtension::DEFAULT_TTL): self
314
    {
315 3
        return $this->addExtension(new SingleServerExtension($ttl));
316
    }
317
318
    /**
319
     * Only run between given times.
320
     *
321
     * @param string $startTime "HH:MM" (ie "09:00")
322
     * @param string $endTime   "HH:MM" (ie "14:30")
323
     * @param bool   $inclusive Whether to include the start and end time
324
     */
325 7
    final public function between(string $startTime, string $endTime, bool $inclusive = true): self
326
    {
327 7
        return $this->addExtension(BetweenTimeExtension::whenWithin($startTime, $endTime, $inclusive));
328
    }
329
330
    /**
331
     * Skip when between given times.
332
     *
333
     * @param string $startTime "HH:MM" (ie "09:00")
334
     * @param string $endTime   "HH:MM" (ie "14:30")
335
     * @param bool   $inclusive Whether to include the start and end time
336
     */
337 7
    final public function unlessBetween(string $startTime, string $endTime, bool $inclusive = true): self
338
    {
339 7
        return $this->addExtension(BetweenTimeExtension::unlessWithin($startTime, $endTime, $inclusive));
340
    }
341
342
    /**
343
     * Set your own cron expression (ie "15 3 * * 1,4").
344
     */
345 54
    final public function cron(string $expression): self
346
    {
347 54
        $this->expression = $expression;
348
349 54
        return $this;
350
    }
351
352 2
    final public function everyMinute(): self
353
    {
354 2
        return $this->spliceIntoPosition(1, '*');
355
    }
356
357 1
    final public function everyFiveMinutes(): self
358
    {
359 1
        return $this->spliceIntoPosition(1, '*/5');
360
    }
361
362 1
    final public function everyTenMinutes(): self
363
    {
364 1
        return $this->spliceIntoPosition(1, '*/10');
365
    }
366
367 1
    final public function everyFifteenMinutes(): self
368
    {
369 1
        return $this->spliceIntoPosition(1, '*/15');
370
    }
371
372 1
    final public function everyThirtyMinutes(): self
373
    {
374 1
        return $this->spliceIntoPosition(1, '0,30');
375
    }
376
377 1
    final public function hourly(): self
378
    {
379 1
        return $this->spliceIntoPosition(1, 0);
380
    }
381
382
    /**
383
     * @param int $minute 0-59
384
     */
385 1
    final public function hourlyAt(int $minute): self
386
    {
387 1
        return $this->spliceIntoPosition(1, $minute);
388
    }
389
390 3
    final public function daily(): self
391
    {
392
        return $this
393 3
            ->spliceIntoPosition(1, 0)
394 3
            ->spliceIntoPosition(2, 0)
395
        ;
396
    }
397
398
    /**
399
     * @param string $time "HH:MM" (ie "14:30")
400
     */
401 11
    final public function at(string $time): self
402
    {
403 11
        $segments = \explode(':', $time);
404
405
        return $this
406 11
            ->spliceIntoPosition(2, (int) $segments[0])
407 11
            ->spliceIntoPosition(1, 2 === \count($segments) ? (int) $segments[1] : '0')
408
        ;
409
    }
410
411
    /**
412
     * Alias for at().
413
     */
414 3
    final public function dailyAt(string $time): self
415
    {
416 3
        return $this->at($time);
417
    }
418
419
    /**
420
     * @param int $firstHour  0-23
421
     * @param int $secondHour 0-23
422
     */
423 2
    final public function twiceDaily(int $firstHour = 1, int $secondHour = 13): self
424
    {
425
        return $this
426 2
            ->spliceIntoPosition(1, 0)
427 2
            ->spliceIntoPosition(2, $firstHour.','.$secondHour)
428
        ;
429
    }
430
431 2
    final public function weekdays(): self
432
    {
433 2
        return $this->spliceIntoPosition(5, '1-5');
434
    }
435
436 1
    final public function weekends(): self
437
    {
438 1
        return $this->spliceIntoPosition(5, '0,6');
439
    }
440
441
    /**
442
     * 0 = Sunday, 6 = Saturday.
443
     */
444 13
    final public function weeklyOn(int $day, int ...$days): self
445
    {
446 13
        return $this->spliceIntoPosition(5, \implode(',', \array_merge([$day], $days)));
447
    }
448
449 6
    final public function mondays(): self
450
    {
451 6
        return $this->weeklyOn(1);
452
    }
453
454 2
    final public function tuesdays(): self
455
    {
456 2
        return $this->weeklyOn(2);
457
    }
458
459 1
    final public function wednesdays(): self
460
    {
461 1
        return $this->weeklyOn(3);
462
    }
463
464 1
    final public function thursdays(): self
465
    {
466 1
        return $this->weeklyOn(4);
467
    }
468
469 1
    final public function fridays(): self
470
    {
471 1
        return $this->weeklyOn(5);
472
    }
473
474 1
    final public function saturdays(): self
475
    {
476 1
        return $this->weeklyOn(6);
477
    }
478
479 3
    final public function sundays(): self
480
    {
481 3
        return $this->weeklyOn(0);
482
    }
483
484 2
    final public function weekly(): self
485
    {
486
        return $this
487 2
            ->spliceIntoPosition(1, 0)
488 2
            ->spliceIntoPosition(2, 0)
489 2
            ->spliceIntoPosition(5, 0)
490
        ;
491
    }
492
493 1
    final public function monthly(): self
494
    {
495
        return $this
496 1
            ->spliceIntoPosition(1, 0)
497 1
            ->spliceIntoPosition(2, 0)
498 1
            ->spliceIntoPosition(3, 1)
499
        ;
500
    }
501
502
    /**
503
     * @param int    $day  1-31
504
     * @param string $time "HH:MM" (ie "14:30")
505
     */
506 2
    final public function monthlyOn(int $day, string $time = '0:0'): self
507
    {
508
        return $this
509 2
            ->dailyAt($time)
510 2
            ->spliceIntoPosition(3, $day)
511
        ;
512
    }
513
514
    /**
515
     * @param int $firstDay  1-31
516
     * @param int $secondDay 1-31
517
     */
518 3
    final public function twiceMonthly(int $firstDay = 1, int $secondDay = 16): self
519
    {
520
        return $this
521 3
            ->spliceIntoPosition(1, 0)
522 3
            ->spliceIntoPosition(2, 0)
523 3
            ->spliceIntoPosition(3, $firstDay.','.$secondDay)
524
        ;
525
    }
526
527 1
    final public function quarterly(): self
528
    {
529
        return $this
530 1
            ->spliceIntoPosition(1, 0)
531 1
            ->spliceIntoPosition(2, 0)
532 1
            ->spliceIntoPosition(3, 1)
533 1
            ->spliceIntoPosition(4, '1-12/3')
534
        ;
535
    }
536
537 2
    final public function yearly(): self
538
    {
539
        return $this
540 2
            ->spliceIntoPosition(1, 0)
541 2
            ->spliceIntoPosition(2, 0)
542 2
            ->spliceIntoPosition(3, 1)
543 2
            ->spliceIntoPosition(4, 1)
544
        ;
545
    }
546
547 43
    private function spliceIntoPosition(int $position, string $value): self
548
    {
549 43
        $segments = \explode(' ', $this->expression);
550
551 43
        if (5 !== \count($segments)) { // reset if set to alias or invalid
552 1
            $this->expression = self::DEFAULT_EXPRESSION;
553
554 1
            return $this->spliceIntoPosition($position, $value);
555
        }
556
557 43
        $segments[$position - 1] = $value;
558
559 43
        return $this->cron(\implode(' ', $segments));
560
    }
561
562 76
    private function getTimezoneValue(): ?string
563
    {
564 76
        return $this->getTimezone() ? $this->getTimezone()->getName() : null;
565
    }
566
}
567