From 844142001102430527aabe988e5a32fe48944aea Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 24 Oct 2022 13:09:10 +0200 Subject: [PATCH] Introduce widget `Crontab` --- src/Widget/Crontab.php | 463 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 src/Widget/Crontab.php diff --git a/src/Widget/Crontab.php b/src/Widget/Crontab.php new file mode 100644 index 00000000..5aaf024f --- /dev/null +++ b/src/Widget/Crontab.php @@ -0,0 +1,463 @@ + 'crontab']; + + protected $data; + + public function __construct($name, $data = null, $attributes = null) + { + parent::__construct($name, $attributes); + + $this->data = $data; + self::$allowedFrequencies = [ + self::DAILY_EXPR => t('Daily'), + self::WEEKLY_EXPR => t('Weekly'), + self::MONTHLY_EXPR => t('Monthly'), + self::ANNUALLY_EXPR => t('Annually') + ]; + } + + /** + * Get the configured schedule frequency of this cron expression + * + * @return string + */ + public function getFrequency(): string + { + if (! $this->data || ! isset($this->data->frequency)) { + return Crontab::MINUTELY_EXPR; + } + + return $this->data->frequency; + } + + /** + * Get the configured customized cron frequency, if any + * + * @return string + */ + public function getCustomFrequency(): string + { + if (! $this->data || ! isset($this->data->custom_frequency)) { + return Crontab::DAILY_EXPR; + } + + return $this->data->custom_frequency; + } + + /** + * Load the given cron expression parts into this widget + * + * @param Cron $cron + * + * @return void + */ + public function load(Cron $cron): void + { + if (! $this->data || $this->getFrequency() !== static::CUSTOM_EXPR) { + return; + } + + $frequency = $this->getCustomFrequency(); + if ($frequency === static::DAILY_EXPR) { + $days = Str::trimSplit($cron->getPart(Cron::PART_DAY), '/'); + $this->populate(['days' => $this->getPopulatedValue('days', $days)]); + } elseif ($frequency === static::WEEKLY_EXPR) { + // We know already how weekly expression is being transformed to the cron expression, + // so we just have to transform it back + $hours = $cron->getPart(Cron::PART_DAY); + $weeks = Str::trimSplit($hours, '/'); + if (count($weeks) !== 2) { + $weeks = 1; + } else { + $weeks = (int) $weeks[1]; + $weeks = $weeks / 7; // Magic 7 stands for one week + } + + $days = Str::trimSplit($cron->getPart(Cron::PART_WEEKDAY)); + if ($days[0] !== '*') { // Whether the expr contains any specific days + foreach ($days as &$day) { + $day = $this->exprToDay($day); + } + } + + $this->populate([ + 'weeks' => $this->getPopulatedValue('weeks', $weeks), + 'days' => $this->getPopulatedValue('days', $days) + ]); + } elseif ($frequency === static::MONTHLY_EXPR) { + $months = Str::trimSplit($cron->getPart(Cron::PART_MONTH), '/'); + $ranges = $cron->getPart(Cron::PART_DAY); + $weekDay = $cron->getPart(Cron::PART_WEEKDAY); + + $this->populate([ + 'months' => $this->getPopulatedValue('months', $months[1] ?? $months[0]), + 'ordinal' => $this->getPopulatedValue('ordinal', $this->fromRanges($ranges)), + 'weekDay' => $this->getPopulatedValue('weekDay', $this->exprToDay($weekDay)) + ]); + } else { // It's annually expr + $years = $cron->getPart(Cron::PART_MONTH); + $ranges = $cron->getPart(Cron::PART_DAY); + $weekDay = $cron->getPart(Cron::PART_WEEKDAY); + + $this->populate([ + 'years' => $this->getPopulatedValue('years', $years), + 'ordinal' => $this->getPopulatedValue('ordinal', $this->fromRanges($ranges)), + 'weekDay' => $this->getPopulatedValue('weekDay', $this->exprToDay($weekDay)) + ]); + } + + $this->populate(['custom_expr' => $this->getPopulatedValue('custom_expr', $this->getCustomFrequency())]); + } + + /** + * Get the prepared cron expression parts of this widget + * + * @return string + */ + public function getPreparedExpr(): string + { + $frequency = $this->getPopulatedValue('custom_expr', static::DAILY_EXPR); + if ($frequency === static::DAILY_EXPR) { + $days = $this->getPopulatedValue('days', 1); + $expression = "0 0 */$days * *"; + } elseif ($frequency === static::WEEKLY_EXPR) { + $days = $this->getPopulatedValue('days', []); + $weeks = (int) $this->getPopulatedValue('weeks', 1); + + $sum = null; + foreach ($days as $day) { + if ($sum) { + $sum .= ','; + } + + $sum .= $this->dayToNumber($day); + } + + $days = $sum ?: '*'; // When there are no specific days selected by the user, just use "*" + $expression = "0 0 * * $days"; + if ($weeks > 1) { + $weeks = 7 * $weeks; // 1 Week (7 days) * N Weeks (User input) + $expression = "0 0 */$weeks * $days"; + } + } else { // Monthly || Annually + $ordinal = $this->getPopulatedValue('ordinal'); + $weekDay = $this->getPopulatedValue('weekDay', static::EVERY_DAY); + list($ranges, $day) = $this->getRanges($ordinal); + + switch ($weekDay) { + case static::EVERY_DAY: // MON - SUN + $weekDay = '*'; + break; + case static::EVERY_WEEKDAY: + $weekDay = $day; + break; + case static::EVERY_WEEKEND: // Saturday & Sunday + $weekDay = '6,0'; + break; + default: + $weekDay = $this->dayToNumber($weekDay); + } + + if ($ordinal === static::LAST) { + $weekDay .= 'L'; + } + + if ($frequency === static::MONTHLY_EXPR) { + $months = $this->getPopulatedValue('months', 1); + $expression = "0 0 $ranges */$months $weekDay"; + } else { // Annually + $years = $this->getPopulatedValue('years', 1); + $expression = "0 0 $ranges $years $weekDay"; + } + } + + return $expression; + } + + /** + * Get integer representation of the given day + * + * @param string $day + * + * @return int|string + */ + protected function dayToNumber(string $day) + { + if (! ($time = strtotime($day))) { + return $day; + } + + return (int) date('N', $time); + } + + /** + * Transform the given expression part into a valid week day string representation + * + * @param string $expr + * + * @return string + */ + protected function exprToDay(string $expr): string + { + if ($expr === '*' || $expr === '*L') { // Every day | Every last day + return static::EVERY_DAY; + } + + if (strpos($expr, '6,0') !== false) { // Every weekend + return static::EVERY_WEEKEND; + } + + if (strpos($expr, 'L') !== false) { // Every last weekday + return static::EVERY_WEEKDAY; + } + + switch ($expr) { + case '0': + case '7': + return 'sun'; + case '1': + return 'mon'; + case '2': + return 'tue'; + case '3': + return 'wed'; + case '4': + return 'thu'; + case '5': + return 'fri'; + default: + return 'sat'; + } + } + + /** + * Transform the given day month ranges into something like first,second... + * + * @param string $ranges + * + * @return string + */ + protected function fromRanges(string $ranges): string + { + switch ($ranges) { + case '1-7': + return static::FIRST; + case '8-14': + return static::SECOND; + case '15-21': + return static::THIRD; + case '22-28': + return static::FOURTH; + case '29-31': + return static::FIFTH; + default: + return static::LAST; + } + } + + /** + * Get expression day ranges for the given enumerator + * + * @param string $ordinal + * + * @return array + */ + protected function getRanges(string $ordinal): array + { + switch ($ordinal) { + case static::FIRST: + return ['1-7', 1]; + case static::SECOND: + return ['8-14', 2]; + case static::THIRD: + return ['15-21', 3]; + case static::FOURTH: + return ['22-28', 4]; + case static::FIFTH: + return ['29-31', 5]; + default: + return ['*', 5]; + } + } + + protected function assemble() + { + $this->addElement('select', 'custom_expr', [ + 'required' => false, + 'class' => 'autosubmit', + 'value' => $this->getPopulatedValue('custom_expr'), + 'options' => static::$allowedFrequencies, + 'label' => t('Custom Frequency'), + 'description' => t('Specifies how often this cron run should be recurring') + ]); + + switch ($this->getPopulatedValue('custom_expr', static::DAILY_EXPR)) { + case static::DAILY_EXPR: + $this->assembleCommonElements(); + break; + case static::WEEKLY_EXPR: + $this->assembleCommonElements(); + + $this->addElement('select', 'days[]', [ + 'required' => false, + 'multiple' => true, + 'class' => 'subform-select', + 'label' => t('Scheduling Day of Week'), + 'description' => t( + 'Run once on the selected day(s). Runs on every day if not specified' + ), + 'multiOptions' => [ + 'sun' => t('Sunday'), + 'mon' => t('Monday'), + 'tue' => t('Tuesday'), + 'wed' => t('Wednesday'), + 'thu' => t('Thursday'), + 'fri' => t('Friday'), + 'sat' => t('Saturday') + ] + ]); + break; + case static::MONTHLY_EXPR: + $this->assembleCommonElements(); + $this->assembleMonthlyOrAnnuallySchedules(); + break; + case static::ANNUALLY_EXPR: + $this->assembleCommonElements(); + $this->assembleMonthlyOrAnnuallySchedules(); + } + } + + /** + * Assemble monthly and annually part schedules + * + * @return void + */ + private function assembleMonthlyOrAnnuallySchedules() + { + $this->addElement('select', 'ordinal', [ + 'required' => false, + 'label' => t('Schedule At'), + 'description' => t('Run once on a specific day, weekend, or every day of the month'), + 'options' => [ + static::FIRST => t('First'), + static::SECOND => t('Second'), + static::THIRD => t('Third'), + static::FOURTH => t('Fourth'), + static::FIFTH => t('Fifth'), + static::LAST => t('Last') + ] + ]); + + $this->addElement('select', 'weekDay', [ + 'required' => false, + 'label' => t('Scheduling Day'), + 'description' => t( + 'Run once on the specified day, weekend, or every day. Runs every day if not specified' + ), + 'options' => [ + 'sun' => t('Sunday'), + 'mon' => t('Monday'), + 'tue' => t('Tuesday'), + 'wed' => t('Wednesday'), + 'thu' => t('Thursday'), + 'fri' => t('Friday'), + 'sat' => t('Saturday'), + static::EVERY_DAY => t('Day'), + static::EVERY_WEEKDAY => t('Weekday'), + static::EVERY_WEEKEND => t('WeekEnd'), + ] + ]); + } + + /** + * Assemble common parts for all the frequencies + * + * @return void + */ + private function assembleCommonElements() + { + $name = 'days'; + $frequency = $this->getPopulatedValue('custom_expr', static::DAILY_EXPR); + $attributes = [ + 'value' => 1, + 'min' => 1, + 'max' => 100, + 'label' => t('Every Day(s)'), + 'description' => t('Run once after the specified day(s)') + ]; + + if ($frequency === static::WEEKLY_EXPR) { + $name = 'weeks'; + $attributes['label'] = t('Every Week(s)'); + $attributes['description'] = t('Run once after the specified week(s)'); + } elseif ($frequency === static::MONTHLY_EXPR) { + $name = 'months'; + $attributes['label'] = t('Every Month(s)'); + $attributes['description'] = t('Run once after the specified month(s)'); + } elseif ($frequency === static::ANNUALLY_EXPR) { + $name = 'years'; + $attributes = [ + 'value' => 1, + 'min' => 1, + 'max' => 1, + 'disabled' => true, + 'label' => t('Every Year(s)'), + 'description' => t('Run once a year at midnight on a specific day, weekend, or weekday') + ]; + } + + $this->addElement('number', $name, $attributes); + } +}