PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/mtdowling/cron-expression/src/Cron/CronExpression.php

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