Completed
Push — master ( 70de42...9aa418 )
by René
02:56
created

Cron::parseElement()   C

Complexity

Conditions 20
Paths 39

Size

Total Lines 61
Code Lines 34

Duplication

Lines 14
Ratio 22.95 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 14
loc 61
rs 6.1367
cc 20
eloc 34
nc 39
nop 3

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
     * Value boundaries
47
     *
48
     * @var array
49
     */
50
    protected static $boundaries = [
51
        0 => [
52
            'min' => 0,
53
            'max' => 59
54
        ],
55
        1 => [
56
            'min' => 0,
57
            'max' => 23
58
        ],
59
        2 => [
60
            'min' => 1,
61
            'max' => 31
62
        ],
63
        3 => [
64
            'min' => 1,
65
            'max' => 12
66
        ],
67
        4 => [
68
            'min' => 0,
69
            'max' => 7
70
        ]
71
    ];
72
73
    /**
74
     * Cron expression
75
     *
76
     * @var string
77
     */
78
    protected $expression;
79
80
    /**
81
     * Time zone
82
     *
83
     * @var \DateTimeZone
84
     */
85
    protected $timeZone;
86
87
    /**
88
     * Matching register
89
     *
90
     * @var array|null
91
     */
92
    protected $registers;
93
94
    /**
95
     * Class constructor sets cron expression property
96
     *
97
     * @param string $expression cron expression
98
     * @param \DateTimeZone $timeZone
99
     */
100
    public function __construct($expression = '* * * * *', \DateTimeZone $timeZone = null)
101
    {
102
        $this->setExpression($expression);
103
        $this->setTimeZone($timeZone);
104
    }
105
106
    /**
107
     * Set expression
108
     *
109
     * @param string $expression
110
     * @return self
111
     */
112
    public function setExpression($expression)
113
    {
114
        $this->expression = trim((string)$expression);
115
        $this->registers = null;
116
117
        return $this;
118
    }
119
120
    /**
121
     * Set time zone
122
     *
123
     * @param \DateTimeZone $timeZone
124
     * @return self
125
     */
126
    public function setTimeZone(\DateTimeZone $timeZone = null)
127
    {
128
        $this->timeZone = $timeZone;
129
        return $this;
130
    }
131
132
    /**
133
     * Parse and validate cron expression
134
     *
135
     * @return bool true if expression is valid, or false on error
136
     */
137
    public function isValid()
138
    {
139
        $result = true;
140
141
        if ($this->registers === null) {
142
            try {
143
                $this->registers = $this->parse();
144
            } catch (\Exception $e) {
145
                $result = false;
146
            }
147
        }
148
149
        return $result;
150
    }
151
152
    /**
153
     * Match current or given date/time against cron expression
154
     *
155
     * @param mixed $dtime \DateTime object, timestamp or null
156
     * @return bool
157
     */
158
    public function isMatching($dtime = null)
159
    {
160
        if ($dtime instanceof \DateTime) {
161
            $dtime->setTimezone($this->timeZone);
162
        } else {
163
            $dt = new \DateTime('now', $this->timeZone);
164
165
            if ((int)$dtime > 0) {
166
                $dt->setTimestamp($dtime);
167
            }
168
169
            $dtime = $dt;
170
        }
171
172
        $segments = sscanf($dtime->format('i G j n w'), '%d %d %d %d %d');
173
174
        try {
175
            $result = true;
176
177
            foreach ($this->parse() as $i => $item) {
178
                if (isset($item[(int)$segments[$i]]) === false) {
179
                    $result = false;
180
                    break;
181
                }
182
            }
183
        } catch (\Exception $e) {
184
            $result = false;
185
        }
186
187
        return $result;
188
    }
189
190
    /**
191
     * Calculate next matching timestamp
192
     *
193
     * @param mixed $dtime \DateTime object, timestamp or null
194
     * @return int|bool next matching timestamp, or false on error
195
     */
196
    public function getNext($dtime = null)
197
    {
198
        $result = false;
199
200
        if ($this->isValid()) {
201
            if ($dtime instanceof \DateTime) {
202
                $timestamp = $dtime->getTimestamp();
203
            } elseif ((int)$dtime > 0) {
204
                $timestamp = $dtime;
205
            } else {
206
                $timestamp = time();
207
            }
208
209
            $dt = new \DateTime('now', $this->timeZone);
210
            $dt->setTimestamp(ceil($timestamp / 60) * 60);
211
212
            list($pday, $pmonth, $pyear, $phour) = sscanf(
213
                $dt->format('j n Y G'),
214
                '%d %d %d %d'
215
            );
216
217
            while ($result === false) {
218
                list($minute, $hour, $day, $month, $year, $weekday) = sscanf(
219
                    $dt->format('i G j n Y w'),
220
                    '%d %d %d %d %d %d'
221
                );
222
223
                if ($pyear !== $year) {
224
                    $dt->setDate($year, 1, 1);
225
                    $dt->setTime(0, 0);
226
                } elseif ($pmonth !== $month) {
227
                    $dt->setDate($year, $month, 1);
228
                    $dt->setTime(0, 0);
229
                } elseif ($pday !== $day) {
230
                    $dt->setTime(0, 0);
231
                } elseif ($phour !== $hour) {
232
                    $dt->setTime($hour, 0);
233
                }
234
235
                list($pday, $pmonth, $pyear, $phour) = [$day, $month, $year, $hour];
236
237
                if (isset($this->registers[3][$month]) === false) {
238
                    $dt->modify('+1 month');
239
                    continue;
240
                } elseif (false === (isset($this->registers[2][$day]) && isset($this->registers[4][$weekday]))) {
241
                    $dt->modify('+1 day');
242
                    continue;
243
                } elseif (isset($this->registers[1][$hour]) === false) {
244
                    $dt->modify('+1 hour');
245
                    continue;
246
                } elseif (isset($this->registers[0][$minute]) === false) {
247
                    $dt->modify('+1 minute');
248
                    continue;
249
                }
250
251
                $result = $dt->getTimestamp();
252
            }
253
        }
254
255
        return $result;
256
    }
