Schedule::addProcess()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

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