Passed
Pull Request — master (#28)
by Kevin
18:45 queued 10s
created

Schedule::skip()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 9
ccs 1
cts 1
cp 1
rs 10
cc 3
nc 2
nop 2
crap 3
1
<?php
2
3
namespace Zenstruck\ScheduleBundle;
4
5
use Zenstruck\ScheduleBundle\Schedule\Exception\SkipSchedule;
6
use Zenstruck\ScheduleBundle\Schedule\Extension\CallbackExtension;
7
use Zenstruck\ScheduleBundle\Schedule\Extension\EmailExtension;
8
use Zenstruck\ScheduleBundle\Schedule\Extension\EnvironmentExtension;
9
use Zenstruck\ScheduleBundle\Schedule\Extension\PingExtension;
10
use Zenstruck\ScheduleBundle\Schedule\Extension\SingleServerExtension;
11
use Zenstruck\ScheduleBundle\Schedule\HasExtensions;
12
use Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext;
13
use Zenstruck\ScheduleBundle\Schedule\Task;
14
use Zenstruck\ScheduleBundle\Schedule\Task\CallbackTask;
15
use Zenstruck\ScheduleBundle\Schedule\Task\CommandTask;
16
use Zenstruck\ScheduleBundle\Schedule\Task\CompoundTask;
17
use Zenstruck\ScheduleBundle\Schedule\Task\MessageTask;
18
use Zenstruck\ScheduleBundle\Schedule\Task\PingTask;
19
use Zenstruck\ScheduleBundle\Schedule\Task\ProcessTask;
20
21
/**
22
 * @author Kevin Bond <[email protected]>
23
 */
24
final class Schedule
25
{
26
    use HasExtensions;
27
28
    public const FILTER = 'Filter Schedule';
29
    public const BEFORE = 'Before Schedule';
30
    public const AFTER = 'After Schedule';
31
    public const SUCCESS = 'On Schedule Success';
32
    public const FAILURE = 'On Schedule Failure';
33
34
    private $tasks = [];
35
    private $allTasks;
36
    private $dueTasks;
37
    private $timezone;
38 3
39
    public function getId(): string
40 3
    {
41
        $tasks = \array_map(
42 2
            function(Task $task) {
43 3
                return $task->getId();
44 3
            },
45
            $this->all()
46
        );
47 3
48
        return \sha1(\implode('', $tasks));
49
    }
50 87
51
    public function add(Task $task): Task
52 87
    {
53
        $this->resetCache();
54 87
55
        return $this->tasks[] = $task;
56
    }
57
58
    /**
59
     * @see CommandTask::__construct()
60 11
     */
61
    public function addCommand(string $name, string ...$arguments): CommandTask
62 11
    {
63
        return $this->add(new CommandTask($name, ...$arguments));
64
    }
65
66
    /**
67
     * @see CallbackTask::__construct()
68 2
     */
69
    public function addCallback(callable $callback): CallbackTask
70 2
    {
71
        return $this->add(new CallbackTask($callback));
72
    }
73
74
    /**
75
     * @see ProcessTask::__construct()
76 2
     */
77
    public function addProcess($process): ProcessTask
78 2
    {
79
        return $this->add(new ProcessTask($process));
80
    }
81
82
    /**
83
     * @see PingTask::__construct()
84 1
     */
85
    public function addPing(string $url, string $method = 'GET', array $options = []): PingTask
86 1
    {
87
        return $this->add(new PingTask($url, $method, $options));
88
    }
89 2
90
    /**
91 2
     * @see MessageTask::__construct()
92
     */
93
    public function addMessage(object $message, array $stamps = []): MessageTask
94
    {
95
        return $this->add(new MessageTask($message, $stamps));
96
    }
97
98
    public function addCompound(): CompoundTask
99 12
    {
100
        return $this->add(new CompoundTask());
101 12
    }
102
103
    /**
104
     * Prevent schedule from running if callback throws \Zenstruck\ScheduleBundle\Schedule\Exception\SkipSchedule.
105
     *
106
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
107
     */
108
    public function filter(callable $callback): self
109
    {
110 4
        return $this->addExtension(CallbackExtension::scheduleFilter($callback));
111
    }
112
113 2
    /**
114 4
     * Only run schedule if true.
115
     *
116
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
117 4
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
118 2
     */
119
    public function when(string $description, $callback): self
120 4
    {
121
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
122
            return (bool) $callback;
123
        };
124
125
        return $this->filter(function(ScheduleRunContext $context) use ($callback, $description) {
126
            if (!$callback($context)) {
127
                throw new SkipSchedule($description);
128
            }
129 5
        });
130
    }
131
132 3
    /**
133 5
     * Skip schedule if true.
134
     *
135
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
136 5
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
137 3
     */
138
    public function skip(string $description, $callback): self
139 5
    {
140
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
141
            return (bool) $callback;
142
        };
143
144
        return $this->filter(function(ScheduleRunContext $context) use ($callback, $description) {
145
            if ($callback($context)) {
146
                throw new SkipSchedule($description);
147 3
            }
148
        });
149 3
    }
