Passed
Push — master ( 76636e...a7cce5 )
by Kevin
03:12
created

Schedule   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Test Coverage

Coverage 98%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 46
eloc 81
c 2
b 0
f 0
dl 0
loc 351
ccs 98
cts 100
cp 0.98
rs 8.72

29 Methods

Rating   Name   Duplication   Size   Complexity  
A then() 0 3 1
A timezone() 0 11 2
A environments() 0 3 1
A thenPing() 0 3 1
A all() 0 17 5
A addProcess() 0 3 1
A pingOnFailure() 0 3 1
A getId() 0 10 1
A pingBefore() 0 3 1
A skip() 0 9 3
A addCallback() 0 3 1
A getTask() 0 18 4
A when() 0 9 3
A getTimezone() 0 3 1
A pingAfter() 0 3 1
A onSuccess() 0 3 1
A onFailure() 0 3 1
A filter() 0 3 1
A emailOnFailure() 0 3 1
A pingOnSuccess() 0 3 1
A before() 0 3 1
A resetCache() 0 3 1
A after() 0 3 1
A onSingleServer() 0 3 1
A add() 0 5 1
A taskIterator() 0 10 3
A addCompound() 0 3 1
A due() 0 15 4
A addCommand() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Schedule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schedule, and based on these observations, apply Extract Interface, too.

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