Passed
Push — master ( 9213a8...e174b6 )
by Kevin
02:14
created

Schedule::getTask()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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