150
151
    /**
152
     * Execute callback before tasks run (even if no tasks are due).
153
     *
154
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
155
     */
156
    public function before(callable $callback): self
157 3
    {
158
        return $this->addExtension(CallbackExtension::scheduleBefore($callback));
159 3
    }
160
161
    /**
162
     * Execute callback after tasks run (even if no tasks ran).
163
     *
164
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
165 3
     */
166
    public function after(callable $callback): self
167 3
    {
168
        return $this->addExtension(CallbackExtension::scheduleAfter($callback));
169
    }
170
171
    /**
172
     * Alias for after().
173
     */
174
    public function then(callable $callback): self
175
    {
176
        return $this->after($callback);
177 3
    }
178
179 3
    /**
180
     * Execute callback after tasks run if all tasks succeeded
181
     *  - even if no tasks ran
182
     *  - skipped tasks are considered successful.
183
     *
184
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
185
     */
186
    public function onSuccess(callable $callback): self
187
    {
188 3
        return $this->addExtension(CallbackExtension::scheduleSuccess($callback));
189
    }
190 3
191
    /**
192
     * Execute callback after tasks run if one or more tasks failed
193
     *  - skipped tasks are considered successful.
194
     *
195
     * @param callable $callback Receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
196
     */
197
    public function onFailure(callable $callback): self
198
    {
199 2
        return $this->addExtension(CallbackExtension::scheduleFailure($callback));
200
    }
201 2
202
    /**
203
     * Ping a webhook before any tasks run (even if none are due).
204
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
205
     *
206
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
207
     */
208
    public function pingBefore(string $url, string $method = 'GET', array $options = []): self
209
    {
210 2
        return $this->addExtension(PingExtension::scheduleBefore($url, $method, $options));
211
    }
212 2
213
    /**
214
     * Ping a webhook after tasks ran (even if none ran).
215
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
216
     *
217
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
218
     */
219
    public function pingAfter(string $url, string $method = 'GET', array $options = []): self
220
    {
221
        return $this->addExtension(PingExtension::scheduleAfter($url, $method, $options));
222
    }
223
224
    /**
225
     * Alias for pingAfter().
226
     */
227
    public function thenPing(string $url, string $method = 'GET', array $options = []): self
228
    {
229 2
        return $this->pingAfter($url, $method, $options);
230
    }
231 2
232
    /**
233
     * Ping a webhook after tasks run if all tasks succeeded (skipped tasks are considered successful).
234
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
235
     *
236
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
237
     */
238
    public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
239
    {
240 2
        return $this->addExtension(PingExtension::scheduleSuccess($url, $method, $options));
241
    }
242 2
243
    /**
244
     * Ping a webhook after tasks run if one or more tasks failed.
245
     * If you want to control the HttpClient used, configure `zenstruck_schedule.http_client`.
246
     *
247
     * @param array $options See HttpClientInterface::OPTIONS_DEFAULTS
248
     */
249
    public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
250
    {
251
        return $this->addExtension(PingExtension::scheduleFailure($url, $method, $options));
252
    }
253 7
254
    /**
255 7
     * Email failed task detail after tasks run if one or more tasks failed.
256
     * Be sure to configure `zenstruck_schedule.mailer`.
257
     *
258
     * @param string|string[] $to       Email address(es)
259
     * @param callable|null   $callback Add your own headers etc
260
     *                                  Receives an instance of \Symfony\Component\Mime\Email
261
     */
