Passed
Push — master ( 0675d7...59fbae )
by Kevin
02:33
created

Schedule::addCommand()   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 2
crap 1
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
39 3
    public function getId(): string
40
    {
41 3
        $tasks = \array_map(
42
            function(Task $task) {
43 2
                return $task->getId();
44 3
            },
45 3
            $this->all()
46
        );
47
48 3
        return \sha1(\implode('', $tasks));
49
    }
50
51 94
    public function add(Task $task): Task
52
    {
53 94
        $this->resetCache();
54
55 94
        return $this->tasks[] = $task;
56
    }
57
58
    /**
59
     * @see CommandTask::__construct()
60
     */
61 11
    public function addCommand(string $name, string ...$arguments): CommandTask
62
    {
63 11
        return $this->add(new CommandTask($name, ...$arguments));
64
    }
65
66
    /**
67
     * @see CallbackTask::__construct()
68
     */
69 2
    public function addCallback(callable $callback): CallbackTask
70
    {
71 2
        return $this->add(new CallbackTask($callback));
72
    }
73
74
    /**
75
     * @see ProcessTask::__construct()
76
     */
77 2
    public function addProcess($process): ProcessTask
78
    {
79 2
        return $this->add(new ProcessTask($process));
80
    }
81
82
    /**
83
     * @see PingTask::__construct()
84
     */
85 1
    public function addPing(string $url, string $method = 'GET', array $options = []): PingTask
86
    {
87 1
        return $this->add(new PingTask($url, $method, $options));
88
    }
89
90
    /**
91
     * @see MessageTask::__construct()
92
     */
93 1
    public function addMessage(object $message, array $stamps = []): MessageTask
94
    {
95 1
        return $this->add(new MessageTask($message, $stamps));
96
    }
97
98 2
    public function addCompound(): CompoundTask
99
    {
100 2
        return $this->add(new CompoundTask());
101
    }
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 12
    public function filter(callable $callback): self
109
    {
110 12
        return $this->addExtension(CallbackExtension::scheduleFilter($callback));
111
    }
112
113
    /**
114
     * Only run schedule if true.
115
     *
116
     * @param bool|callable $callback bool: skip if false, callable: skip if return value is false
117
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
118
     */
119 4
    public function when(string $description, $callback): self
120
    {
121
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
122 2
            return (bool) $callback;
123 4
        };
124
125
        return $this->filter(function(ScheduleRunContext $context) use ($callback, $description) {
126 4
            if (!$callback($context)) {
127 2
                throw new SkipSchedule($description);
128
            }
129 4
        });
130
    }
131
132
    /**
133
     * Skip schedule if true.
134
     *
135
     * @param bool|callable $callback bool: skip if true, callable: skip if return value is true
136
     *                                callable receives an instance of \Zenstruck\ScheduleBundle\Schedule\ScheduleRunContext
137
     */
138 5
    public function skip(string $description, $callback): self
139
    {
140
        $callback = \is_callable($callback) ? $callback : function() use ($callback) {
141 3
            return (bool) $callback;
142 5
        };
143
144
        return $this->filter(function(ScheduleRunContext $context) use ($callback, $description) {
145 5
            if ($callback($context)) {
146 3
                throw new SkipSchedule($description);
147
            }
148 5
        });
149
    }
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 3
    public function before(callable $callback): self
157
    {
158 3
        return $this->addExtension(CallbackExtension::scheduleBefore($callback));
159
    }
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
     */
166 3
    public function after(callable $callback): self
167
    {
168 3
        return $this->addExtension(CallbackExtension::scheduleAfter($callback));
169
    }
170
171
    /**
172
     * Alias for after().
173
     */
174 3
    public function then(callable $callback): self
175
    {
176 3
        return $this->after($callback);
177
    }