257
258
    /**
259
     * Parse cron expression and return expression parsed into matchable registers
260
     *
261
     * @return array
262
     * @throws \Exception
263
     */
264
    private function parse()
265
    {
266
        $registers = [];
267
268
        if (sizeof($segments = preg_split('/\s+/', $this->expression)) === 5) {
269
            foreach ($segments as $index => $segment) {
270
                $this->parseSegment($index, $segment, $registers);
271
            }
272
273
            $registers[4][0] = isset($registers[4][7]);
274
        } else {
275
            throw new \Exception('invalid number of segments');
276
        }
277
278
        return $registers;
279
    }
280
281
    /**
282
     * @param int $index
283
     * @param string $segment
284
     * @param array $registers
285
     * @throws \Exception
286
     */
287
    private function parseSegment($index, $segment, &$registers)
288
    {
289
        $strv = [false, false, false, self::$months, self::$weekdays];
290
291
        // month names, weekdays
292
        if ($strv[$index] !== false && isset($strv[$index][strtolower($segment)])) {
293
            // cannot be used with lists or ranges, see crontab(5) man page
294
            $registers[$index][$strv[$index][strtolower($segment)]] = true;
295
        } else {
296
            // split up current segment into single elements, e.g. "1,5-7,*/2" => [ "1", "5-7", "*/2" ]
297
            foreach (explode(',', $segment) as $element) {
298
                $this->parseElement($index, $element, $registers);
299
            }
300
        }
301
    }
302
303
    /**
304
     * @param int $index
305
     * @param string $element
306
     * @param array $registers
307
     * @throws \Exception
308
     */
309
    private function parseElement($index, $element, &$registers)
310
    {
311
        $stepping = $this->parseStepping($index, $element);
312
313
        // single value
314
        if (is_numeric($element)) {
315 View Code Duplication
            if (intval($element) < self::$boundaries[$index]['min'] ||
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...
316
                intval($element) > self::$boundaries[$index]['max']) {
317
                throw new \Exception('value out of allowed range');
318
            }
319
320
            if ($stepping !== 1) {
321
                throw new \Exception('invalid combination of value and stepping notation');
322
            }
323
324
            $registers[$index][intval($element)] = true;
325
        } else {
326
            // asterisk indicates full range of values
327
            if ($element === '*') {
328
                $element = sprintf('%d-%d', self::$boundaries[$index]['min'], self::$boundaries[$index]['max']);
329
            }
330
331
            // range of values, e.g. "9-17"
332
            if (strpos($element, '-') !== false) {
333
                if (sizeof($ranges = explode('-', $element)) !== 2) {
334
                    throw new \Exception('invalid range notation');
335
                }
336
337
                // validate range
338
                foreach ($ranges as $range) {
339
                    if (is_numeric($range)) {
340 View Code Duplication
                        if (intval($range) < self::$boundaries[$index]['min'] ||
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...
341
                            intval($range) > self::$boundaries[$index]['max']) {
342
                            throw new \Exception('invalid range start or end value');
343
                        }
344
                    } else {
345
                        throw new \Exception('non-numeric range notation');
346
                    }
347
                }
348
349
                // fill matching register
350
                if ($ranges[0] === $ranges[1]) {
351
                    $registers[$index][$ranges[0]] = true;
352
                } else {
353
                    for ($i = self::$boundaries[$index]['min']; $i <= self::$boundaries[$index]['max']; $i++) {
354
                        if (($i - $ranges[0]) % $stepping === 0) {
355
                            if ($ranges[0] < $ranges[1]) {
356 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...
357
                                    $registers[$index][$i] = true;
358
                                }
359 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...
360
                                $registers[$index][$i] = true;
361
                            }
362
                        }
363
                    }
364
                }
365
            } else {
366
                throw new \Exception('failed to parse list segment');
367
            }
368
        }
369
    }
370
371
    /**
372
     * @param int $index
373
     * @param string $element
374
     * @return int
375
     * @throws \Exception
376
     */
377
    private function parseStepping($index, &$element)
378
    {
379
        $stepping = 1;
380
381
        // parse stepping notation
382
        if (strpos($element, '/') !== false) {
383
            if (sizeof($stepsegments = explode('/', $element)) === 2) {
384
                $element = $stepsegments[0];
385
386
                if (is_numeric($stepsegments[1])) {
387
                    if ($stepsegments[1] > 0 && $stepsegments[1] <= self::$boundaries[$index]['max']) {
388
                        $stepping = intval($stepsegments[1]);
389
                    } else {
390
                        throw new \Exception('stepping value out of allowed range');
391
                    }
392
                } else {
393
                    throw new \Exception('non-numeric stepping notation');
394
                }
395
            } else {
396
                throw new \Exception('invalid stepping notation');
397
            }
398
        }
399
400
        return $stepping;
401
    }
402
}
403