Overview

Namespaces

  • Cron
  • None

Classes

  • AbstractField
  • CronExpression
  • DayOfMonthField
  • DayOfWeekField
  • FieldFactory
  • HoursField
  • MinutesField
  • MonthField
  • YearField

Interfaces

  • FieldInterface
  • Overview
  • Namespace
  • Class
  • Tree
  1: <?php
  2: 
  3: namespace Cron;
  4: 
  5: /**
  6:  * CRON expression parser that can determine whether or not a CRON expression is
  7:  * due to run, the next run date and previous run date of a CRON expression.
  8:  * The determinations made by this class are accurate if checked run once per
  9:  * minute (seconds are dropped from date time comparisons).
 10:  *
 11:  * Schedule parts must map to:
 12:  * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
 13:  * [1-7|MON-SUN], and an optional year.
 14:  *
 15:  * @link http://en.wikipedia.org/wiki/Cron
 16:  */
 17: class CronExpression
 18: {
 19:     const MINUTE = 0;
 20:     const HOUR = 1;
 21:     const DAY = 2;
 22:     const MONTH = 3;
 23:     const WEEKDAY = 4;
 24:     const YEAR = 5;
 25: 
 26:     /**
 27:      * @var array CRON expression parts
 28:      */
 29:     private $cronParts;
 30: 
 31:     /**
 32:      * @var FieldFactory CRON field factory
 33:      */
 34:     private $fieldFactory;
 35: 
 36:     /**
 37:      * @var array Order in which to test of cron parts
 38:      */
 39:     private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
 40: 
 41:     /**
 42:      * Factory method to create a new CronExpression.
 43:      *
 44:      * @param string $expression The CRON expression to create.  There are
 45:      *      several special predefined values which can be used to substitute the
 46:      *      CRON expression:
 47:      *
 48:      *      @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
 49:      *      @monthly - Run once a month, midnight, first of month - 0 0 1 * *
 50:      *      @weekly - Run once a week, midnight on Sun - 0 0 * * 0
 51:      *      @daily - Run once a day, midnight - 0 0 * * *
 52:      *      @hourly - Run once an hour, first minute - 0 * * * *
 53:      * @param FieldFactory $fieldFactory Field factory to use
 54:      *
 55:      * @return CronExpression
 56:      */
 57:     public static function factory($expression, FieldFactory $fieldFactory = null)
 58:     {
 59:         $mappings = array(
 60:             '@yearly' => '0 0 1 1 *',
 61:             '@annually' => '0 0 1 1 *',
 62:             '@monthly' => '0 0 1 * *',
 63:             '@weekly' => '0 0 * * 0',
 64:             '@daily' => '0 0 * * *',
 65:             '@hourly' => '0 * * * *'
 66:         );
 67: 
 68:         if (isset($mappings[$expression])) {
 69:             $expression = $mappings[$expression];
 70:         }
 71: 
 72:         return new static($expression, $fieldFactory ?: new FieldFactory());
 73:     }
 74: 
 75:     /**
 76:      * Parse a CRON expression
 77:      *
 78:      * @param string       $expression   CRON expression (e.g. '8 * * * *')
 79:      * @param FieldFactory $fieldFactory Factory to create cron fields
 80:      */
 81:     public function __construct($expression, FieldFactory $fieldFactory)
 82:     {
 83:         $this->fieldFactory = $fieldFactory;
 84:         $this->setExpression($expression);
 85:     }
 86: 
 87:     /**
 88:      * Set or change the CRON expression
 89:      *
 90:      * @param string $value CRON expression (e.g. 8 * * * *)
 91:      *
 92:      * @return CronExpression
 93:      * @throws \InvalidArgumentException if not a valid CRON expression
 94:      */
 95:     public function setExpression($value)
 96:     {
 97:         $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
 98:         if (count($this->cronParts) < 5) {
 99:             throw new \InvalidArgumentException(
100:                 $value . ' is not a valid CRON expression'
101:             );
102:         }
103: 
104:         foreach ($this->cronParts as $position => $part) {
105:             $this->setPart($position, $part);
106:         }
107: 
108:         return $this;
109:     }
110: 
111:     /**
112:      * Set part of the CRON expression
113:      *
114:      * @param int    $position The position of the CRON expression to set
115:      * @param string $value    The value to set
116:      *
117:      * @return CronExpression
118:      * @throws \InvalidArgumentException if the value is not valid for the part
119:      */
120:     public function setPart($position, $value)
121:     {
122:         if (!$this->fieldFactory->getField($position)->validate($value)) {
123:             throw new \InvalidArgumentException(
124:                 'Invalid CRON field value ' . $value . ' as position ' . $position
125:             );
126:         }
127: 
128:         $this->cronParts[$position] = $value;
129: 
130:         return $this;
131:     }
132: 
133:     /**
134:      * Get a next run date relative to the current date or a specific date
135:      *
136:      * @param string|\DateTime $currentTime Relative calculation date
137:      * @param int             $nth          Number of matches to skip before returning a
138:      *     matching next run date.  0, the default, will return the current
139:      *     date and time if the next run date falls on the current date and
140:      *     time.  Setting this value to 1 will skip the first match and go to
141:      *     the second match.  Setting this value to 2 will skip the first 2
142:      *     matches and so on.
143:      * @param bool $allowCurrentDate Set to TRUE to return the current date if
144:      *     it matches the cron expression.
145:      *
146:      * @return \DateTime
147:      * @throws \RuntimeException on too many iterations
148:      */
149:     public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
150:     {
151:         return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate);
152:     }
153: 
154:     /**
155:      * Get a previous run date relative to the current date or a specific date
156:      *
157:      * @param string|\DateTime $currentTime     Relative calculation date
158:      * @param int             $nth              Number of matches to skip before returning
159:      * @param bool            $allowCurrentDate Set to TRUE to return the
160:      *     current date if it matches the cron expression
161:      *
162:      * @return \DateTime
163:      * @throws \RuntimeException on too many iterations
164:      * @see Cron\CronExpression::getNextRunDate
165:      */
166:     public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
167:     {
168:         return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate);
169:     }
170: 
171:     /**
172:      * Get multiple run dates starting at the current date or a specific date
173:      *
174:      * @param int             $total            Set the total number of dates to calculate
175:      * @param string|\DateTime $currentTime     Relative calculation date
176:      * @param bool            $invert           Set to TRUE to retrieve previous dates
177:      * @param bool            $allowCurrentDate Set to TRUE to return the
178:      *     current date if it matches the cron expression
179:      *
180:      * @return array Returns an array of run dates
181:      */
182:     public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false)
183:     {
184:         $matches = array();
185:         for ($i = 0; $i < max(0, $total); $i++) {
186:             $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate);
187:         }
188: 
189:         return $matches;
190:     }
191: 
192:     /**
193:      * Get all or part of the CRON expression
194:      *
195:      * @param string $part Specify the part to retrieve or NULL to get the full
196:      *     cron schedule string.
197:      *
198:      * @return string|null Returns the CRON expression, a part of the
199:      *      CRON expression, or NULL if the part was specified but not found
200:      */
201:     public function getExpression($part = null)
202:     {
203:         if (null === $part) {
204:             return implode(' ', $this->cronParts);
205:         } elseif (array_key_exists($part, $this->cronParts)) {
206:             return $this->cronParts[$part];
207:         }
208: 
209:         return null;
210:     }
211: 
212:     /**
213:      * Helper method to output the full expression.
214:      *
215:      * @return string Full CRON expression
216:      */
217:     public function __toString()
218:     {
219:         return $this->getExpression();
220:     }
221: 
222:     /**
223:      * Determine if the cron is due to run based on the current date or a
224:      * specific date.  This method assumes that the current number of
225:      * seconds are irrelevant, and should be called once per minute.
226:      *
227:      * @param string|\DateTime $currentTime Relative calculation date
228:      *
229:      * @return bool Returns TRUE if the cron is due to run or FALSE if not
230:      */
231:     public function isDue($currentTime = 'now')
232:     {
233:         if ('now' === $currentTime) {
234:             $currentDate = date('Y-m-d H:i');
235:             $currentTime = strtotime($currentDate);
236:         } elseif ($currentTime instanceof \DateTime) {
237:             $currentDate = clone $currentTime;
238:             // Ensure time in 'current' timezone is used
239:             $currentDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
240:             $currentDate = $currentDate->format('Y-m-d H:i');
241:             $currentTime = strtotime($currentDate);
242:         } else {
243:             $currentTime = new \DateTime($currentTime);
244:             $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
245:             $currentDate = $currentTime->format('Y-m-d H:i');
246:             $currentTime = $currentTime->getTimeStamp();
247:         }
248: 
249:         return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
250:     }
251: 
252:     /**
253:      * Get the next or previous run date of the expression relative to a date
254:      *
255:      * @param string|\DateTime $currentTime     Relative calculation date
256:      * @param int             $nth              Number of matches to skip before returning
257:      * @param bool            $invert           Set to TRUE to go backwards in time
258:      * @param bool            $allowCurrentDate Set to TRUE to return the
259:      *     current date if it matches the cron expression
260:      *
261:      * @return \DateTime
262:      * @throws \RuntimeException on too many iterations
263:      */
264:     protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false)
265:     {
266:         if ($currentTime instanceof \DateTime) {
267:             $currentDate = $currentTime;
268:         } else {
269:             $currentDate = new \DateTime($currentTime ?: 'now');
270:             $currentDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
271:         }
272: 
273:         $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
274:         $nextRun = clone $currentDate;
275:         $nth = (int) $nth;
276: 
277:         // We don't have to satisfy * or null fields
278:         $parts = array();
279:         $fields = array();
280:         foreach (self::$order as $position) {
281:             $part = $this->getExpression($position);
282:             if (null === $part || '*' === $part) {
283:                 continue;
284:             }
285:             $parts[$position] = $part;
286:             $fields[$position] = $this->fieldFactory->getField($position);
287:         }
288: 
289:         // Set a hard limit to bail on an impossible date
290:         for ($i = 0; $i < 1000; $i++) {
291: 
292:             foreach ($parts as $position => $part) {
293:                 $part = $this->getExpression($position);
294:                 if (null === $part) {
295:                     continue;
296:                 }
297: 
298:                 $satisfied = false;
299:                 // Get the field object used to validate this part
300:                 $field = $fields[$position];
301:                 // Check if this is singular or a list
302:                 if (strpos($part, ',') === false) {
303:                     $satisfied = $field->isSatisfiedBy($nextRun, $part);
304:                 } else {
305:                     foreach (array_map('trim', explode(',', $part)) as $listPart) {
306:                         if ($field->isSatisfiedBy($nextRun, $listPart)) {
307:                             $satisfied = true;
308:                             break;
309:                         }
310:                     }
311:                 }
312: 
313:                 // If the field is not satisfied, then start over
314:                 if (!$satisfied) {
315:                     $field->increment($nextRun, $invert);
316:                     continue 2;
317:                 }
318:             }
319: 
320:             // Skip this match if needed
321:             if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
322:                 $this->fieldFactory->getField(0)->increment($nextRun, $invert);
323:                 continue;
324:             }
325: 
326:             return $nextRun;
327:         }
328: 
329:         // @codeCoverageIgnoreStart
330:         throw new \RuntimeException('Impossible CRON expression');
331:         // @codeCoverageIgnoreEnd
332:     }
333: }
334: 
API documentation generated by ApiGen 2.8.0