Passed
Push — master ( 8ecb46...0675d7 )
by Kevin
03:22 queued 22s
created

Schedule   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Test Coverage

Coverage 98.04%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 47
eloc 82
c 2
b 0
f 0
dl 0
loc 359
ccs 100
cts 102
cp 0.9804
rs 8.64

30 Methods

Rating   Name   Duplication   Size   Complexity  
A then() 0 3 1
A timezone() 0 11 2
A pingOnFailure() 0 3 1
A pingBefore() 0 3 1
A skip() 0 9 3
A when() 0 9 3
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 after() 0 3 1
A onSingleServer() 0 3 1
A addProcess() 0 3 1
A getId() 0 10 1
A addCallback() 0 3 1
A add() 0 5 1
A addCommand() 0 3 1
A environments() 0 3 1
A thenPing() 0 3 1
A all() 0 17 5
A getTask() 0 18 4
A getTimezone() 0 3 1
A resetCache() 0 3 1
A addPing() 0 3 1
A taskIterator() 0 10 3
A addCompound() 0 3 1
A due() 0 15 4

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