eca-1.0.x-dev/modules/base/src/Event/CronEvent.php
modules/base/src/Event/CronEvent.php
<?php namespace Drupal\eca_base\Event; use Cron\CronExpression; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\eca\EcaState; use Symfony\Contracts\EventDispatcher\Event; /** * Provides a cron event. * * @internal * This class is not meant to be used as a public API. It is subject for name * change or may be removed completely, also on minor version updates. * * @package Drupal\eca_base\Event */ class CronEvent extends Event { /** * ECA state service. * * @var \Drupal\eca\EcaState */ protected EcaState $state; /** * The date formatter service. * * @var \Drupal\Core\Datetime\DateFormatterInterface */ protected DateFormatterInterface $dateFormatter; /** * The logger channel. * * @var \Drupal\Core\Logger\LoggerChannelInterface */ protected LoggerChannelInterface $logger; /** * List of timestamps keyed by state ID. * * @var int[] */ private static array $lastRun = []; /** * Constructs a new CronEvent object. * * @param \Drupal\eca\EcaState $state * The ECA state service. * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter * The date formatter service. * @param \Drupal\Core\Logger\LoggerChannelInterface $logger * The logger channel. */ public function __construct(EcaState $state, DateFormatterInterface $dateFormatter, LoggerChannelInterface $logger) { $this->state = $state; $this->dateFormatter = $dateFormatter; $this->logger = $logger; } /** * Determines, if the cron with $id is due for next execution. * * It receives the last execution time of this event cron and calculates * by the given frequency, if the next execution time has already been * passed and returns TRUE, if so. * * @param string $id * The id of the modeller event. * @param string $frequency * The frequency as a cron pattern. * * @return bool * TRUE, if the event $id is due for next execution, FALSE otherwise. */ public function isDue(string $id, string $frequency): bool { $currentTime = $this->state->getCurrentTimestamp(); $key = 'cron-' . $id; if (!isset(self::$lastRun[$key])) { self::$lastRun[$key] = $this->state->getTimestamp($key); } $lastRun = self::$lastRun[$key]; // Cron's maximum granularity is on minute level. Therefore we round the // current time to the last passed minute. That way we avoid accidental // concurrent runs. $currentTime -= ($currentTime % 60); $lastRun -= ($lastRun % 60); $nextRun = 0; $due = FALSE; try { $nextRun = $this->getNextRunTimestamp($lastRun, $frequency); $due = $currentTime >= $nextRun; } catch (\Exception $e) { $this->logger->error('Can not determine next run tim for cron: %msg', [ '%msg' => $e->getMessage(), ]); } $this->logger->debug('Cron event assertion: now = %current - last = %last - next = %next - due %due', [ '%current' => $this->dateFormatter->format($currentTime), '%last' => $this->dateFormatter->format($lastRun), '%next' => $this->dateFormatter->format($nextRun), '%due' => $due ? 'yes' : 'no', ]); return $due; } /** * Calculates the timestamp for the next execution. * * @param int $lastRunTimestamp * Timestamp, when it was executed last or 0 if it never ran before. * @param string $frequency * The frequency as a cron pattern. * * @return int * Timestamp for next execution. * * @throws \Exception */ public function getNextRunTimestamp(int $lastRunTimestamp, string $frequency): int { $cron = new CronExpression($frequency); $dt = new \DateTime(); $dt ->setTimezone(new \DateTimeZone('UTC')) ->setTimestamp($lastRunTimestamp); return $cron->getNextRunDate($dt)->getTimestamp(); } /** * Stores the execution time for the modeller event $id in ECA state. * * @param string $id * The id of the modeller event. * @param int|null $timestamp * (optional) The timestamp value to store. When not given, the current time * will be used. */ public function storeTimestamp(string $id, ?int $timestamp = NULL): void { $this->state->setTimestamp('cron-' . $id, $timestamp); } }