178
179
    /**
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 3
    public function onSuccess(callable $callback): self
187
    {
188 3
        return $this->addExtension(CallbackExtension::scheduleSuccess($callback));
189
    }
190
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 3
    public function onFailure(callable $callback): self
198
    {
199 3
        return $this->addExtension(CallbackExtension::scheduleFailure($callback));
200
    }
201
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 2
    public function pingBefore(string $url, string $method = 'GET', array $options = []): self
209
    {
210 2
        return $this->addExtension(PingExtension::scheduleBefore($url, $method, $options));
211
    }
212
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 2
    public function pingAfter(string $url, string $method = 'GET', array $options = []): self
220
    {
221 2
        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
        return $this->pingAfter($url, $method, $options);
230
    }
231
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 2
    public function pingOnSuccess(string $url, string $method = 'GET', array $options = []): self
239
    {
240 2
        return $this->addExtension(PingExtension::scheduleSuccess($url, $method, $options));
241
    }
242
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 2
    public function pingOnFailure(string $url, string $method = 'GET', array $options = []): self
250
    {
251 2
        return $this->addExtension(PingExtension::scheduleFailure($url, $method, $options));
252
    }
253
254
    /**
255
     * 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 7
    public function emailOnFailure($to = null, ?string $subject = null, ?callable $callback = null): self
263
    {
264 7
        return $this->addExtension(EmailExtension::scheduleFailure($to, $subject, $callback));
265
    }
266
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 2
    public function onSingleServer(int $ttl = SingleServerExtension::DEFAULT_TTL): self
274
    {
275 2
        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 1
    public function environments(string $environment, string ...$environments): self
283
    {
284 1
        return $this->addExtension(new EnvironmentExtension(\array_merge([$environment], $environments)));
285
    }
286
287
    /**
288
     * The default timezone for tasks (tasks can override).
289
     *
290
     * @param string|\DateTimeZone $value
291
     */
292 2
    public function timezone($value): self
293
    {
294 2
        $this->resetCache();
295
296 2
        if (!$value instanceof \DateTimeZone) {
297 2
            $value = new \DateTimeZone($value);
298
        }
299
300 2
        $this->timezone = $value;
301
302 2
        return $this;
303
    }
304
305 93
    public function getTimezone(): ?\DateTimeZone
306
    {
307 93
        return $this->timezone;
308
    }
309
310 5
    public function getTask(string $id): Task
311
    {
312
        // check for duplicated task ids
313 5
        $tasks = [];
314
315 5
        foreach ($this->all() as $task) {
316 5
            $tasks[$task->getId()][] = $task;
317
        }
318
319 5
        if (!\array_key_exists($id, $tasks)) {
320 1
            throw new \InvalidArgumentException("Task with ID \"{$id}\" not found.");
321
        }
322
323 4
        if (1 !== $count = \count($tasks[$id])) {
324 1
            throw new \RuntimeException(\sprintf('Task ID "%s" is ambiguous, there are %d tasks this id.', $id, $count));
325
        }
326
327 3
        return $tasks[$id][0];
328
    }
329
330
    /**
331
     * @return Task[]
332
     */
333 114
    public function all(): array
334
    {
335 114
        if (null !== $this->allTasks) {
336 39
            return $this->allTasks;
337
        }
338
339 114
        $this->allTasks = [];
340
341 114
        foreach ($this->taskIterator() as $task) {
342 93
            if ($this->getTimezone() && !$task->getTimezone()) {
343 2
                $task->timezone($this->getTimezone());
344
            }
345
346 93
            $this->allTasks[] = $task;
347
        }
348
349 114
        return $this->allTasks;
350
    }
351
352
    /**
353
     * @return Task[]
354
     */
355 87
    public function due(\DateTimeInterface $timestamp): array
356
    {
357 87
        if (null !== $this->dueTasks) {
358 2
            return $this->dueTasks;
359
        }
360
361 87
        $this->dueTasks = [];
362
363 87
        foreach ($this->all() as $task) {
364 67
            if ($task->isDue($timestamp)) {
365 67
                $this->dueTasks[] = $task;
366
            }
367
        }
368
369 87
        return $this->dueTasks;
370
    }
371
372
    /**
373
     * @return Task[]
374
     */
375 114
    private function taskIterator(): iterable
376
    {
377 114
        foreach ($this->tasks as $task) {
378 93
            if ($task instanceof CompoundTask) {
379 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...
380
381 5
                continue;
382
            }
383
384 91
            yield $task;
385
        }
386 114
    }
387
388 94
    private function resetCache(): void
389
    {
390 94
        $this->allTasks = $this->dueTasks = null;
391 94
    }
392
}
393