Completed
Push — master ( 09e1a4...b9c09d )
by René
02:57
created

Cron::parse()   D

Complexity

Conditions 31
Paths 30

Size

Total Lines 111
Code Lines 61

Duplication

Lines 6
Ratio 5.41 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 6
loc 111
rs 4.3983
cc 31
eloc 61
nc 30
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Cron expression parser and validator
5
 *
6
 * @author René Pollesch
7
 */
8
class Cron
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
9
{
10
    /**
11
     * Weekday look-up table
12
     *
13
     * @var array
14
     */
15
    protected static $weekdays = [
16
        'sun' => 0,
17
        'mon' => 1,
18
        'tue' => 2,
19
        'wed' => 3,
20
        'thu' => 4,
21
        'fri' => 5,
22
        'sat' => 6
23
    ];
24
25
    /**
26
     * Month name look-up table
27
     *
28
     * @var array
29
     */
30
    protected static $months = [
31
        'jan' => 1,
32
        'feb' => 2,
33
        'mar' => 3,
34
        'apr' => 4,
35
        'may' => 5,
36
        'jun' => 6,
37
        'jul' => 7,
38
        'aug' => 8,
39
        'sep' => 9,
40
        'oct' => 10,
41
        'nov' => 11,
42
        'dec' => 12
43
    ];
44
45
    /**
46
     * Cron expression
47
     *
48
     * @var string
49
     */
50
    protected $expression;
51
52
    /**
53
     * Time zone
54
     *
55
     * @var \DateTimeZone
56
     */
57
    protected $timeZone;
58
59
    /**
60
     * Matching register
61
     *
62
     * @var array|null
63
     */
64
    protected $registers;
65
66
    /**
67
     * Class constructor sets cron expression property
68
     *
69
     * @param string $expression cron expression
70
     * @param \DateTimeZone $timeZone
71
     */
72
    public function __construct($expression = '* * * * *', \DateTimeZone $timeZone = null)
73
    {
74
        $this->setExpression($expression);
75
        $this->setTimeZone($timeZone);
76
    }
77
78
    /**
79
     * Set expression
80
     *
81
     * @param string $expression
82
     * @return self
83
     */
84
    public function setExpression($expression)
85
    {
86
        $this->expression = trim((string)$expression);
87
        $this->registers = null;
88
89
        return $this;
90
    }
91
92
    /**
93
     * Set time zone
94
     *
95
     * @param \DateTimeZone $timeZone
96
     * @return self
97
     */
98
    public function setTimeZone(\DateTimeZone $timeZone = null)
99
    {
100
        $this->timeZone = $timeZone;
101
        return $this;
102
    }
103
104
    /**
105
     * Parse and validate cron expression
106
     *
107
     * @return bool true if expression is valid, or false on error
108
     */
109
    public function isValid()
110
    {
111
        $result = true;
112
113
        if ($this->registers === null) {
114
            try {
115
                $this->registers = $this->parse();
116
            } catch (\Exception $e) {
117
                $result = false;
118
            }
119
        }
120
121
        return $result;
122
    }
123
124
    /**
125
     * Match current or given date/time against cron expression
126
     *
127
     * @param mixed $dtime \DateTime object, timestamp or null
128
     * @return bool
129
     */
130
    public function isMatching($dtime = null)
131
    {
132
        $result = false;
133
134
        if ($this->isValid()) {
135
            $result = true;
136
137
            if ($dtime instanceof \DateTime) {
138
                $dtime->setTimezone($this->timeZone);
139
            } else {
140
                $dt = new \DateTime('now', $this->timeZone);
141
142
                if ((int)$dtime > 0) {
143
                    $dt->setTimestamp($dtime);
144
                }
145
146
                $dtime = $dt;
147
            }
148
149
            $segments = sscanf($dtime->format('i G j n w'), '%d %d %d %d %d');
150
151
            foreach ($this->registers as $i => $item) {
0 ignored issues
show
Bug introduced by
The expression $this->registers of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
152
                if (isset($item[(int)$segments[$i]]) === false) {
153
                    $result = false;
154
                    break;
155
                }
156
            }
157
        }
158
159
        return $result;
160
    }
161
162
    /**
163
     * Calculate next matching timestamp
164
     *
165
     * @param mixed $dtime \DateTime object, timestamp or null
166
     * @return int|bool next matching timestamp, or false on error
167
     */
168
    public function getNext($dtime = null)
169
    {
170
        $result = false;
171
172
        if ($this->isValid()) {
173
            if ($dtime instanceof \DateTime) {
174
                $timestamp = $dtime->getTimestamp();
175
            } elseif ((int)$dtime > 0) {
176
                $timestamp = $dtime;
177
            } else {
178
                $timestamp = time();
179
            }
180
181
            $dt = new \DateTime('now', $this->timeZone);
182
            $dt->setTimestamp(ceil($timestamp / 60) * 60);
183
184
            list($pday, $pmonth, $pyear, $phour) = sscanf(
185
                $dt->format('j n Y G'),
186
                '%d %d %d %d'
187
            );
188
189
            while ($result === false) {
190
                list($minute, $hour, $day, $month, $year, $weekday) = sscanf(
191
                    $dt->format('i G j n Y w'),
192
                    '%d %d %d %d %d %d'
193
                );
194
195
                if ($pyear !== $year) {
196
                    $dt->setDate($year, 1, 1);
197
                    $dt->setTime(0, 0);
198
                } elseif ($pmonth !== $month) {
199
                    $dt->setDate($year, $month, 1);
200
                    $dt->setTime(0, 0);
201
                } elseif ($pday !== $day) {
202
                    $dt->setTime(0, 0);
203
                } elseif ($phour !== $hour) {
204
                    $dt->setTime($hour, 0);
205
                }
206
207
                list($pday, $pmonth, $pyear, $phour) = [$day, $month, $year, $hour];
208
209
                if (isset($this->registers[3][$month]) === false) {
210
                    $dt->modify('+1 month');
211
                    continue;
212
                } elseif (false === (isset($this->registers[2][$day]) && isset($this->registers[4][$weekday]))) {
213
                    $dt->modify('+1 day');
214
                    continue;
215
                } elseif (isset($this->registers[1][$hour]) === false) {
216
                    $dt->modify('+1 hour');
217
                    continue;
218
                } elseif (isset($this->registers[0][$minute]) === false) {
219
                    $dt->modify('+1 minute');
220
                    continue;
221
                }
222
223
                $result = $dt->getTimestamp();
224
            }
225
        }
226
227
        return $result;
228
    }
229
230
    /**
231
     * Parse cron expression and return expression parsed into matchable registers
232
     *
233
     * @throws \Exception
234
     * @return array
235
     */
236
    private function parse()
237
    {
238
        $registers = null;
0 ignored issues
show
Unused Code introduced by
$registers is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
239
240
        if (sizeof($segments = preg_split('/\s+/', $this->expression)) === 5) {
241
            $registers = [];
242
243
            $minv = [0, 0, 1, 1, 0];
244
            $maxv = [59, 23, 31, 12, 7];
245
            $strv = [false, false, false, self::$months, self::$weekdays];
246
247
            foreach ($segments as $s => $segment) {
248
                // month names, weekdays
249
                if ($strv[$s] !== false && isset($strv[$s][strtolower($segment)])) {
250
                    // cannot be used with lists or ranges, see crontab(5) man page
251
                    $registers[$s][$strv[$s][strtolower($segment)]] = true;
252
                    continue;
253
                }
254
255
                // split up list into segments (e.g. "1,3-5,9")
256
                foreach (explode(',', $segment) as $listsegment) {
257
                    // parse stepping notation
258
                    if (strpos($listsegment, '/') !== false) {
259
                        if (sizeof($stepsegments = explode('/', $listsegment)) === 2) {
260
                            $listsegment = $stepsegments[0];
261
262
                            if (is_numeric($stepsegments[1])) {
263
                                if ($stepsegments[1] > 0 && $stepsegments[1] <= $maxv[$s]) {
264
                                    $steps = intval($stepsegments[1]);
265
                                } else {
266
                                    throw new \Exception('stepping value out of allowed range');
267
                                }
268
                            } else {
269
                                throw new \Exception('non-numeric stepping notation');
270
                            }
271
                        } else {
272
                            throw new \Exception('invalid stepping notation');
273
                        }
274
                    } else {
275
                        $steps = 1;
276
                    }
277
278
                    // single value
279
                    if (is_numeric($listsegment)) {
280
                        if (intval($listsegment) < $minv[$s] || intval($listsegment) > $maxv[$s]) {
281
                            throw new \Exception('value out of allowed range');
282
                        }
283
284
                        if ($steps !== 1) {
285
                            throw new \Exception('invalid combination of value and stepping notation');
286
                        }
287
288
                        $registers[$s][intval($listsegment)] = true;
289
                        continue;
290
                    }
291
292
                    // asterisk indicates full range of values
293
                    if ($listsegment === '*') {
294
                        $listsegment = sprintf('%d-%d', $minv[$s], $maxv[$s]);
295
                    }
296
297
                    // range of values, e.g. "9-17"
298
                    if (strpos($listsegment, '-') !== false) {
299
                        if (sizeof($ranges = explode('-', $listsegment)) !== 2) {
300
                            throw new \Exception('invalid range notation');
301
                        }
302
303
                        // validate range
304
                        foreach ($ranges as $range) {
305
                            if (is_numeric($range)) {
306
                                if (intval($range) < $minv[$s] || intval($range) > $maxv[$s]) {
307
                                    throw new \Exception('invalid range start or end value');
308
                                }
309
                            } else {
310
                                throw new \Exception('non-numeric range notation');
311
                            }
312
                        }
313
314
                        // fill matching register
315
                        if ($ranges[0] === $ranges[1]) {
316
                            $registers[$s][$ranges[0]] = true;
317
                        } else {
318
                            for ($i = $minv[$s]; $i <= $maxv[$s]; $i++) {
319
                                if (($i - $ranges[0]) % $steps === 0) {
320
                                    if ($ranges[0] < $ranges[1]) {
321 View Code Duplication
                                        if ($i >= $ranges[0] && $i <= $ranges[1]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
322
                                            $registers[$s][$i] = true;
323
                                        }
324 View Code Duplication
                                    } elseif ($i >= $ranges[0] || $i <= $ranges[1]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
                                        $registers[$s][$i] = true;
326
                                    }
327
                                }
328
                            }
329
                        }
330
331
                        continue;
332
                    }
333
334
                    throw new \Exception('failed to parse list segment');
335
                }
336
            }
337
338
            if (isset($registers[4][7])) {
339
                $registers[4][0] = true;
340
            }
341
        } else {
342
            throw new \Exception('invalid number of segments');
343
        }
344
345
        return $registers;
346
    }
347
}
348