From 30507f51e36621489ac54ef9891bd6e223563ff2 Mon Sep 17 00:00:00 2001 From: panlatent Date: Fri, 21 Jun 2024 18:08:34 +0800 Subject: [PATCH] Update --- UPGRADE-1.x.md | 2 +- abstract/Action.php | 2 +- abstract/ActionInterface.php | 4 +- abstract/ConditionAction.php | 34 ++++++ abstract/TriggerInterface.php | 1 - src/Scheduler.php | 6 +- src/actions/ElementAction.php | 61 +++++++++++ src/base/Timer.php | 20 +--- src/base/TimerInterface.php | 6 +- src/base/TimerTrait.php | 18 ---- src/helpers/CronHelper.php | 6 ++ src/models/Schedule.php | 26 ++--- src/models/ScheduleInfo.php | 11 ++ src/records/Action.php | 10 ++ src/services/Actions.php | 10 ++ src/services/Schedules.php | 15 +-- .../actions/ElementAction/settings.twig | 63 +++++++++++ src/templates/_components/timers/Cron.twig | 44 ++++---- src/templates/_edit.twig | 29 +++++ src/templates/_includes/forms/actionType.twig | 39 +++---- src/timers/Cron.php | 101 ++++++++++++++---- src/timers/Relay.php | 41 ++++--- 22 files changed, 400 insertions(+), 149 deletions(-) create mode 100644 abstract/ConditionAction.php create mode 100644 src/actions/ElementAction.php create mode 100644 src/models/ScheduleInfo.php create mode 100644 src/records/Action.php create mode 100644 src/templates/_components/actions/ElementAction/settings.twig diff --git a/UPGRADE-1.x.md b/UPGRADE-1.x.md index 1d237b1..c74b2e6 100644 --- a/UPGRADE-1.x.md +++ b/UPGRADE-1.x.md @@ -6,7 +6,7 @@ This is a 0.x - 1.0 update note. This article assumes that you have already used ## Documentation Promise -The new version will promise a relatively complete documentation.py +The new version will promise a relatively complete documentation ## Schedule diff --git a/abstract/Action.php b/abstract/Action.php index f11f489..82eab20 100644 --- a/abstract/Action.php +++ b/abstract/Action.php @@ -6,5 +6,5 @@ abstract class Action extends SavableComponent implements ActionInterface { - + public ?string $uid = null; } \ No newline at end of file diff --git a/abstract/ActionInterface.php b/abstract/ActionInterface.php index 3af1d96..806686b 100644 --- a/abstract/ActionInterface.php +++ b/abstract/ActionInterface.php @@ -2,7 +2,9 @@ namespace panlatent\craft\actions\abstract; -interface ActionInterface +use craft\base\SavableComponentInterface; + +interface ActionInterface extends SavableComponentInterface { public function execute(ContextInterface $context): bool; } \ No newline at end of file diff --git a/abstract/ConditionAction.php b/abstract/ConditionAction.php new file mode 100644 index 0000000..d36390b --- /dev/null +++ b/abstract/ConditionAction.php @@ -0,0 +1,34 @@ +action; + } + + public function validateConditions(): bool + { + return true; + } + + public function execute(ContextInterface $context): bool + { + + + + return $this->action->execute($context); + } +} \ No newline at end of file diff --git a/abstract/TriggerInterface.php b/abstract/TriggerInterface.php index cd71842..c630980 100644 --- a/abstract/TriggerInterface.php +++ b/abstract/TriggerInterface.php @@ -4,5 +4,4 @@ interface TriggerInterface { - public function handle(): bool; } \ No newline at end of file diff --git a/src/Scheduler.php b/src/Scheduler.php index 045d0c6..ceaf152 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -87,10 +87,8 @@ public function runSchedule(Schedule $schedule): bool */ public function getTriggerTimers(): array { - $timers = Plugin::getInstance()->timers->getActiveTimers(); - $now = new \DateTime('now', $this->timezone); - return array_filter($timers, static function (TimerInterface $timer) use($now) { - return (new CronExpression($timer->getCronExpression()))->isDue($now); + return array_filter(Plugin::getInstance()->timers->getActiveTimers(), static function (TimerInterface $timer) { + return $timer->isDue(); }); } diff --git a/src/actions/ElementAction.php b/src/actions/ElementAction.php new file mode 100644 index 0000000..51581d0 --- /dev/null +++ b/src/actions/ElementAction.php @@ -0,0 +1,61 @@ +getElements()->getAllElementTypes(); + + $elementTypeOptions = []; + $allElementActionOptions = []; + $allElementSourceOptions = []; + foreach ($elementTypes as $elementType) { + /** @var ElementInterface|string $elementType */ + $elementTypeOptions[] = ['label' => $elementType::displayName(), 'value' => $elementType]; + foreach ($elementType::actions('*') as $action) { + $allElementActionOptions[$elementType][] = [ + 'label' => $action['label'] ?? $action::displayName(), + 'value' => $action['type'] ?? $action, + ]; + } + + $allElementSourceOptions[$elementType] = []; + foreach($elementType::sources('index') as $source) { + if (isset($source['heading'])) { + continue; + } + $allElementSourceOptions[$elementType][] = [ + 'label' => $source['label'], + 'value' => $source['key'], + 'enabled' => false, + ]; + } + } + + return Craft::$app->getView()->renderTemplate('schedule/_components/actions/ElementAction/settings', [ + 'action' => $this, + 'elementTypeOptions' => $elementTypeOptions, + 'allElementActionOptions' => $allElementActionOptions, + 'allElementSourceOptions' => $allElementSourceOptions, + ]); + } +} \ No newline at end of file diff --git a/src/base/Timer.php b/src/base/Timer.php index ba264b1..2f35826 100644 --- a/src/base/Timer.php +++ b/src/base/Timer.php @@ -9,7 +9,6 @@ use Craft; use craft\base\SavableComponent; -use panlatent\schedule\helpers\CronHelper; use panlatent\schedule\models\Schedule; use panlatent\schedule\Plugin; use yii\base\InvalidConfigException; @@ -39,16 +38,6 @@ abstract class Timer extends SavableComponent implements TimerInterface // Public Methods // ========================================================================= - /** - * @return string - */ - public function __toString() - { - return Craft::t('schedule', '# {order}' , [ - 'order' => (int)$this->sortOrder - ]); - } - /** * @return array */ @@ -61,17 +50,12 @@ public function rules(): array return $rules; } - public function handle(): bool - { - return $this->getSchedule()->run(); - } - /** - * @inheritdoc + * @deprecated */ public function isValid(): bool { - return $this->getSchedule()->isValid() && $this->enabled; + return true; } /** diff --git a/src/base/TimerInterface.php b/src/base/TimerInterface.php index cbae4bf..ff14f46 100644 --- a/src/base/TimerInterface.php +++ b/src/base/TimerInterface.php @@ -22,11 +22,11 @@ interface TimerInterface extends TriggerInterface * @see \panlatent\schedule\services\Timers::getAllTimers() * * @return bool whether to run the timer. + * @deprecated since 1.0.0 */ public function isValid(): bool; - /** - * @return Schedule - */ + public function isDue(): bool; + public function getSchedule(): Schedule; } \ No newline at end of file diff --git a/src/base/TimerTrait.php b/src/base/TimerTrait.php index 7871e02..faa1747 100644 --- a/src/base/TimerTrait.php +++ b/src/base/TimerTrait.php @@ -15,26 +15,8 @@ */ trait TimerTrait { - // Properties - // ========================================================================= - - /** - * @var int|null - */ public ?int $scheduleId = null; - - /** - * @var bool|null - */ public ?bool $enabled = true; - - /** - * @var int|null - */ public ?int $sortOrder = null; - - /** - * @var string|null - */ public ?string $uid = null; } \ No newline at end of file diff --git a/src/helpers/CronHelper.php b/src/helpers/CronHelper.php index dd74697..2c61d3f 100644 --- a/src/helpers/CronHelper.php +++ b/src/helpers/CronHelper.php @@ -65,4 +65,10 @@ public static function toDescription(array|string $expression): string return $descriptor->getDescription(); } + + public function nextTime(string $expression, int $total = 5): int + { + $cron = new \Cron\CronExpression($expression); + return $cron->getNextRunDate($time)->getTimeStamp(); + } } \ No newline at end of file diff --git a/src/models/Schedule.php b/src/models/Schedule.php index 212dccf..a41675f 100644 --- a/src/models/Schedule.php +++ b/src/models/Schedule.php @@ -12,29 +12,15 @@ /** * @property-read ScheduleGroup $group + * @property-read ScheduleInfo $info * @since 1.0.0 */ class Schedule extends Model { public ?int $id = null; - /** - * @var int|null - */ public ?int $groupId = null; - - /** - * @var string|null - */ public ?string $name = null; - - /** - * @var string|null - */ public ?string $handle = null; - - /** - * @var string|null - */ public ?string $description = null; /** @@ -70,6 +56,8 @@ class Schedule extends Model public ?DateTime $dateUpdated = null; + private ?ScheduleInfo $_info = null; + /** * @return array * @todo @@ -79,6 +67,14 @@ public function getConditions(): array return []; } + public function getInfo(): ScheduleInfo + { + if ($this->_info === null) { + $this->_info = new ScheduleInfo(); + } + return $this->_info; + } + public function canRun(): bool { return true; diff --git a/src/models/ScheduleInfo.php b/src/models/ScheduleInfo.php new file mode 100644 index 0000000..de3ba89 --- /dev/null +++ b/src/models/ScheduleInfo.php @@ -0,0 +1,11 @@ + [ Command::class, Console::class, + ElementAction::class, HttpRequest::class, SendEmail::class, ] @@ -119,6 +123,12 @@ public function saveAction(ActionInterface $action, bool $runValidation = true): return false; } + if ($isNew) { + $action->uid = StringHelper::UUID(); + } elseif ($action->uid === null) { + $action->uid = Db::uidById(Table::ACTIONS, $action->id); + } + if ($runValidation && !$action->validate()) { Craft::info('Action not saved due to validation error.', __METHOD__); return false; diff --git a/src/services/Schedules.php b/src/services/Schedules.php index 66fc510..a5a1950 100644 --- a/src/services/Schedules.php +++ b/src/services/Schedules.php @@ -508,20 +508,7 @@ public function saveSchedule(Schedule $schedule, bool $runValidation = true): bo $record = new ScheduleRecord(); } - if (!$schedule->action->id) { - Craft::$app->getDb() - ->createCommand() - ->upsert(Table::ACTIONS, [ - - ], [ - 'type' => get_class($schedule->action), - 'settings' => $action->getSettings(), - 'dateUpdated' => $action->dateUpdated, - 'dateCreated' => $action->dateCreated, - 'uid' => $action->uid, - ]) - ->execute(); - } + Plugin::getInstance()->actions->saveAction($schedule->action); $record->groupId = $schedule->groupId; $record->name = $schedule->name; diff --git a/src/templates/_components/actions/ElementAction/settings.twig b/src/templates/_components/actions/ElementAction/settings.twig new file mode 100644 index 0000000..fff8769 --- /dev/null +++ b/src/templates/_components/actions/ElementAction/settings.twig @@ -0,0 +1,63 @@ +{% import "_includes/forms" as forms %} + +{{ forms.selectField({ + first: true, + label: "Element Type"|t("schedule"), + instructions: "", + id: "elementType", + name: "elementType", + value: action.elementType, + options: elementTypeOptions, + required: true, + errors: action.getErrors("elementType"), + toggle: true, +}) }} + + {% for elementType, elementActionOptions in allElementActionOptions %} + {% set isCurrent = (elementType == action.elementType) %} + + {% endfor %} + + diff --git a/src/templates/_components/timers/Cron.twig b/src/templates/_components/timers/Cron.twig index 780c9ae..12cc48d 100644 --- a/src/templates/_components/timers/Cron.twig +++ b/src/templates/_components/timers/Cron.twig @@ -1,6 +1,6 @@ {% import "_includes/forms.twig" as forms %} -
+
{{ forms.radio({ label: 'Every'|t('schedule'), name: 'mode', @@ -17,38 +17,31 @@ ]) %} {% endfor %} - {{ forms.text({ - required: true, - id: 'value', - name: 'value', - errors: timer.getErrors('value'), - }) }} - {{ forms.select({ required: true, - id: 'value', - name: 'value', - value: timer.everyUnit, + id: 'every', + name: 'every', + value: timer.every, options: options, - errors: timer.getErrors('value'), + errors: timer.getErrors('every'), }) }}
-
+
{{ forms.radio({ label: 'Datetime'|t('schedule'), name: 'mode', value: 'datetime' }) }} - {{ forms.datetimeField({ + {% include "_includes/forms/datetime.twig" with { name: 'datetime', value: timer.datetime, errors: timer.getErrors('datetime'), - }) }} + } %}
-
+
{{ forms.radio({ label: 'Cron Expression'|t('schedule'), name: 'mode', @@ -74,13 +67,23 @@
+
+ +
+ +{{ forms.timezoneField({ + label: 'Timezone'|t('schedule'), + name: 'timezone', + value: timer.timezone ?: craft.app.timezone +}) }} + {% css %} -#every input[type=text] { - width: 80px; -} #datetime fieldset { display: inline-block; } +.every-field { + margin: 12px 0; +} .expression { display: inline-block; width: 100%; @@ -92,5 +95,8 @@ .expression-item input { width: 100%; } +.every-field .datetimewrapper,.every-field .clear-btn { + display: inline-block; +} {% endcss %} diff --git a/src/templates/_edit.twig b/src/templates/_edit.twig index 593e854..4d9a885 100644 --- a/src/templates/_edit.twig +++ b/src/templates/_edit.twig @@ -109,6 +109,17 @@ {% endblock %} {% block details %} +
+ {{ forms.datetimeField({ + label: 'Expiry Date'|t('schedule'), + id: 'timeout', + name: 'timeout', + errors: schedule.getErrors('timeout'), + }) }} + + +
+ {% if craft.app.config.general.allowAdminChanges or not isNewSchedule %}
{{ forms.lightswitchField({ @@ -123,6 +134,24 @@
{% endif %} +
+ {{ "Limits"|t('app') }} +
+ {{ forms.textField({ + label: 'Timeout'|t('schedule'), + id: 'timeout', + name: 'timeout', + errors: schedule.getErrors('timeout'), + }) }} + + {{ forms.textField({ + label: 'Retry'|t('schedule'), + id: 'retry', + name: 'retry', + errors: schedule.getErrors('retry'), + }) }} +
+
{{ "Then"|t('app') }} diff --git a/src/templates/_includes/forms/actionType.twig b/src/templates/_includes/forms/actionType.twig index 5722cfb..ef7a75e 100644 --- a/src/templates/_includes/forms/actionType.twig +++ b/src/templates/_includes/forms/actionType.twig @@ -1,19 +1,22 @@ -{% set addOptionFn %} -(createOption, selectize) => { - const slideout = new Craft.CpScreenSlideout('schedule/actions/edit'); - slideout.on('submit', ev => { - createOption({ - text: ev.data.name, - value: ev.data.id, - }); - }); - slideout.on('close', () => { - selectize.focus(); - }); -} -{% endset %} +{%- set id = id ?? "actionType#{random()}" %} +{%- set containerId = "#{id}-container" %} -{% include '_includes/forms/selectize' with { - options: options ?? [] ?? craft.cp.getEntryTypeOptions(), - addOptionLabel: 'Create a new action…'|t('schedule'), -} %} +
+ +
+ +{% js %} +$('#{{ id }}').click(function() { + const slideout = new Craft.CpScreenSlideout('schedule/actions/edit'); + slideout.on('submit', ev => { + createOption({ + text: ev.data.name, + value: ev.data.id, + }); + }); + slideout.on('close', () => { + selectize.focus(); + }); + return false; +}); +{% endjs %} diff --git a/src/timers/Cron.php b/src/timers/Cron.php index ddff0d1..50541b3 100644 --- a/src/timers/Cron.php +++ b/src/timers/Cron.php @@ -3,16 +3,28 @@ namespace panlatent\schedule\timers; use Craft; +use craft\helpers\DateTimeHelper; +use Cron\CronExpression; use DateTimeZone; use panlatent\schedule\base\Timer; use panlatent\schedule\helpers\CronHelper; +/** + * @property-read ?\DateTime $datetime + */ class Cron extends Timer { public const MODE_EVERY = 'every'; public const MODE_DATETIME = 'datetime'; public const MODE_EXPRESSION = 'expression'; + public const EVERY_MINUTE = 'minute'; + public const EVERY_HOURLY = 'hourly'; + public const EVERY_DAILY = 'daily'; + public const EVERY_WEEKLY = 'weekly'; + public const EVERY_MONTHLY = 'monthly'; + public const EVERY_YEARLY = 'yearly'; + public static function displayName(): string { return Craft::t('schedule', 'Cron'); @@ -45,6 +57,13 @@ public static function displayName(): string */ public string $week = '*'; + public string $year = '*'; + + /** + * @var string|null + */ + public ?string $timezone = null; + /** * @return array */ @@ -73,17 +92,19 @@ public function getSettingsHtml(): ?string ]); } - /** - * @inheritdoc - */ - public function getCronExpression(): string + public function isDue(): bool { - return sprintf('%s %s %s %s %s', $this->minute, $this->hour, $this->day, $this->month, $this->week); + $now = new \DateTime('now', $this->timezone); + if (($this->mode === self::MODE_DATETIME) && $this->getDatetime()?->format('Y') !== $now->format('Y')) { + return false; + } + + return (new CronExpression($this->getCronExpression()))->isDue($now); } - public function getCronDescription(): string + public function getCronExpression(): string { - return CronHelper::toDescription($this->getCronExpression()); + return sprintf('%s %s %s %s %s', $this->minute, $this->hour, $this->day, $this->month, $this->week); } public function setCronExpression(string $cron): void @@ -91,30 +112,68 @@ public function setCronExpression(string $cron): void [$this->minute, $this->hour, $this->day, $this->month, $this->week, ] = explode(' ', $cron); } + public function getCronDescription(): string + { + return CronHelper::toDescription($this->getCronExpression()); + } + public function getDatetime(): ?\DateTime { - if (empty($this->year) || empty($this->timezone)) { + if ($this->year === '*') { return null; } - + $timezone = new DateTimeZone($this->timezone ?: Craft::$app->getTimeZone()); $datetime = sprintf("%d-%d-%d %d:%d", $this->year, $this->month, $this->day, $this->hour, $this->minute); - return new \DateTime($datetime, new DateTimeZone($this->timezone)); + return new \DateTime($datetime, $timezone); } - public function getEveryOptions(): array + public function setDateTime(mixed $datetime): void { - return [ - Craft::t('schedule', 'Minute') => 'minute', - Craft::t('schedule', 'Hourly') => 'hourly', - Craft::t('schedule', 'Daily') => 'daily', - Craft::t('schedule', 'Monthly') => 'monthly', - Craft::t('schedule', 'Yearly') => 'yearly', - Craft::t('schedule', 'Weekly') => 'weekly', - ]; + $datetime = DateTimeHelper::toDateTime($datetime); + $this->timezone = $datetime->getTimezone()->getName(); + $this->year = $datetime->format('Y'); + $this->month = $datetime->format('m'); + $this->day = $datetime->format('d'); + $this->hour = $datetime->format('H'); + $this->minute = $datetime->format('m'); } - public function getEveryUnit(): string + public function getEvery(): string { - return ''; + if ($this->mode !== self::MODE_EVERY){ + return self::EVERY_MINUTE; + } + return match ($this->getCronExpression()) { + '* * * * *' => self::EVERY_MINUTE, + '0 * * * *' => self::EVERY_HOURLY, + '0 0 * * *' => self::EVERY_DAILY, + '0 0 1 * *' => self::EVERY_MONTHLY, + '0 0 * * 0' => self::EVERY_WEEKLY, + '0 0 1 1 *' => self::EVERY_YEARLY, + }; + } + + public function setEvery(string $unit): void + { + $this->setCronExpression(match ($unit) { + self::EVERY_MINUTE => '* * * * *', + self::EVERY_HOURLY => '0 * * * *', + self::EVERY_DAILY => '0 0 * * *', + self::EVERY_MONTHLY => '0 0 1 * *', + self::EVERY_WEEKLY => '0 0 * * 0', + self::EVERY_YEARLY => '0 0 1 1 *', + }); + } + + public function getEveryOptions(): array + { + return [ + Craft::t('schedule', 'Minute') => self::EVERY_MINUTE, + Craft::t('schedule', 'Hourly') => self::EVERY_HOURLY, + Craft::t('schedule', 'Daily') => self::EVERY_DAILY, + Craft::t('schedule', 'Monthly') => self::EVERY_MONTHLY, + Craft::t('schedule', 'Yearly') => self::EVERY_YEARLY, + Craft::t('schedule', 'Weekly') => self::EVERY_WEEKLY, + ]; } } \ No newline at end of file diff --git a/src/timers/Relay.php b/src/timers/Relay.php index d6ced88..53a1b1f 100644 --- a/src/timers/Relay.php +++ b/src/timers/Relay.php @@ -8,9 +8,9 @@ namespace panlatent\schedule\timers; use Craft; +use Cron\CronExpression; use DateInterval; use DateTime; -use panlatent\schedule\base\Schedule; use panlatent\schedule\base\Timer; /** @@ -65,26 +65,37 @@ public function attributeLabels(): array return $attributeLabels; } - /** - * @inheritdoc - */ - public function getCronExpression(): string + public function isDue(): bool { - /** @var Schedule $schedule */ $schedule = $this->getSchedule(); - - if (!$schedule->getLastFinishedDate()) { - return '* * * * *'; + $lastFinishedTime = $schedule->getInfo()->getLastFinishedTime(); + if (!$lastFinishedTime) { + return true; } + $date = $lastFinishedTime->add(new DateInterval("PT{$this->wait}M")); + return $date->format('YmdHi') <= date('YmdHi'); - $date = $schedule->getLastFinishedDate()->add(new DateInterval("PT{$this->wait}M")); - if ($date->format('YmdHi') < date('YmdHi')) { - $date = new DateTime('now'); - } - - return $date->format('i H d m *'); +// $now = new \DateTime('now'); +// return (new CronExpression($this->getCronExpression()))->isDue($now); } +// public function getCronExpression(): string +// { +// $schedule = $this->getSchedule(); +// +// $lastFinishedTime = $schedule->getInfo()->getLastFinishedTime(); +// if (!$lastFinishedTime) { +// return '* * * * *'; +// } +// +// $date = $lastFinishedTime->add(new DateInterval("PT{$this->wait}M")); +// if ($date->format('YmdHi') <= date('YmdHi')) { +// $date = new DateTime('now'); +// } +// +// return $date->format('i H d m *'); +// } + /** * @inheritdoc */