262
    public function emailOnFailure($to = null, ?string $subject = null, ?callable $callback = null): self
263
    {
264 2
        return $this->addExtension(EmailExtension::scheduleFailure($to, $subject, $callback));
265
    }
266 2
267
    /**
268
     * Restrict running of schedule to a single server.
269
     * Be sure to configure `zenstruck_schedule.single_server_lock_factory`.
270
     *
271
     * @param int $ttl Maximum expected lock duration in seconds
272
     */
273 1
    public function onSingleServer(int $ttl = SingleServerExtension::DEFAULT_TTL): self
274
    {
275 1
        return $this->addExtension(new SingleServerExtension($ttl));
276
    }
277
278
    /**
279
     * Define the application environment(s) you wish to run the schedule in. Trying to
280
     * run in another environment will skip the schedule.
281
     */
282
    public function environments(string $environment, string ...$environments): self
283 2
    {
284
        return $this->addExtension(new EnvironmentExtension(\array_merge([$environment], $environments)));
285 2
    }
286
287 2
    /**
288 2
     * The default timezone for tasks (tasks can override).
289
     *
290
     * @param string|\DateTimeZone $value
291 2
     */
292
    public function timezone($value): self
293 2
    {
294
        $this->resetCache();
295
296 86
        if (!$value instanceof \DateTimeZone) {
297
            $value = new \DateTimeZone($value);
298 86
        }
299
300
        $this->timezone = $value;
301 5
302
        return $this;
303
    }
304 5
305
    public function getTimezone(): ?\DateTimeZone
306 5
    {
307 5
        return $this->timezone;
308
    }
309
310 5
    public function getTask(string $id): Task
311 1
    {
312
        // check for duplicated task ids
313
        $tasks = [];
314 4
315 1
        foreach ($this->all() as $task) {
316
            $tasks[$task->getId()][] = $task;
317
        }
318 3
319
        if (!\array_key_exists($id, $tasks)) {
320
            throw new \InvalidArgumentException("Task with ID \"{$id}\" not found.");
321
        }
322
323
        if (1 !== $count = \count($tasks[$id])) {
324 107
            throw new \RuntimeException(\sprintf('Task ID "%s" is ambiguous, there are %d tasks this id.', $id, $count));
325
        }
326 107
327 39
        return $tasks[$id][0];
328
    }
329
330 107
    /**
331
     * @return Task[]
332 107
     */
333 86
    public function all(): array
334 2
    {
335
        if (null !== $this->allTasks) {
336
            return $this->allTasks;
337 86
        }
338
339
        $this->allTasks = [];
340 107
341
        foreach ($this->taskIterator() as $task) {
342
            if ($this->getTimezone() && !$task->getTimezone()) {
343
                $task->timezone($this->getTimezone());
344
            }
345
346 80
            $this->allTasks[] = $task;
347
        }
348 80
349 2
        return $this->allTasks;
350
    }
351
352 80
    /**
353
     * @return Task[]
354 80
     */
355 60
    public function due(\DateTimeInterface $timestamp): array
356 60
    {
357
        if (null !== $this->dueTasks) {
358
            return $this->dueTasks;
359
        }
360 80
361
        $this->dueTasks = [];
362
363
        foreach ($this->all() as $task) {
364
            if ($task->isDue($timestamp)) {
365
                $this->dueTasks[] = $task;
366 107
            }
367
        }
368 107
369 86
        return $this->dueTasks;
370 5
    }
371
372 5
    /**
373
     * @return Task[]
374
     */
375 84
    private function taskIterator(): iterable
376
    {
377 107
        foreach ($this->tasks as $task) {
378
            if ($task instanceof CompoundTask) {
379 87
                yield from $task;
0 ignored issues
show
Bug Best Practice introduced by
The expression YieldFromNode returns the type Generator which is incompatible with the documented return type Zenstruck\ScheduleBundle\Schedule\Task[].
Loading history...
380
381 87
                continue;
382 87
            }
383
384
            yield $task;
385
        }
386
    }
387
388
    private function resetCache(): void
389
    {
390
        $this->allTasks = $this->dueTasks = null;
391
    }
392
}
393