diff --git a/administrator/components/com_scheduler/src/Helper/ExecRuleHelper.php b/administrator/components/com_scheduler/src/Helper/ExecRuleHelper.php deleted file mode 100644 index f78139d165180..0000000000000 --- a/administrator/components/com_scheduler/src/Helper/ExecRuleHelper.php +++ /dev/null @@ -1,170 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -namespace Joomla\Component\Scheduler\Administrator\Helper; - -use Cron\CronExpression; -use Joomla\CMS\Date\Date; -use Joomla\CMS\Factory; -use Joomla\Component\Scheduler\Administrator\Task\Task; -use Joomla\Database\DatabaseInterface; -use Joomla\Utilities\ArrayHelper; - -// phpcs:disable PSR1.Files.SideEffects -\defined('_JEXEC') or die; -// phpcs:enable PSR1.Files.SideEffects - -/** - * Helper class for supporting task execution rules. - * - * @since 4.1.0 - * @todo This helper should probably be merged into the {@see Task} class. - */ -class ExecRuleHelper -{ - /** - * The execution rule type - * - * @var string - * @since 4.1.0 - */ - private $type; - - /** - * @var array - * @since 4.1.0 - */ - private $task; - - /** - * @var object - * @since 4.1.0 - */ - private $rule; - - /** - * @param array|object $task A task entry - * - * @since 4.1.0 - */ - public function __construct($task) - { - $this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task); - $rule = $this->getFromTask('cron_rules'); - $this->rule = \is_string($rule) - ? (object) json_decode($rule) - : (\is_array($rule) ? (object) $rule : $rule); - $this->type = $this->rule->type; - } - - /** - * Get a property from the task array - * - * @param string $property The property to get - * @param mixed $default The default value returned if property does not exist - * - * @return mixed - * - * @since 4.1.0 - */ - private function getFromTask(string $property, $default = null) - { - $property = ArrayHelper::getValue($this->task, $property); - - return $property ?? $default; - } - - /** - * @param boolean $string If true, an SQL formatted string is returned. - * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next - * execution. - * - * @return ?Date|string - * - * @since 4.1.0 - * @throws \Exception - */ - public function nextExec(bool $string = true, bool $basisNow = false) - { - $executionRules = $this->getFromTask('execution_rules'); - $type = $executionRules['rule-type']; - switch ($type) { - case 'interval-minutes': - $now = Factory::getDate('now', 'UTC'); - $intervalMinutes = (int) $executionRules['interval-minutes']; - $interval = new \DateInterval('PT' . $intervalMinutes . 'M'); - $nextExec = $now->add($interval); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'interval-hours': - $now = Factory::getDate('now', 'UTC'); - $intervalHours = $executionRules['interval-hours']; - $interval = new \DateInterval('PT' . $intervalHours . 'H'); - $nextExec = $now->add($interval); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'interval-days': - $now = Factory::getDate('now', 'UTC'); - $intervalDays = $executionRules['interval-days']; - $interval = new \DateInterval('P' . $intervalDays . 'D'); - $nextExec = $now->add($interval); - $execTime = $executionRules['exec-time']; - [$hour, $minute] = explode(':', $execTime); - $nextExec->setTime($hour, $minute); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'interval-months': - $now = Factory::getDate('now', 'UTC'); - $intervalMonths = $executionRules['interval-months']; - $interval = new \DateInterval('P' . $intervalMonths . 'M'); - $nextExec = $now->add($interval); - $execDay = $executionRules['exec-day']; - $nextExecYear = $nextExec->format('Y'); - $nextExecMonth = $nextExec->format('n'); - $nextExec->setDate($nextExecYear, $nextExecMonth, $execDay); - - $execTime = $executionRules['exec-time']; - [$hour, $minute] = explode(':', $execTime); - $nextExec->setTime($hour, $minute); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'cron-expression': - // @todo: testing - $cExp = new CronExpression((string) $this->rule->exp); - $nextExec = $cExp->getNextRunDate('now', 0, false, Factory::getApplication()->get('offset', 'UTC')); - $nextExec->setTimezone(new \DateTimeZone('UTC')); - $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; - break; - default: - // 'manual' execution is handled here. - $nextExec = null; - } - - return $nextExec; - } - - /** - * Returns a sql-formatted string for a DateTime object. - * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. - * - * @param \DateTime $dateTime A DateTime object to format - * - * @return string - * - * @since 4.1.0 - */ - private function dateTimeToSql(\DateTime $dateTime): string - { - static $db; - $db ??= Factory::getContainer()->get(DatabaseInterface::class); - - return $dateTime->format($db->getDateFormat()); - } -} diff --git a/administrator/components/com_scheduler/src/Model/TaskModel.php b/administrator/components/com_scheduler/src/Model/TaskModel.php index ee58a19a99910..83db2fff7a122 100644 --- a/administrator/components/com_scheduler/src/Model/TaskModel.php +++ b/administrator/components/com_scheduler/src/Model/TaskModel.php @@ -22,10 +22,8 @@ use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Table\Table; -use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper; use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; use Joomla\Component\Scheduler\Administrator\Table\TaskTable; -use Joomla\Component\Scheduler\Administrator\Task\TaskOption; use Joomla\Database\DatabaseInterface; use Joomla\Database\ParameterType; use Symfony\Component\OptionsResolver\Exception\AccessException; @@ -453,15 +451,6 @@ private function buildLockQuery($db, $now, $options) ->set($db->quoteName('locked') . ' = :now1') ->bind(':now1', $now); - $activeRoutines = array_map( - static function (TaskOption $taskOption): string { - return $taskOption->id; - }, - SchedulerHelper::getTaskOptions()->options - ); - - $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - if (!$options['includeCliExclusive']) { $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); } diff --git a/administrator/components/com_scheduler/src/Task/Task.php b/administrator/components/com_scheduler/src/Task/Task.php index 26817b39789ac..5dd997fa7ec57 100644 --- a/administrator/components/com_scheduler/src/Task/Task.php +++ b/administrator/components/com_scheduler/src/Task/Task.php @@ -10,15 +10,16 @@ namespace Joomla\Component\Scheduler\Administrator\Task; +use Cron\CronExpression; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Date\Date; use Joomla\CMS\Event\AbstractEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; -use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper; use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper; use Joomla\Component\Scheduler\Administrator\Scheduler\Scheduler; use Joomla\Component\Scheduler\Administrator\Table\TaskTable; @@ -134,8 +135,11 @@ class Task implements LoggerAwareInterface public function __construct(object $record) { // Workaround because Registry dumps private properties otherwise. - $taskOption = $record->taskOption; - $record->params = json_decode($record->params, true); + $taskOption = $record->taskOption; + + if (\is_string($record->params)) { + $record->params = json_decode($record->params, true); + } $this->taskRegistry = new Registry($record); @@ -245,7 +249,7 @@ public function run(): bool $this->set('last_exit_code', $this->snapshot['status']); if ($this->snapshot['status'] !== Status::WILL_RESUME) { - $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); + $this->set('next_execution', $this->computeNextExecution()); $this->set('times_executed', $this->get('times_executed') + 1); } else { /** @@ -436,7 +440,7 @@ public function skipExecution(): void $query = $db->getQuery(true); $id = $this->get('id'); - $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); + $nextExec = $this->computeNextExecution(true, true); $query->update($db->quoteName('#__scheduler_tasks', 't')) ->set('t.next_execution = :nextExec') @@ -559,4 +563,41 @@ public static function isValidId(string $id): bool return true; } + + /** + * Compute the next execution datetime for the task. This method is used once when a task is created or modified, + * and each time a task is triggered. + * + * @param boolean $asString If true, an SQL formatted string is returned. + * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next + * execution. + * + * @return ?Date|string Next due execution. + * + * @since 4.1.0 + * @throws \Exception + */ + public function computeNextExecution(bool $asString = true, bool $basisNow = false) + { + $expression = $this->get('cron_rules.exp'); + + switch ($this->get('cron_rules.type')) { + case 'interval': + $lastExec = Factory::getDate($basisNow ? 'now' : $this->get('last_execution'), 'UTC'); + $interval = new \DateInterval($expression); + $nextExec = $lastExec->add($interval); + break; + case 'cron-expression': + $cronExpression = new CronExpression($expression); + $nextExec = $cronExpression->getNextRunDate('now', 0, false, 'UTC'); + break; + default: + // 'manual' execution is handled here. + $nextExec = null; + } + + return ($asString && !empty($nextExec)) + ? $nextExec->format($this->db->getDateFormat()) + : $nextExec; + } }