Project

General

Profile

Paste
Download (9.33 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / job_scheduler / JobSchedulerCronTab.inc @ 082b75eb

1
<?php
2

    
3
/**
4
 * @file
5
 * JobSchedulerCronTab class.
6
 */
7

    
8
/**
9
 * Jose's cron tab parser = Better try only simple crontab strings.
10
 *
11
 * Usage:
12
 *   // Run 23 minutes after midn, 2am, 4am ..., everyday
13
 *   $crontab = new JobSchedulerCronTab('23 0-23/2 * * *');
14
 *   // When this needs to run next, from current time?
15
 *   $next_time = $crontab->nextTime(time());
16
 *
17
 * I hate Sundays.
18
 */
19
class JobSchedulerCronTab {
20

    
21
  /**
22
   * Original crontab elements.
23
   *
24
   * @var string
25
   */
26
  public $crontab;
27

    
28
  /**
29
   * Parsed numeric values indexed by type.
30
   *
31
   * @var string
32
   */
33
  public $cron;
34

    
35
  /**
36
   * Constructor.
37
   *
38
   * About crontab strings, see all about possible formats
39
   * http://linux.die.net/man/5/crontab.
40
   *
41
   * @param string $crontab
42
   *   Crontab text line: minute hour day-of-month month day-of-week.
43
   */
44
  public function __construct($crontab) {
45
    $this->crontab = $crontab;
46
    $this->cron = is_array($crontab) ? $this->values($crontab) : $this->parse($crontab);
47
  }
48

    
49
  /**
50
   * Parse full crontab string into an array of type => values.
51
   *
52
   * Note this one is static and can be used to validate values.
53
   */
54
  public static function parse($crontab) {
55
    // Crontab elements, names match PHP date indexes (getdate)
56
    // Example:
57
    // $keys = array('minutes', 'hours', 'mday', 'mon', 'wday');
58
    // Replace multiple spaces by single space.
59
    $crontab = preg_replace('/(\s+)/', ' ', $crontab);
60
    // Expand into elements and parse all.
61
    $values = explode(' ', trim($crontab));
62
    return self::values($values);
63
  }
64

    
65
  /**
66
   * Parse array of values, check whether this is valid.
67
   */
68
  public static function values($array) {
69
    if (count($array) == 5) {
70
      $values = array_combine(array('minutes', 'hours', 'mday', 'mon', 'wday'), array_map('trim', $array));
71
      $elements = array();
72
      foreach ($values as $type => $string) {
73
        $elements[$type] = self::parseElement($type, $string, TRUE);
74
      }
75
      // Return only if we have the right number of elements
76
      // Dangerous means works running every second or things like that.
77
      if (count(array_filter($elements)) == 5) {
78
        return $elements;
79
      }
80
    }
81
    return NULL;
82
  }
83

    
84
  /**
85
   * Find the next occurrence within the next year as unix timestamp.
86
   *
87
   * @param timestamp $start_time
88
   *   Starting time.
89
   * @param int $limit
90
   *   Default is 366.
91
   *
92
   * @codingStandardsIgnoreStart
93
   */
94
  public function nextTime($start_time = NULL, $limit = 366) {
95
    // @codingStandardsIgnoreEnd
96
    $start_time = isset($start_time) ? $start_time : time();
97
    // Get minutes, hours, mday, wday, mon, year.
98
    $start_date = getdate($start_time);
99
    if ($date = $this->nextDate($start_date, $limit)) {
100
      return mktime($date['hours'], $date['minutes'], 0, $date['mon'], $date['mday'], $date['year']);
101
    }
102
    else {
103
      return 0;
104
    }
105
  }
106

    
107
  /**
108
   * Find the next occurrence within the next year as a date array,.
109
   *
110
   * @param array $date
111
   *   Date array with: 'mday', 'mon', 'year', 'hours', 'minutes'.
112
   * @param int $limit
113
   *   Default is 366.
114
   *
115
   * @see getdate()
116
   *
117
   * @codingStandardsIgnoreStart
118
   */
119
  public function nextDate($date, $limit = 366) {
120
    // @codingStandardsIgnoreEnd
121
    $date['seconds'] = 0;
122
    // It is possible that the current date doesn't match.
123
    if ($this->checkDay($date) && ($nextdate = $this->nextHour($date))) {
124
      return $nextdate;
125
    }
126
    elseif ($nextdate = $this->nextDay($date, $limit)) {
127
      return $nextdate;
128
    }
129
    else {
130
      return FALSE;
131
    }
132
  }
133

    
134
  /**
135
   * Check whether date's day is a valid one.
136
   */
137
  protected function checkDay($date) {
138
    foreach (array('wday', 'mday', 'mon') as $key) {
139
      if (!in_array($date[$key], $this->cron[$key])) {
140
        return FALSE;
141
      }
142
    }
143
    return TRUE;
144
  }
145

    
146
  /**
147
   * Find the next day from date that matches with cron parameters.
148
   *
149
   * Maybe it's possible that it's within the next years, maybe no day of a year
150
   * matches all conditions.
151
   * However, to prevent infinite loops we restrict it to the next year.
152
   */
153
  protected function nextDay($date, $limit = 366) {
154
    // Safety check, we love infinite loops...
155
    $i = 0;
156
    while ($i++ <= $limit) {
157
      // This should fix values out of range, like month > 12, day > 31....
158
      // So we can trust we get the next valid day, can't we?
159
      $time = mktime(0, 0, 0, $date['mon'], $date['mday'] + 1, $date['year']);
160
      $date = getdate($time);
161
      if ($this->checkDay($date)) {
162
        $date['hours'] = reset($this->cron['hours']);
163
        $date['minutes'] = reset($this->cron['minutes']);
164
        return $date;
165
      }
166
    }
167
  }
168

    
169
  /**
170
   * Find the next available hour within the same day.
171
   */
172
  protected function nextHour($date) {
173
    $cron = $this->cron;
174
    while ($cron['hours']) {
175
      $hour = array_shift($cron['hours']);
176
      // Current hour; next minute.
177
      if ($date['hours'] == $hour) {
178
        foreach ($cron['minutes'] as $minute) {
179
          if ($date['minutes'] < $minute) {
180
            $date['hours'] = $hour;
181
            $date['minutes'] = $minute;
182
            return $date;
183
          }
184
        }
185
      }
186
      // Next hour; first avaiable minute.
187
      elseif ($date['hours'] < $hour) {
188
        $date['hours'] = $hour;
189
        $date['minutes'] = reset($cron['minutes']);
190
        return $date;
191
      }
192
    }
193
    return FALSE;
194
  }
195

    
196
  /**
197
   * Parse each text element. Recursive up to some point...
198
   */
199
  protected static function parseElement($type, $string, $translate = FALSE) {
200
    $string = trim($string);
201
    if ($translate) {
202
      $string = self::translateNames($type, $string);
203
    }
204
    if ($string === '*') {
205
      // This means all possible values, return right away, no need to double
206
      // check.
207
      return self::possibleValues($type);
208
    }
209
    elseif (strpos($string, '/')) {
210
      // Multiple. Example */2, for weekday will expand into 2, 4, 6.
211
      list($values, $multiple) = explode('/', $string);
212
      $values = self::parseElement($type, $values);
213
      foreach ($values as $value) {
214
        if (!($value % $multiple)) {
215
          $range[] = $value;
216
        }
217
      }
218
    }
219
    elseif (strpos($string, ',')) {
220
      // Now process list parts, expand into items, process each and merge back.
221
      $list = explode(',', $string);
222
      $range = array();
223
      foreach ($list as $item) {
224
        if ($values = self::parseElement($type, $item)) {
225
          $range = array_merge($range, $values);
226
        }
227
      }
228
    }
229
    elseif (strpos($string, '-')) {
230
      // This defines a range. Example 1-3, will expand into 1,2,3.
231
      list($start, $end) = explode('-', $string);
232
      // Double check the range is within possible values.
233
      $range = range($start, $end);
234
    }
235
    elseif (is_numeric($string)) {
236
      // This looks like a single number, double check it's int.
237
      $range = array((int) $string);
238
    }
239

    
240
    // Return unique sorted values and double check they're within possible
241
    // values.
242
    if (!empty($range)) {
243
      $range = array_intersect(array_unique($range), self::possibleValues($type));
244
      sort($range);
245
      // Sunday validation. We need cron values to match PHP values, thus week
246
      // day 7 is not allowed, must be 0.
247
      if ($type == 'wday' && in_array(7, $range)) {
248
        array_pop($range);
249
        array_unshift($range, 0);
250
      }
251
      return $range;
252
    }
253
    else {
254
      // No match found for this one, will produce an error with validation.
255
      return array();
256
    }
257
  }
258

    
259
  /**
260
   * Get values for each type.
261
   */
262
  public static function possibleValues($type) {
263
    switch ($type) {
264
      case 'minutes':
265
        return range(0, 59);
266

    
267
      case 'hours':
268
        return range(0, 23);
269

    
270
      case 'mday':
271
        return range(1, 31);
272

    
273
      case 'mon':
274
        return range(1, 12);
275

    
276
      case 'wday':
277
        // These are PHP values, not *nix ones.
278
        return range(0, 6);
279

    
280
    }
281
  }
282

    
283
  /**
284
   * Replace element names by values.
285
   */
286
  public static function translateNames($type, $string) {
287
    switch ($type) {
288
      case 'wday':
289
        $replace = array_merge(
290
          // Tricky, tricky, we need sunday to be zero at the beginning of a
291
          // range, but 7 at the end.
292
          array(
293
            '-sunday' => '-7',
294
            '-sun' => '-7',
295
            'sunday-' => '0-',
296
            'sun-' => '0-',
297
          ),
298
          array_flip(array(
299
            'sunday',
300
            'monday',
301
            'tuesday',
302
            'wednesday',
303
            'thursday',
304
            'friday',
305
            'saturday',
306
          )),
307
          array_flip(array('sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'))
308
        );
309
        break;
310

    
311
      case 'mon':
312
        $replace = array_merge(
313
          array_flip(array(
314
            'nomonth1',
315
            'january',
316
            'february',
317
            'march',
318
            'april',
319
            'may',
320
            'june',
321
            'july',
322
            'august',
323
            'september',
324
            'october',
325
            'november',
326
            'december',
327
          )),
328
          array_flip(array(
329
            'nomonth2',
330
            'jan',
331
            'feb',
332
            'mar',
333
            'apr',
334
            'may',
335
            'jun',
336
            'jul',
337
            'aug',
338
            'sep',
339
            'oct',
340
            'nov',
341
            'dec',
342
          )),
343
          array('sept' => 9)
344
        );
345
        break;
346
    }
347
    if (empty($replace)) {
348
      return $string;
349
    }
350
    else {
351
      return strtr($string, $replace);
352
    }
353
  }
354

    
355
}