summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/scheduler
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/ipl/scheduler')
-rw-r--r--vendor/ipl/scheduler/composer.json39
-rw-r--r--vendor/ipl/scheduler/src/Common/Promises.php108
-rw-r--r--vendor/ipl/scheduler/src/Common/TaskProperties.php83
-rw-r--r--vendor/ipl/scheduler/src/Common/Timers.php60
-rw-r--r--vendor/ipl/scheduler/src/Contract/Frequency.php62
-rw-r--r--vendor/ipl/scheduler/src/Contract/Task.php39
-rw-r--r--vendor/ipl/scheduler/src/Cron.php203
-rw-r--r--vendor/ipl/scheduler/src/OneOff.php69
-rw-r--r--vendor/ipl/scheduler/src/RRule.php328
-rw-r--r--vendor/ipl/scheduler/src/Scheduler.php323
-rw-r--r--vendor/ipl/scheduler/src/register_cron_aliases.php11
11 files changed, 1325 insertions, 0 deletions
diff --git a/vendor/ipl/scheduler/composer.json b/vendor/ipl/scheduler/composer.json
new file mode 100644
index 0000000..5431be8
--- /dev/null
+++ b/vendor/ipl/scheduler/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "ipl/scheduler",
+ "type": "library",
+ "description": "Icinga PHP Library - Tasks scheduler",
+ "keywords": ["task", "job", "scheduler", "cron"],
+ "homepage": "https://github.com/Icinga/ipl-scheduler",
+ "license": "MIT",
+ "config": {
+ "sort-packages": true
+ },
+ "require": {
+ "php": ">=7.2",
+ "ext-json": "*",
+ "dragonmantank/cron-expression": "^3",
+ "psr/log": "^1",
+ "ramsey/uuid": "^4.2.3",
+ "react/event-loop": "^1.4",
+ "react/promise": "^2.10",
+ "simshaun/recurr": "^5",
+ "ipl/stdlib": ">=0.12.0"
+ },
+ "require-dev": {
+ "ipl/stdlib": "dev-main"
+ },
+ "suggest": {
+ "ext-ev": "Improves performance, efficiency and avoids system limitations. Highly recommended! (See https://www.php.net/manual/en/intro.ev.php for details)"
+ },
+ "autoload": {
+ "files": ["src/register_cron_aliases.php"],
+ "psr-4": {
+ "ipl\\Scheduler\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "ipl\\Tests\\Scheduler\\": "tests"
+ }
+ }
+}
diff --git a/vendor/ipl/scheduler/src/Common/Promises.php b/vendor/ipl/scheduler/src/Common/Promises.php
new file mode 100644
index 0000000..b896627
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Common/Promises.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace ipl\Scheduler\Common;
+
+use ArrayObject;
+use InvalidArgumentException;
+use Ramsey\Uuid\UuidInterface;
+use React\Promise\PromiseInterface;
+use SplObjectStorage;
+
+trait Promises
+{
+ /** @var SplObjectStorage<UuidInterface, ArrayObject<int, PromiseInterface>> */
+ protected $promises;
+
+ /**
+ * Add the given promise for the specified UUID
+ *
+ * **Example Usage:**
+ *
+ * ```php
+ * $promise = work();
+ * $promises->addPromise($uuid, $promise);
+ * ```
+ *
+ * @param UuidInterface $uuid
+ * @param PromiseInterface $promise
+ *
+ * @return $this
+ */
+ protected function addPromise(UuidInterface $uuid, PromiseInterface $promise): self
+ {
+ if (! $this->promises->contains($uuid)) {
+ $this->promises->attach($uuid, new ArrayObject());
+ }
+
+ $this->promises[$uuid][] = $promise;
+
+ return $this;
+ }
+
+ /**
+ * Remove the given promise for the specified UUID
+ *
+ * **Example Usage:**
+ *
+ * ```php
+ * $promise->always(function () use ($uuid, $promise) {
+ * $promises->removePromise($uuid, $promise);
+ * })
+ * ```
+ *
+ * @param UuidInterface $uuid
+ * @param PromiseInterface $promise
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If the given UUID doesn't have any registered promises or when the specified
+ * UUID promises doesn't contain the provided promise
+ */
+ protected function removePromise(UuidInterface $uuid, PromiseInterface $promise): self
+ {
+ if (! $this->promises->contains($uuid)) {
+ throw new InvalidArgumentException(
+ sprintf('There are no registered promises for UUID %s', $uuid->toString())
+ );
+ }
+
+ foreach ($this->promises[$uuid] as $k => $v) {
+ if ($v === $promise) {
+ unset($this->promises[$uuid][$k]);
+
+ return $this;
+ }
+ }
+
+ throw new InvalidArgumentException(
+ sprintf('There is no such promise for UUID %s', $uuid->toString())
+ );
+ }
+
+ /**
+ * Detach and return promises for the given UUID, if any
+ *
+ * **Example Usage:**
+ *
+ * ```php
+ * foreach ($promises->detachPromises($uuid) as $promise) {
+ * $promise->cancel();
+ * }
+ * ```
+ *
+ * @param UuidInterface $uuid
+ *
+ * @return PromiseInterface[]
+ */
+ protected function detachPromises(UuidInterface $uuid): array
+ {
+ if (! $this->promises->contains($uuid)) {
+ return [];
+ }
+
+ $promises = $this->promises[$uuid];
+ $this->promises->detach($uuid);
+
+ return $promises->getArrayCopy();
+ }
+}
diff --git a/vendor/ipl/scheduler/src/Common/TaskProperties.php b/vendor/ipl/scheduler/src/Common/TaskProperties.php
new file mode 100644
index 0000000..4ab65e2
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Common/TaskProperties.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace ipl\Scheduler\Common;
+
+use LogicException;
+use Ramsey\Uuid\UuidInterface;
+
+trait TaskProperties
+{
+ /** @var string */
+ protected $description;
+
+ /** @var string Name of this task */
+ protected $name;
+
+ /** @var UuidInterface Unique identifier of this task */
+ protected $uuid;
+
+ /**
+ * Set the description of this task
+ *
+ * @param ?string $desc
+ *
+ * @return $this
+ */
+ public function setDescription(?string $desc): self
+ {
+ $this->description = $desc;
+
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function getName(): string
+ {
+ if (! $this->name) {
+ throw new LogicException('Task name must not be null');
+ }
+
+ return $this->name;
+ }
+
+ /**
+ * Set the name of this Task
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName(string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getUuid(): UuidInterface
+ {
+ if (! $this->uuid) {
+ throw new LogicException('Task UUID must not be null');
+ }
+
+ return $this->uuid;
+ }
+
+ /**
+ * Set the UUID of this task
+ *
+ * @param UuidInterface $uuid
+ *
+ * @return $this
+ */
+ public function setUuid(UuidInterface $uuid): self
+ {
+ $this->uuid = $uuid;
+
+ return $this;
+ }
+}
diff --git a/vendor/ipl/scheduler/src/Common/Timers.php b/vendor/ipl/scheduler/src/Common/Timers.php
new file mode 100644
index 0000000..2d0641f
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Common/Timers.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace ipl\Scheduler\Common;
+
+use Ramsey\Uuid\UuidInterface;
+use React\EventLoop\TimerInterface;
+use SplObjectStorage;
+
+trait Timers
+{
+ /** @var SplObjectStorage<UuidInterface, TimerInterface> */
+ protected $timers;
+
+ /**
+ * Set a timer for the given UUID
+ *
+ * **Example Usage:**
+ *
+ * ```php
+ * $timers->attachTimer($uuid, Loop::addTimer($interval, $callback));
+ * ```
+ *
+ * @param UuidInterface $uuid
+ * @param TimerInterface $timer
+ *
+ * @return $this
+ */
+ protected function attachTimer(UuidInterface $uuid, TimerInterface $timer): self
+ {
+ $this->timers->attach($uuid, $timer);
+
+ return $this;
+ }
+
+ /**
+ * Detach and return the timer for the given UUID, if any
+ *
+ * **Example Usage:**
+ *
+ * ```php
+ * Loop::cancelTimer($timers->detachTimer($uuid));
+ * ```
+ *
+ * @param UuidInterface $uuid
+ *
+ * @return ?TimerInterface
+ */
+ protected function detachTimer(UuidInterface $uuid): ?TimerInterface
+ {
+ if (! $this->timers->contains($uuid)) {
+ return null;
+ }
+
+ $timer = $this->timers->offsetGet($uuid);
+
+ $this->timers->detach($uuid);
+
+ return $timer;
+ }
+}
diff --git a/vendor/ipl/scheduler/src/Contract/Frequency.php b/vendor/ipl/scheduler/src/Contract/Frequency.php
new file mode 100644
index 0000000..2235787
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Contract/Frequency.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace ipl\Scheduler\Contract;
+
+use DateTimeInterface;
+use JsonSerializable;
+
+interface Frequency extends JsonSerializable
+{
+ /** @var string Format for representing datetimes when serializing the frequency to JSON */
+ public const SERIALIZED_DATETIME_FORMAT = 'Y-m-d\TH:i:s.ue';
+
+ /**
+ * Get whether the frequency is due at the specified time
+ *
+ * @param DateTimeInterface $dateTime
+ *
+ * @return bool
+ */
+ public function isDue(DateTimeInterface $dateTime): bool;
+
+ /**
+ * Get the next due date relative to the given time
+ *
+ * @param DateTimeInterface $dateTime
+ *
+ * @return DateTimeInterface
+ */
+ public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface;
+
+ /**
+ * Get whether the specified time is beyond the frequency's expiry time
+ *
+ * @param DateTimeInterface $dateTime
+ *
+ * @return bool
+ */
+ public function isExpired(DateTimeInterface $dateTime): bool;
+
+ /**
+ * Get the start time of this frequency
+ *
+ * @return ?DateTimeInterface
+ */
+ public function getStart(): ?DateTimeInterface;
+
+ /**
+ * Get the end time of this frequency
+ *
+ * @return ?DateTimeInterface
+ */
+ public function getEnd(): ?DateTimeInterface;
+
+ /**
+ * Create frequency from its stored JSON representation previously encoded with {@see json_encode()}
+ *
+ * @param string $json
+ *
+ * @return $this
+ */
+ public static function fromJson(string $json): self;
+}
diff --git a/vendor/ipl/scheduler/src/Contract/Task.php b/vendor/ipl/scheduler/src/Contract/Task.php
new file mode 100644
index 0000000..db09ddc
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Contract/Task.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace ipl\Scheduler\Contract;
+
+use Ramsey\Uuid\UuidInterface;
+use React\Promise\ExtendedPromiseInterface;
+
+interface Task
+{
+ /**
+ * Get the name of this task
+ *
+ * @return string
+ */
+ public function getName(): string;
+
+ /**
+ * Get unique identifier of this task
+ *
+ * @return UuidInterface
+ */
+ public function getUuid(): UuidInterface;
+
+ /**
+ * Get the description of this task
+ *
+ * @return ?string
+ */
+ public function getDescription(): ?string;
+
+ /**
+ * Run this tasks operations
+ *
+ * This commits the actions in a non-blocking fashion to the event loop and yields a deferred promise
+ *
+ * @return ExtendedPromiseInterface
+ */
+ public function run(): ExtendedPromiseInterface;
+}
diff --git a/vendor/ipl/scheduler/src/Cron.php b/vendor/ipl/scheduler/src/Cron.php
new file mode 100644
index 0000000..639957b
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Cron.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace ipl\Scheduler;
+
+use Cron\CronExpression;
+use DateTime;
+use DateTimeInterface;
+use DateTimeZone;
+use InvalidArgumentException;
+use ipl\Scheduler\Contract\Frequency;
+
+use function ipl\Stdlib\get_php_type;
+
+class Cron implements Frequency
+{
+ public const PART_MINUTE = 0;
+ public const PART_HOUR = 1;
+ public const PART_DAY = 2;
+ public const PART_MONTH = 3;
+ public const PART_WEEKDAY = 4;
+
+ /** @var CronExpression */
+ protected $cron;
+
+ /** @var ?DateTimeInterface Start time of this frequency */
+ protected $start;
+
+ /** @var ?DateTimeInterface End time of this frequency */
+ protected $end;
+
+ /** @var string String representation of the cron expression */
+ protected $expression;
+
+ /**
+ * Create frequency from the specified cron expression
+ *
+ * @param string $expression
+ *
+ * @throws InvalidArgumentException If expression is not a valid cron expression
+ */
+ public function __construct(string $expression)
+ {
+ $this->cron = new CronExpression($expression);
+ $this->expression = $expression;
+ }
+
+ public function isDue(DateTimeInterface $dateTime): bool
+ {
+ if ($this->isExpired($dateTime) || $dateTime < $this->start) {
+ return false;
+ }
+
+ return $this->cron->isDue($dateTime);
+ }
+
+ public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface
+ {
+ if ($this->isExpired($dateTime)) {
+ return $this->end;
+ }
+
+ if ($dateTime < $this->start) {
+ return $this->start;
+ }
+
+ return $this->cron->getNextRunDate($dateTime);
+ }
+
+ public function isExpired(DateTimeInterface $dateTime): bool
+ {
+ return $this->end !== null && $this->end < $dateTime;
+ }
+
+ public function getStart(): ?DateTimeInterface
+ {
+ return $this->start;
+ }
+
+ public function getEnd(): ?DateTimeInterface
+ {
+ return $this->end;
+ }
+
+ /**
+ * Get the configured cron expression
+ *
+ * @return string
+ */
+ public function getExpression(): string
+ {
+ return $this->expression;
+ }
+
+ /**
+ * Set the start time of this frequency
+ *
+ * @param DateTimeInterface $start
+ *
+ * @return $this
+ */
+ public function startAt(DateTimeInterface $start): self
+ {
+ $this->start = clone $start;
+ $this->start->setTimezone(new DateTimeZone(date_default_timezone_get()));
+
+ return $this;
+ }
+
+ /**
+ * Set the end time of this frequency
+ *
+ * @param DateTimeInterface $end
+ *
+ * @return $this
+ */
+ public function endAt(DateTimeInterface $end): Frequency
+ {
+ $this->end = clone $end;
+ $this->end->setTimezone(new DateTimeZone(date_default_timezone_get()));
+
+ return $this;
+ }
+
+ /**
+ * Get the given part of the underlying cron expression
+ *
+ * @param int $part One of the classes `PART_*` constants
+ *
+ * @return string
+ *
+ * @throws InvalidArgumentException If the given part is invalid
+ */
+ public function getPart(int $part): string
+ {
+ $value = $this->cron->getExpression($part);
+ if ($value === null) {
+ throw new InvalidArgumentException(sprintf('Invalid expression part specified: %d', $part));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Get the parts of the underlying cron expression as an array
+ *
+ * @return string[]
+ */
+ public function getParts(): array
+ {
+ return $this->cron->getParts();
+ }
+
+ /**
+ * Get whether the given cron expression is valid
+ *
+ * @param string $expression
+ *
+ * @return bool
+ */
+ public static function isValid(string $expression): bool
+ {
+ return CronExpression::isValidExpression($expression);
+ }
+
+ public static function fromJson(string $json): Frequency
+ {
+ $data = json_decode($json, true);
+ if (! is_array($data)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ '%s expects json decoded value to be an array, got %s instead',
+ __METHOD__,
+ get_php_type($data)
+ )
+ );
+ }
+
+ $self = new static($data['expression']);
+ if (isset($data['start'])) {
+ $self->startAt(new DateTime($data['start']));
+ }
+
+ if (isset($data['end'])) {
+ $self->endAt(new DateTime($data['end']));
+ }
+
+ return $self;
+ }
+
+ public function jsonSerialize(): array
+ {
+ $data = ['expression' => $this->getExpression()];
+ if ($this->start) {
+ $data['start'] = $this->start->format(static::SERIALIZED_DATETIME_FORMAT);
+ }
+
+ if ($this->end) {
+ $data['end'] = $this->end->format(static::SERIALIZED_DATETIME_FORMAT);
+ }
+
+ return $data;
+ }
+}
diff --git a/vendor/ipl/scheduler/src/OneOff.php b/vendor/ipl/scheduler/src/OneOff.php
new file mode 100644
index 0000000..ebe945d
--- /dev/null
+++ b/vendor/ipl/scheduler/src/OneOff.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace ipl\Scheduler;
+
+use DateTime;
+use DateTimeInterface;
+use DateTimeZone;
+use InvalidArgumentException;
+use ipl\Scheduler\Contract\Frequency;
+
+use function ipl\Stdlib\get_php_type;
+
+class OneOff implements Frequency
+{
+ /** @var DateTimeInterface Start time of this frequency */
+ protected $dateTime;
+
+ public function __construct(DateTimeInterface $dateTime)
+ {
+ $this->dateTime = clone $dateTime;
+ $this->dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ }
+
+ public function isDue(DateTimeInterface $dateTime): bool
+ {
+ return ! $this->isExpired($dateTime) && $this->dateTime == $dateTime;
+ }
+
+ public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface
+ {
+ return $this->dateTime;
+ }
+
+ public function isExpired(DateTimeInterface $dateTime): bool
+ {
+ return $this->dateTime < $dateTime;
+ }
+
+ public function getStart(): ?DateTimeInterface
+ {
+ return $this->dateTime;
+ }
+
+ public function getEnd(): ?DateTimeInterface
+ {
+ return $this->getStart();
+ }
+
+ public static function fromJson(string $json): Frequency
+ {
+ $data = json_decode($json, true);
+ if (! is_string($data)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ '%s expects json decoded value to be string, got %s instead',
+ __METHOD__,
+ get_php_type($data)
+ )
+ );
+ }
+
+ return new static(new DateTime($data));
+ }
+
+ public function jsonSerialize(): string
+ {
+ return $this->dateTime->format(static::SERIALIZED_DATETIME_FORMAT);
+ }
+}
diff --git a/vendor/ipl/scheduler/src/RRule.php b/vendor/ipl/scheduler/src/RRule.php
new file mode 100644
index 0000000..bfad0e5
--- /dev/null
+++ b/vendor/ipl/scheduler/src/RRule.php
@@ -0,0 +1,328 @@
+<?php
+
+namespace ipl\Scheduler;
+
+use BadMethodCallException;
+use DateTime;
+use DateTimeInterface;
+use DateTimeZone;
+use Generator;
+use InvalidArgumentException;
+use ipl\Scheduler\Contract\Frequency;
+use Recurr\Exception\InvalidRRule;
+use Recurr\Rule as RecurrRule;
+use Recurr\Transformer\ArrayTransformer;
+use Recurr\Transformer\ArrayTransformerConfig;
+use Recurr\Transformer\Constraint\AfterConstraint;
+use Recurr\Transformer\Constraint\BetweenConstraint;
+use stdClass;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * Support scheduling a task based on expressions in iCalendar format
+ */
+class RRule implements Frequency
+{
+ /** @var string Run once a year */
+ public const YEARLY = 'YEARLY';
+
+ /** @var string Run every 3 month starting from the given start time */
+ public const QUARTERLY = 'QUARTERLY';
+
+ /** @var string Run once a month */
+ public const MONTHLY = 'MONTHLY';
+
+ /** @var string Run once a week based on the specified start time */
+ public const WEEKLY = 'WEEKLY';
+
+ /** @var string Run once a day at the specified start time */
+ public const DAILY = 'DAILY';
+
+ /** @var string Run once an hour */
+ public const HOURLY = 'HOURLY';
+
+ /** @var string Run once a minute */
+ public const MINUTELY = 'MINUTELY';
+
+ /** @var int Default limit of the recurrences to be generated by the transformer */
+ private const DEFAULT_LIMIT = 1;
+
+ /** @var RecurrRule */
+ protected $rrule;
+
+ /** @var ArrayTransformer */
+ protected $transformer;
+
+ /** @var ArrayTransformerConfig */
+ protected $transformerConfig;
+
+ /** @var string */
+ protected $frequency;
+
+ /**
+ * Construct a new rrule instance
+ *
+ * @param string|array<string, mixed> $rule
+ *
+ * @throws InvalidRRule
+ */
+ public function __construct($rule)
+ {
+ $this->rrule = new RecurrRule($rule);
+ $this->frequency = $this->rrule->getFreqAsText();
+ $this->transformerConfig = new ArrayTransformerConfig();
+ $this->transformerConfig->setVirtualLimit(self::DEFAULT_LIMIT);
+
+ // If the run day isn't set explicitly, we can enable the last day of month
+ // fix, so that it doesn't skip some months which doesn't have e.g. 29,30,31 days.
+ if (
+ $this->getFrequency() === static::MONTHLY
+ && ! $this->rrule->getByDay()
+ && ! $this->rrule->getByMonthDay()
+ ) {
+ $this->transformerConfig->enableLastDayOfMonthFix();
+ }
+
+ $this->transformer = new ArrayTransformer($this->transformerConfig);
+ }
+
+ /**
+ * Get an RRule instance from the provided frequency
+ *
+ * @param string $frequency
+ *
+ * @return $this
+ */
+ public static function fromFrequency(string $frequency): self
+ {
+ $frequencies = array_flip([
+ static::MINUTELY,
+ static::HOURLY,
+ static::DAILY,
+ static::WEEKLY,
+ static::MONTHLY,
+ static::QUARTERLY,
+ static::YEARLY
+ ]);
+
+ if (! isset($frequencies[$frequency])) {
+ throw new InvalidArgumentException(sprintf('Unknown frequency provided: %s', $frequency));
+ }
+
+ if ($frequency === static::QUARTERLY) {
+ $repeat = static::MONTHLY;
+ $rule = "FREQ=$repeat;INTERVAL=3";
+ } else {
+ $rule = "FREQ=$frequency";
+ }
+
+ $self = new static($rule);
+ $self->frequency = $frequency;
+
+ return $self;
+ }
+
+ public static function fromJson(string $json): Frequency
+ {
+ /** @var stdClass $data */
+ $data = json_decode($json);
+ $self = new static($data->rrule);
+ $self->frequency = $data->frequency;
+ if (isset($data->start)) {
+ $start = DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start);
+ if (! $start) {
+ throw new InvalidArgumentException(sprintf('Cannot deserialize start time: %s', $data->start));
+ }
+
+ $self->startAt($start);
+ }
+
+ return $self;
+ }
+
+ public function isDue(DateTimeInterface $dateTime): bool
+ {
+ if ($dateTime < $this->rrule->getStartDate() || $this->isExpired($dateTime)) {
+ return false;
+ }
+
+ $nextDue = $this->getNextRecurrences($dateTime);
+ if (! $nextDue->valid()) {
+ return false;
+ }
+
+ return $nextDue->current() == $dateTime;
+ }
+
+ public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface
+ {
+ if ($this->isExpired($dateTime)) {
+ return $this->getEnd();
+ }
+
+ $nextDue = $this->getNextRecurrences($dateTime, 1, false);
+ if (! $nextDue->valid()) {
+ return $dateTime;
+ }
+
+ return $nextDue->current();
+ }
+
+ public function isExpired(DateTimeInterface $dateTime): bool
+ {
+ if ($this->rrule->repeatsIndefinitely()) {
+ return false;
+ }
+
+ return $this->getEnd() !== null && $this->getEnd() < $dateTime;
+ }
+
+ /**
+ * Set the start time of this frequency
+ *
+ * The given datetime will be cloned and microseconds removed since iCalendar datetimes only work to the second.
+ *
+ * @param DateTimeInterface $start
+ *
+ * @return $this
+ */
+ public function startAt(DateTimeInterface $start): self
+ {
+ $startDate = clone $start;
+ // When the start time contains microseconds, the first recurrence will always be skipped, as
+ // the transformer operates only up to seconds level. See also the upstream issue #155
+ $startDate->setTime($start->format('H'), $start->format('i'), $start->format('s'));
+ // In case start time uses a different tz than what the rrule internally does, we force it to use the same
+ $startDate->setTimezone(new DateTimeZone($this->rrule->getTimezone()));
+
+ $this->rrule->setStartDate($startDate);
+
+ return $this;
+ }
+
+ public function getStart(): ?DateTimeInterface
+ {
+ return $this->rrule->getStartDate();
+ }
+
+ /**
+ * Set the time until this frequency lasts
+ *
+ * The given datetime will be cloned and microseconds removed since iCalendar datetimes only work to the second.
+ *
+ * @param DateTimeInterface $end
+ *
+ * @return $this
+ */
+ public function endAt(DateTimeInterface $end): self
+ {
+ $end = clone $end;
+ $end->setTime($end->format('H'), $end->format('i'), $end->format('s'));
+
+ $this->rrule->setUntil($end);
+
+ return $this;
+ }
+
+ public function getEnd(): ?DateTimeInterface
+ {
+ return $this->rrule->getEndDate() ?? $this->rrule->getUntil();
+ }
+
+ /**
+ * Get the frequency of this rule
+ *
+ * @return string
+ */
+ public function getFrequency(): string
+ {
+ return $this->frequency;
+ }
+
+ /**
+ * Get a set of recurrences relative to the given time
+ *
+ * @param DateTimeInterface $dateTime
+ * @param int $limit Limit the recurrences to be generated to the given value
+ * @param bool $include Whether to include the passed time in the result set
+ *
+ * @return Generator<DateTimeInterface>
+ */
+ public function getNextRecurrences(
+ DateTimeInterface $dateTime,
+ int $limit = self::DEFAULT_LIMIT,
+ bool $include = true
+ ): Generator {
+ $resetTransformerConfig = function (int $limit = self::DEFAULT_LIMIT): void {
+ $this->transformerConfig->setVirtualLimit($limit);
+ $this->transformer->setConfig($this->transformerConfig);
+ };
+
+ if ($limit > self::DEFAULT_LIMIT) {
+ $resetTransformerConfig($limit);
+ }
+
+ $constraint = new AfterConstraint($dateTime, $include);
+ if (! $this->rrule->repeatsIndefinitely()) {
+ // When accessing this method externally (not by using `getNextDue()`), the transformer may
+ // generate recurrences beyond the configured end time.
+ $constraint = new BetweenConstraint($dateTime, $this->getEnd(), $include);
+ }
+
+ // Setting the start date to a date time smaller than now causes the underlying library
+ // not to generate any recurrences when using the regular frequencies such as `MINUTELY` etc.
+ // and the `$countConstraintFailures` is set to true. We need also to tell the transformer
+ // not to count the recurrences that fail the constraint's test!
+ $recurrences = $this->transformer->transform($this->rrule, $constraint, false);
+ foreach ($recurrences as $recurrence) {
+ yield $recurrence->getStart();
+ }
+
+ if ($limit > self::DEFAULT_LIMIT) {
+ $resetTransformerConfig();
+ }
+ }
+
+ public function jsonSerialize(): array
+ {
+ $data = [
+ 'rrule' => $this->rrule->getString(RecurrRule::TZ_FIXED),
+ 'frequency' => $this->frequency
+ ];
+
+ $start = $this->getStart();
+ if ($start) {
+ $data['start'] = $start->format(static::SERIALIZED_DATETIME_FORMAT);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Redirect all public method calls to the underlying rrule object
+ *
+ * @param string $methodName
+ * @param array<mixed> $args
+ *
+ * @return mixed
+ *
+ * @throws BadMethodCallException If the given method doesn't exist or when setter method is called
+ */
+ public function __call(string $methodName, array $args)
+ {
+ if (! method_exists($this->rrule, $methodName)) {
+ throw new BadMethodCallException(
+ sprintf('Call to undefined method %s::%s()', get_php_type($this->rrule), $methodName)
+ );
+ }
+
+ if (strtolower(substr($methodName, 0, 3)) !== 'get') {
+ throw new BadMethodCallException(
+ sprintf('Dynamic method %s is not supported. Only getters (get*) are', $methodName)
+ );
+ }
+
+ return call_user_func_array([$this->rrule, $methodName], $args);
+ }
+}
diff --git a/vendor/ipl/scheduler/src/Scheduler.php b/vendor/ipl/scheduler/src/Scheduler.php
new file mode 100644
index 0000000..25ad3a1
--- /dev/null
+++ b/vendor/ipl/scheduler/src/Scheduler.php
@@ -0,0 +1,323 @@
+<?php
+
+namespace ipl\Scheduler;
+
+use DateTime;
+use InvalidArgumentException;
+use ipl\Scheduler\Common\Promises;
+use ipl\Scheduler\Common\Timers;
+use ipl\Scheduler\Contract\Frequency;
+use ipl\Scheduler\Contract\Task;
+use ipl\Stdlib\Events;
+use React\EventLoop\Loop;
+use React\Promise;
+use React\Promise\ExtendedPromiseInterface;
+use SplObjectStorage;
+use Throwable;
+
+class Scheduler
+{
+ use Events;
+ use Timers;
+ use Promises;
+
+ /**
+ * Event raised when a {@link Task task} is canceled
+ *
+ * The task and its pending operations as an array of canceled {@link ExtendedPromiseInterface promise}s
+ * are passed as parameters to the event callbacks.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on($scheduler::ON_TASK_CANCEL, function (Task $task, array $_) use ($logger) {
+ * $logger->info(sprintf('Task %s cancelled', $task->getName()));
+ * });
+ * ```
+ */
+ public const ON_TASK_CANCEL = 'task-cancel';
+
+ /**
+ * Event raised when an operation of a {@link Task task} is done
+ *
+ * The task and the operation result are passed as parameters to the event callbacks.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on($scheduler::ON_TASK_DONE, function (Task $task, $result) use ($logger) {
+ * $logger->info(sprintf('Operation of task %s done: %s', $task->getName(), $result));
+ * });
+ * ```
+ */
+ public const ON_TASK_DONE = 'task-done';
+
+ /**
+ * Event raised when an operation of a {@link Task task} failed
+ *
+ * The task and the {@link Throwable reason} why the operation failed
+ * are passed as parameters to the event callbacks.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on($scheduler::ON_TASK_FAILED, function (Task $task, Throwable $e) use ($logger) {
+ * $logger->error(
+ * sprintf('Operation of task %s failed: %s', $task->getName(), $e),
+ * ['exception' => $e]
+ * );
+ * });
+ * ```
+ */
+ public const ON_TASK_FAILED = 'task-failed';
+
+ /**
+ * Event raised when a {@link Task task} operation is scheduled
+ *
+ * The task and the {@link DateTime time} when it should run
+ * are passed as parameters to the event callbacks.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on($scheduler::ON_TASK_SCHEDULED, function (Task $task, DateTime $dateTime) use ($logger) {
+ * $logger->info(sprintf(
+ * 'Scheduling task %s to run at %s',
+ * $task->getName(),
+ * IntlDateFormatter::formatObject($dateTime)
+ * ));
+ * });
+ * ```
+ */
+ public const ON_TASK_SCHEDULED = 'task-scheduled';
+
+ /**
+ * Event raised upon operation of a {@link Task task}
+ *
+ * The task and the possibly not yet completed result of the operation as a {@link ExtendedPromiseInterface promise}
+ * are passed as parameters to the event callbacks.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on($scheduler::ON_TASK_OPERATION, function (Task $task, ExtendedPromiseInterface $_) use ($logger) {
+ * $logger->info(sprintf('Task %s operating', $task->getName()));
+ * });
+ * ```
+ */
+ public const ON_TASK_RUN = 'task-run';
+
+ /**
+ * Event raised when a {@see Task task} is expired
+ *
+ * The task and the {@see DateTime expire time} are passed as parameters to the event callbacks.
+ * Note that the expiration time is the first time that is considered expired based on the frequency
+ * of the task and can be later than the specified end time.
+ *
+ * **Example usage:**
+ *
+ * ```php
+ * $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Task $task, DateTime $dateTime) use ($logger) {
+ * $logger->info(sprintf('Removing expired task %s at %s', $task->getName(), $dateTime->format('Y-m-d H:i:s')));
+ * });
+ * ```
+ */
+ public const ON_TASK_EXPIRED = 'task-expired';
+
+ /** @var SplObjectStorage<Task, null> The scheduled tasks of this scheduler */
+ protected $tasks;
+
+ public function __construct()
+ {
+ $this->tasks = new SplObjectStorage();
+
+ $this->promises = new SplObjectStorage();
+ $this->timers = new SplObjectStorage();
+
+ $this->init();
+ }
+
+ /**
+ * Initialize this scheduler
+ */
+ protected function init(): void
+ {
+ }
+
+ /**
+ * Remove and cancel the given task
+ *
+ * @param Task $task
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If the given task isn't scheduled
+ */
+ public function remove(Task $task): self
+ {
+ if (! $this->hasTask($task)) {
+ throw new InvalidArgumentException(sprintf('Task %s not scheduled', $task->getName()));
+ }
+
+ $this->cancelTask($task);
+
+ $this->tasks->detach($task);
+
+ return $this;
+ }
+
+ /**
+ * Remove and cancel all tasks
+ *
+ * @return $this
+ */
+ public function removeTasks(): self
+ {
+ foreach ($this->tasks as $task) {
+ $this->cancelTask($task);
+ }
+
+ $this->tasks = new SplObjectStorage();
+
+ return $this;
+ }
+
+ /**
+ * Get whether the specified task is scheduled
+ *
+ * @param Task $task
+ *
+ * @return bool
+ */
+ public function hasTask(Task $task): bool
+ {
+ return $this->tasks->contains($task);
+ }
+
+ /**
+ * Schedule the given task based on the specified frequency
+ *
+ * @param Task $task
+ * @param Frequency $frequency
+ *
+ * @return $this
+ */
+ public function schedule(Task $task, Frequency $frequency): self
+ {
+ $now = new DateTime();
+ if ($frequency->isExpired($now)) {
+ return $this;
+ }
+
+ if ($frequency->isDue($now)) {
+ Loop::futureTick(function () use ($task): void {
+ $promise = $this->runTask($task);
+ $this->emit(static::ON_TASK_RUN, [$task, $promise]);
+ });
+ $this->emit(static::ON_TASK_SCHEDULED, [$task, $now]);
+
+ if ($frequency instanceof OneOff) {
+ return $this;
+ }
+ }
+
+ $loop = function () use (&$loop, $task, $frequency): void {
+ $promise = $this->runTask($task);
+ $this->emit(static::ON_TASK_RUN, [$task, $promise]);
+
+ $now = new DateTime();
+ $nextDue = $frequency->getNextDue($now);
+ if ($frequency instanceof OneOff || $frequency->isExpired($nextDue)) {
+ $removeTask = function () use ($task, $nextDue): void {
+ $this->remove($task);
+ $this->emit(static::ON_TASK_EXPIRED, [$task, $nextDue]);
+ };
+
+ if ($this->promises->contains($task->getUuid())) {
+ $pendingPromises = (array) $this->promises->offsetGet($task->getUuid());
+ Promise\all($pendingPromises)->always($removeTask);
+ } else {
+ $removeTask();
+ }
+
+ return;
+ }
+
+ $this->attachTimer(
+ $task->getUuid(),
+ Loop::addTimer($nextDue->getTimestamp() - $now->getTimestamp(), $loop)
+ );
+ $this->emit(static::ON_TASK_SCHEDULED, [$task, $nextDue]);
+ };
+
+ $nextDue = $frequency->getNextDue($now);
+ $this->attachTimer(
+ $task->getUuid(),
+ Loop::addTimer($nextDue->getTimestamp() - $now->getTimestamp(), $loop)
+ );
+ $this->emit(static::ON_TASK_SCHEDULED, [$task, $nextDue]);
+
+ $this->tasks->attach($task);
+
+ return $this;
+ }
+
+ public function isValidEvent(string $event): bool
+ {
+ $events = array_flip([
+ static::ON_TASK_CANCEL,
+ static::ON_TASK_DONE,
+ static::ON_TASK_EXPIRED,
+ static::ON_TASK_FAILED,
+ static::ON_TASK_RUN,
+ static::ON_TASK_SCHEDULED
+ ]);
+
+ return isset($events[$event]);
+ }
+
+ /**
+ * Cancel the timer of the task and all pending operations
+ *
+ * @param Task $task
+ */
+ protected function cancelTask(Task $task): void
+ {
+ Loop::cancelTimer($this->detachTimer($task->getUuid()));
+
+ /** @var ExtendedPromiseInterface[] $promises */
+ $promises = $this->detachPromises($task->getUuid());
+ if (! empty($promises)) {
+ /** @var Promise\CancellablePromiseInterface $promise */
+ foreach ($promises as $promise) {
+ $promise->cancel();
+ }
+ $this->emit(self::ON_TASK_CANCEL, [$task, $promises]);
+ }
+ }
+
+ /**
+ * Runs the given task immediately and registers handlers for the returned promise
+ *
+ * @param Task $task
+ *
+ * @return ExtendedPromiseInterface
+ */
+ protected function runTask(Task $task): ExtendedPromiseInterface
+ {
+ $promise = $task->run();
+ $this->addPromise($task->getUuid(), $promise);
+
+ return $promise->then(
+ function ($result) use ($task): void {
+ $this->emit(self::ON_TASK_DONE, [$task, $result]);
+ },
+ function (Throwable $reason) use ($task): void {
+ $this->emit(self::ON_TASK_FAILED, [$task, $reason]);
+ }
+ )->always(function () use ($task, $promise): void {
+ // Unregister the promise without canceling it as it's already resolved
+ $this->removePromise($task->getUuid(), $promise);
+ });
+ }
+}
diff --git a/vendor/ipl/scheduler/src/register_cron_aliases.php b/vendor/ipl/scheduler/src/register_cron_aliases.php
new file mode 100644
index 0000000..2987248
--- /dev/null
+++ b/vendor/ipl/scheduler/src/register_cron_aliases.php
@@ -0,0 +1,11 @@
+<?php
+
+use Cron\CronExpression;
+
+if (! CronExpression::supportsAlias('@minutely')) {
+ CronExpression::registerAlias('@minutely', '* * * * *');
+}
+
+if (! CronExpression::supportsAlias('@quarterly')) {
+ CronExpression::registerAlias('@quarterly', '0 0 1 */3 *');
+}