cronCommandSections = GeneralUtility::trimExplode(' ', $cronCommand); // Initialize the values with the starting time // This takes care that the calculated time is always in the future if ($timestamp === false) { $timestamp = strtotime('+1 minute'); } else { $timestamp += 60; } $this->timestamp = $this->roundTimestamp($timestamp); } /** * Calculates the date of the next execution. * * @throws \RuntimeException */ public function calculateNextValue(): void { $newTimestamp = $this->getTimestamp(); // Calculate next minute and hour field $loopCount = 0; while (true) { $loopCount++; // If there was no match within two days, cron command is invalid. // The second day is needed to catch the summertime leap in some countries. if ($loopCount > 2880) { throw new \RuntimeException('Unable to determine next execution timestamp: Hour and minute combination is invalid.', 1291494126); } if ($this->minuteAndHourMatchesCronCommand($newTimestamp)) { break; } $newTimestamp += 60; } $loopCount = 0; while (true) { $loopCount++; // A date must match within the next 4 years, this high number makes // sure leap year cron command configuration are caught. // If the loop runs longer than that, the cron command is invalid. if ($loopCount > 1464) { throw new \RuntimeException('Unable to determine next execution timestamp: Day of month, month and day of week combination is invalid.', 1291501280); } if ($this->dayMatchesCronCommand($newTimestamp)) { break; } $newTimestamp += $this->numberOfSecondsInDay($newTimestamp); } $this->timestamp = $newTimestamp; } /** * Get next timestamp */ public function getTimestamp(): int { return $this->timestamp; } /** * Get cron command sections. Array of strings, each containing either * a list of comma separated integers or * */ public function getCronCommandSections(): array { return $this->cronCommandSections; } /** * Determine if current timestamp matches minute and hour cron command restriction. */ protected function minuteAndHourMatchesCronCommand(int $timestamp): bool { $minute = (int)date('i', $timestamp); $hour = (int)date('G', $timestamp); $commandMatch = false; if ($this->isInCommandList($this->cronCommandSections[0], $minute) && $this->isInCommandList($this->cronCommandSections[1], $hour)) { $commandMatch = true; } return $commandMatch; } /** * Determine if current timestamp matches day of month, month and day of week * cron command restriction */ protected function dayMatchesCronCommand(int $timestamp): bool { $dayOfMonth = (int)date('j', $timestamp); $month = (int)date('n', $timestamp); $dayOfWeek = (int)date('N', $timestamp); $isInDayOfMonth = $this->isInCommandList($this->cronCommandSections[2], $dayOfMonth); $isInMonth = $this->isInCommandList($this->cronCommandSections[3], $month); $isInDayOfWeek = $this->isInCommandList($this->cronCommandSections[4], $dayOfWeek); // Quote from vixiecron: // Note: The day of a command's execution can be specified by two fields — day of month, and day of week. // If both fields are restricted (i.e., aren't *), the command will be run when either field // matches the current time. For example, `30 4 1,15 * 5' would cause // a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday. $isDayOfMonthRestricted = (string)$this->cronCommandSections[2] !== '*'; $isDayOfWeekRestricted = (string)$this->cronCommandSections[4] !== '*'; if (!$isInMonth) { return false; } // If both day-of-month and day-of-week are unrestricted, month match is enough. if (!$isDayOfMonthRestricted && !$isDayOfWeekRestricted) { return true; } // Otherwise, at least one restriction must match. return ($isInDayOfMonth && $isDayOfMonthRestricted) || ($isInDayOfWeek && $isDayOfWeekRestricted); } /** * Determine if a given number validates a cron command section. The given cron * command must be a 'normalized' list with only comma separated integers or '*' */ protected function isInCommandList(string $commandExpression, int $numberToMatch): bool { if ($commandExpression === '*') { $inList = true; } else { $inList = GeneralUtility::inList($commandExpression, (string)$numberToMatch); } return $inList; } /** * Helper method to calculate number of seconds in a day. * * This is not always 86400 (60*60*24) and depends on the timezone: * Some countries like Germany have a summertime / wintertime switch, * on every last sunday in march clocks are forwarded by one hour (set from 2:00 to 3:00), * and on last sunday of october they are set back one hour (from 3:00 to 2:00). * This shortens and lengthens the length of a day by one hour. */ protected function numberOfSecondsInDay(int $timestamp): int { $now = mktime(0, 0, 0, (int)date('n', $timestamp), (int)date('j', $timestamp), (int)date('Y', $timestamp)); // Make sure to be in next day, even if day has 25 hours $nextDay = $now + 60 * 60 * 25; $nextDay = mktime(0, 0, 0, (int)date('n', $nextDay), (int)date('j', $nextDay), (int)date('Y', $nextDay)); return $nextDay - $now; } /** * Round a timestamp down to full minute. */ protected function roundTimestamp(int $timestamp): int { return (int)(floor($timestamp / 60) * 60); } }