AbstractField.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php
  2. declare(strict_types=1);
  3. namespace Cron;
  4. /**
  5. * Abstract CRON expression field.
  6. */
  7. abstract class AbstractField implements FieldInterface
  8. {
  9. /**
  10. * Full range of values that are allowed for this field type.
  11. *
  12. * @var array
  13. */
  14. protected $fullRange = [];
  15. /**
  16. * Literal values we need to convert to integers.
  17. *
  18. * @var array
  19. */
  20. protected $literals = [];
  21. /**
  22. * Start value of the full range.
  23. *
  24. * @var int
  25. */
  26. protected $rangeStart;
  27. /**
  28. * End value of the full range.
  29. *
  30. * @var int
  31. */
  32. protected $rangeEnd;
  33. /**
  34. * Constructor
  35. */
  36. public function __construct()
  37. {
  38. $this->fullRange = range($this->rangeStart, $this->rangeEnd);
  39. }
  40. /**
  41. * Check to see if a field is satisfied by a value.
  42. *
  43. * @param int $dateValue Date value to check
  44. * @param string $value Value to test
  45. *
  46. * @return bool
  47. */
  48. public function isSatisfied(int $dateValue, string $value): bool
  49. {
  50. if ($this->isIncrementsOfRanges($value)) {
  51. return $this->isInIncrementsOfRanges($dateValue, $value);
  52. }
  53. if ($this->isRange($value)) {
  54. return $this->isInRange($dateValue, $value);
  55. }
  56. return '*' === $value || $dateValue === (int) $value;
  57. }
  58. /**
  59. * Check if a value is a range.
  60. *
  61. * @param string $value Value to test
  62. *
  63. * @return bool
  64. */
  65. public function isRange(string $value): bool
  66. {
  67. return false !== strpos($value, '-');
  68. }
  69. /**
  70. * Check if a value is an increments of ranges.
  71. *
  72. * @param string $value Value to test
  73. *
  74. * @return bool
  75. */
  76. public function isIncrementsOfRanges(string $value): bool
  77. {
  78. return false !== strpos($value, '/');
  79. }
  80. /**
  81. * Test if a value is within a range.
  82. *
  83. * @param int $dateValue Set date value
  84. * @param string $value Value to test
  85. *
  86. * @return bool
  87. */
  88. public function isInRange(int $dateValue, $value): bool
  89. {
  90. $parts = array_map(
  91. function ($value) {
  92. $value = trim($value);
  93. return $this->convertLiterals($value);
  94. },
  95. explode('-', $value, 2)
  96. );
  97. return $dateValue >= $parts[0] && $dateValue <= $parts[1];
  98. }
  99. /**
  100. * Test if a value is within an increments of ranges (offset[-to]/step size).
  101. *
  102. * @param int $dateValue Set date value
  103. * @param string $value Value to test
  104. *
  105. * @return bool
  106. */
  107. public function isInIncrementsOfRanges(int $dateValue, string $value): bool
  108. {
  109. $chunks = array_map('trim', explode('/', $value, 2));
  110. $range = $chunks[0];
  111. $step = $chunks[1] ?? 0;
  112. // No step or 0 steps aren't cool
  113. /** @phpstan-ignore-next-line */
  114. if (null === $step || '0' === $step || 0 === $step) {
  115. return false;
  116. }
  117. // Expand the * to a full range
  118. if ('*' === $range) {
  119. $range = $this->rangeStart . '-' . $this->rangeEnd;
  120. }
  121. // Generate the requested small range
  122. $rangeChunks = explode('-', $range, 2);
  123. $rangeStart = $rangeChunks[0];
  124. $rangeEnd = $rangeChunks[1] ?? $rangeStart;
  125. if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
  126. throw new \OutOfRangeException('Invalid range start requested');
  127. }
  128. if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) {
  129. throw new \OutOfRangeException('Invalid range end requested');
  130. }
  131. // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps
  132. if ($step >= $this->rangeEnd) {
  133. $thisRange = [$this->fullRange[$step % \count($this->fullRange)]];
  134. } else {
  135. $thisRange = range($rangeStart, $rangeEnd, (int) $step);
  136. }
  137. return \in_array($dateValue, $thisRange, true);
  138. }
  139. /**
  140. * Returns a range of values for the given cron expression.
  141. *
  142. * @param string $expression The expression to evaluate
  143. * @param int $max Maximum offset for range
  144. *
  145. * @return array
  146. */
  147. public function getRangeForExpression(string $expression, int $max): array
  148. {
  149. $values = [];
  150. $expression = $this->convertLiterals($expression);
  151. if (false !== strpos($expression, ',')) {
  152. $ranges = explode(',', $expression);
  153. $values = [];
  154. foreach ($ranges as $range) {
  155. $expanded = $this->getRangeForExpression($range, $this->rangeEnd);
  156. $values = array_merge($values, $expanded);
  157. }
  158. return $values;
  159. }
  160. if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
  161. if (!$this->isIncrementsOfRanges($expression)) {
  162. [$offset, $to] = explode('-', $expression);
  163. $offset = $this->convertLiterals($offset);
  164. $to = $this->convertLiterals($to);
  165. $stepSize = 1;
  166. } else {
  167. $range = array_map('trim', explode('/', $expression, 2));
  168. $stepSize = $range[1] ?? 0;
  169. $range = $range[0];
  170. $range = explode('-', $range, 2);
  171. $offset = $range[0];
  172. $to = $range[1] ?? $max;
  173. }
  174. $offset = '*' === $offset ? $this->rangeStart : $offset;
  175. if ($stepSize >= $this->rangeEnd) {
  176. $values = [$this->fullRange[$stepSize % \count($this->fullRange)]];
  177. } else {
  178. for ($i = $offset; $i <= $to; $i += $stepSize) {
  179. $values[] = (int) $i;
  180. }
  181. }
  182. sort($values);
  183. } else {
  184. $values = [$expression];
  185. }
  186. return $values;
  187. }
  188. /**
  189. * Convert literal.
  190. *
  191. * @param string $value
  192. *
  193. * @return string
  194. */
  195. protected function convertLiterals(string $value): string
  196. {
  197. if (\count($this->literals)) {
  198. $key = array_search(strtoupper($value), $this->literals, true);
  199. if (false !== $key) {
  200. return (string) $key;
  201. }
  202. }
  203. return $value;
  204. }
  205. /**
  206. * Checks to see if a value is valid for the field.
  207. *
  208. * @param string $value
  209. *
  210. * @return bool
  211. */
  212. public function validate(string $value): bool
  213. {
  214. $value = $this->convertLiterals($value);
  215. // All fields allow * as a valid value
  216. if ('*' === $value) {
  217. return true;
  218. }
  219. if (false !== strpos($value, '/')) {
  220. [$range, $step] = explode('/', $value);
  221. // Don't allow numeric ranges
  222. if (is_numeric($range)) {
  223. return false;
  224. }
  225. return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
  226. }
  227. // Validate each chunk of a list individually
  228. if (false !== strpos($value, ',')) {
  229. foreach (explode(',', $value) as $listItem) {
  230. if (!$this->validate($listItem)) {
  231. return false;
  232. }
  233. }
  234. return true;
  235. }
  236. if (false !== strpos($value, '-')) {
  237. if (substr_count($value, '-') > 1) {
  238. return false;
  239. }
  240. $chunks = explode('-', $value);
  241. $chunks[0] = $this->convertLiterals($chunks[0]);
  242. $chunks[1] = $this->convertLiterals($chunks[1]);
  243. if ('*' === $chunks[0] || '*' === $chunks[1]) {
  244. return false;
  245. }
  246. return $this->validate($chunks[0]) && $this->validate($chunks[1]);
  247. }
  248. if (!is_numeric($value)) {
  249. return false;
  250. }
  251. if (false !== strpos($value, '.')) {
  252. return false;
  253. }
  254. // We should have a numeric by now, so coerce this into an integer
  255. $value = (int) $value;
  256. return \in_array($value, $this->fullRange, true);
  257. }
  258. }