Passed
Push — master ( c11449...8bc349 )
by Kevin
02:01
created

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