<?phpdeclare(strict_types=1);/* * This file is part of Contao. * * (c) Leo Feyer * * @license LGPL-3.0-or-later */namespace Contao\CoreBundle\Cron;use Contao\CoreBundle\Entity\CronJob as CronJobEntity;use Contao\CoreBundle\Exception\CronExecutionSkippedException;use Contao\CoreBundle\Repository\CronJobRepository;use Cron\CronExpression;use Doctrine\DBAL\Exception\LockWaitTimeoutException;use Doctrine\ORM\EntityManagerInterface;use Psr\Log\LoggerInterface;class Cron{ public const SCOPE_WEB = 'web'; public const SCOPE_CLI = 'cli'; /** * @var \Closure():CronJobRepository */ private \Closure $repository; /** * @var \Closure():EntityManagerInterface */ private \Closure $entityManager; private ?LoggerInterface $logger; /** * @var array<CronJob> */ private array $cronJobs = []; /** * @param \Closure():CronJobRepository $repository * @param \Closure():EntityManagerInterface $entityManager */ public function __construct(\Closure $repository, \Closure $entityManager, ?LoggerInterface $logger = null) { $this->repository = $repository; $this->entityManager = $entityManager; $this->logger = $logger; } public function addCronJob(CronJob $cronjob): void { $this->cronJobs[] = $cronjob; } /** * Run all the registered Contao cron jobs. */ public function run(string $scope): void { // Validate scope if (self::SCOPE_WEB !== $scope && self::SCOPE_CLI !== $scope) { throw new \InvalidArgumentException('Invalid scope "'.$scope.'"'); } /** @var CronJobRepository $repository */ $repository = ($this->repository)(); /** @var EntityManagerInterface $entityManager */ $entityManager = ($this->entityManager)(); /** @var array<CronJob> $cronJobsToBeRun */ $cronJobsToBeRun = []; $now = new \DateTimeImmutable(); // Return if another cron process is already running try { $repository->lockTable(); } catch (LockWaitTimeoutException $e) { return; } try { // Go through each cron job foreach ($this->cronJobs as $cron) { $interval = $cron->getInterval(); $name = $cron->getName(); // Determine the last run date $lastRunDate = null; $lastRunEntity = $repository->findOneByName($name); if (null !== $lastRunEntity) { $lastRunDate = $lastRunEntity->getLastRun(); } else { $lastRunEntity = new CronJobEntity($name); $entityManager->persist($lastRunEntity); } // Check if the cron should be run $expression = CronExpression::factory($interval); if (null !== $lastRunDate && $now < $expression->getNextRunDate($lastRunDate)) { continue; } // Store the previous run in case the cronjob skips itself $cron->setPreviousRun($lastRunEntity->getLastRun()); // Update the cron entry $lastRunEntity->setLastRun($now); // Add job to the cron jobs to be run $cronJobsToBeRun[] = $cron; } $entityManager->flush(); } finally { $repository->unlockTable(); } $exception = null; // Execute all cron jobs to be run foreach ($cronJobsToBeRun as $cron) { try { if (null !== $this->logger) { $this->logger->debug(sprintf('Executing cron job "%s"', $cron->getName())); } $cron($scope); } catch (CronExecutionSkippedException $e) { // Restore previous run date in case cronjob skips itself $lastRunEntity = $repository->findOneByName($cron->getName()); $lastRunEntity->setLastRun($cron->getPreviousRun()); $entityManager->flush(); } catch (\Throwable $e) { // Catch any exceptions so that other cronjobs are still executed if (null !== $this->logger) { $this->logger->error((string) $e); } if (null === $exception) { $exception = $e; } } } // Throw the first exception if (null !== $exception) { throw $exception; } }}