diff --git a/composer.json b/composer.json index 0e712c9754c..6e555f092f6 100644 --- a/composer.json +++ b/composer.json @@ -109,6 +109,7 @@ "symfony/dependency-injection": "^5.4 || ^6.0", "symfony/deprecation-contracts": "^2.1 || ^3.0", "symfony/doctrine-bridge": "^5.4 || ^6.0", + "symfony/doctrine-messenger": "^5.4 || ^6.0", "symfony/dom-crawler": "^5.4 || ^6.0", "symfony/dotenv": "^5.4 || ^6.0", "symfony/error-handler": "^5.4 || ^6.0", @@ -126,6 +127,7 @@ "symfony/lock": "^5.4 || ^6.0", "symfony/mailer": "^5.4 || ^6.0", "symfony/maker-bundle": "^1.1", + "symfony/messenger": "^5.4 || ^6.0", "symfony/mime": "^5.4 || ^6.0", "symfony/monolog-bridge": "^5.4 || ^6.0", "symfony/monolog-bundle": "^3.1", diff --git a/core-bundle/composer.json b/core-bundle/composer.json index 9d6d6e42a7a..dc09c7812c7 100644 --- a/core-bundle/composer.json +++ b/core-bundle/composer.json @@ -105,6 +105,7 @@ "symfony/dependency-injection": "^5.4 || ^6.0", "symfony/deprecation-contracts": "^2.1 || ^3.0", "symfony/doctrine-bridge": "^5.4 || ^6.0", + "symfony/doctrine-messenger": "^5.4 || ^6.0", "symfony/dom-crawler": "^5.4 || ^6.0", "symfony/event-dispatcher": "^5.4 || ^6.0", "symfony/event-dispatcher-contracts": "^2.0 || ^3.0", @@ -119,6 +120,7 @@ "symfony/intl": "^5.4 || ^6.0", "symfony/lock": "^5.4 || ^6.0", "symfony/mailer": "^5.4 || ^6.0", + "symfony/messenger": "^5.4 || ^6.0", "symfony/mime": "^5.4 || ^6.0", "symfony/monolog-bridge": "^5.4 || ^6.0", "symfony/password-hasher": "^5.4 || ^6.0", diff --git a/core-bundle/config/listener.yaml b/core-bundle/config/listener.yaml index 75c8d85e67a..b51b6fab5ba 100644 --- a/core-bundle/config/listener.yaml +++ b/core-bundle/config/listener.yaml @@ -37,7 +37,6 @@ services: class: Contao\CoreBundle\EventListener\CommandSchedulerListener arguments: - '@contao.cron' - - '@contao.framework' - '@database_connection' - '%fragment.path%' tags: @@ -205,6 +204,7 @@ services: class: Contao\CoreBundle\EventListener\DoctrineSchemaListener arguments: - '@contao.doctrine.dca_schema_provider' + - '@messenger.receiver_locator' tags: - { name: doctrine.event_listener, event: postGenerateSchema } @@ -463,7 +463,7 @@ services: contao.listener.search_index: class: Contao\CoreBundle\EventListener\SearchIndexListener arguments: - - '@contao.search.indexer' + - '@messenger.bus.default' - '%fragment.path%' tags: - kernel.event_listener diff --git a/core-bundle/config/services.yaml b/core-bundle/config/services.yaml index 91b95a76ee4..40ee2cd0a18 100644 --- a/core-bundle/config/services.yaml +++ b/core-bundle/config/services.yaml @@ -95,8 +95,12 @@ services: arguments: - !service_closure '@contao.repository.cron_job' - !service_closure '@doctrine.orm.entity_manager' + - '@cache.app' - '@?logger' + contao.cron.messenger: + class: Contao\CoreBundle\Cron\MessengerCron + contao.cron.purge_expired_data: class: Contao\CoreBundle\Cron\PurgeExpiredDataCron arguments: @@ -453,6 +457,17 @@ services: arguments: - '@contao.menu.matcher' + contao.messenger.message_handler.search_index: + class: Contao\CoreBundle\Messenger\MessageHandler\SearchIndexMessageHandler + arguments: + - '@contao.search.indexer' + + contao.messenger.transport.cron_fallback_transportFactory: + class: Contao\CoreBundle\Messenger\Transport\CronFallbackTransportFactory + arguments: + - '@contao.cron' + - '@messenger.receiver_locator' + contao.model_argument_resolver: class: Contao\CoreBundle\HttpKernel\ModelArgumentResolver arguments: diff --git a/core-bundle/contao/config/default.php b/core-bundle/contao/config/default.php index e0fc0e6ca99..6ed40227fc8 100644 --- a/core-bundle/contao/config/default.php +++ b/core-bundle/contao/config/default.php @@ -100,7 +100,6 @@ $GLOBALS['TL_CONFIG']['backendTheme'] = 'flexible'; $GLOBALS['TL_CONFIG']['doNotCollapse'] = false; $GLOBALS['TL_CONFIG']['minPasswordLength'] = 8; -$GLOBALS['TL_CONFIG']['disableCron'] = false; $GLOBALS['TL_CONFIG']['defaultFileChmod'] = 0644; $GLOBALS['TL_CONFIG']['defaultFolderChmod'] = 0755; $GLOBALS['TL_CONFIG']['maxPaginationLinks'] = 7; diff --git a/core-bundle/src/Cron/AbstractConsoleCron.php b/core-bundle/src/Cron/AbstractConsoleCron.php new file mode 100644 index 00000000000..58098b08db2 --- /dev/null +++ b/core-bundle/src/Cron/AbstractConsoleCron.php @@ -0,0 +1,44 @@ +getPhpBinary(); + $arguments[] = $this->consolePath; + $arguments = array_merge($arguments, $this->getPhpArguments()); + $arguments[] = $command; + $arguments = array_merge($arguments, $commandArguments); + + return new Process($arguments); + } + + protected function getPhpArguments(): array + { + return []; + } + + private function getPhpBinary(): string + { + if (null === $this->phpBinary) { + $executableFinder = new PhpExecutableFinder(); + $this->phpBinary = $executableFinder->find(); + } + + return $this->phpBinary; + } +} diff --git a/core-bundle/src/Cron/Cron.php b/core-bundle/src/Cron/Cron.php index 1502bb1f904..79c6b758ff8 100644 --- a/core-bundle/src/Cron/Cron.php +++ b/core-bundle/src/Cron/Cron.php @@ -16,10 +16,12 @@ use Contao\CoreBundle\Repository\CronJobRepository; use Cron\CronExpression; use Doctrine\ORM\EntityManagerInterface; +use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; class Cron { + private const MINUTELY_CACHE_KEY = 'contao.cron.minutely_run'; final public const SCOPE_WEB = 'web'; final public const SCOPE_CLI = 'cli'; @@ -32,7 +34,7 @@ class Cron * @param \Closure():CronJobRepository $repository * @param \Closure():EntityManagerInterface $entityManager */ - public function __construct(private \Closure $repository, private \Closure $entityManager, private LoggerInterface|null $logger = null) + public function __construct(private \Closure $repository, private \Closure $entityManager, private CacheItemPoolInterface $cachePool, private LoggerInterface|null $logger = null) { } @@ -49,6 +51,13 @@ public function getCronJobs(): array return $this->cronJobs; } + public function hasMinutelyCliCron(): bool + { + $item = $this->cachePool->getItem(self::MINUTELY_CACHE_KEY); + + return $item->isHit(); + } + /** * Run all the registered Contao cron jobs. */ @@ -83,6 +92,12 @@ private function doRun(array $cronJobs, string $scope, bool $force = false): voi throw new \InvalidArgumentException('Invalid scope "'.$scope.'"'); } + if (self::SCOPE_CLI === $scope) { + $cacheItem = $this->cachePool->getItem(self::MINUTELY_CACHE_KEY); + $cacheItem->expiresAfter(60); // 60 seconds + $this->cachePool->save($cacheItem); + } + /** @var CronJobRepository $repository */ $repository = ($this->repository)(); diff --git a/core-bundle/src/Cron/MessengerCron.php b/core-bundle/src/Cron/MessengerCron.php new file mode 100644 index 00000000000..98c880b3e7d --- /dev/null +++ b/core-bundle/src/Cron/MessengerCron.php @@ -0,0 +1,71 @@ +numberOfWorkers < 1) { + return; + } + + $processes = []; + + for ($i = 0; $i < $this->numberOfWorkers; ++$i) { + $process = $this->createProcess( + 'messenger:consume', + '--time-limit=60', // Minutely cronjob running for one minute max + 'contao_prio_high', + 'contao_prio_normal', + 'contao_prio_low' + ); + $process->setTimeout(65); + + // Start the job asynchronously + $process->start(); + $processes[] = $process; + } + + // Now we need to sleep to keep the parent process open. Otherwise, this script will end and thus kill + // our child processes. + // All jobs run for 60 seconds, so we don't need to check every second yet + sleep(55); + + // Now we check every second if all processes are done + while (true) { + $allDone = true; + + foreach ($processes as $process) { + if ($process->isRunning()) { + $allDone = false; + break; + } + } + + if ($allDone) { + break; + } + + sleep(1); + } + } +} diff --git a/core-bundle/src/DependencyInjection/Configuration.php b/core-bundle/src/DependencyInjection/Configuration.php index e15cd63898e..a4c0a5b4fe4 100644 --- a/core-bundle/src/DependencyInjection/Configuration.php +++ b/core-bundle/src/DependencyInjection/Configuration.php @@ -104,6 +104,7 @@ static function (array $options): array { ->always(static fn (string $value): string => Path::canonicalize($value)) ->end() ->end() + ->append($this->addWorkerNode()) ->append($this->addImageNode()) ->append($this->addSecurityNode()) ->append($this->addSearchNode()) @@ -119,6 +120,24 @@ static function (array $options): array { return $treeBuilder; } + private function addWorkerNode(): NodeDefinition + { + return (new TreeBuilder('worker')) + ->getRootNode() + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('console_path') + ->info('The path to the Symfony console.') + ->defaultValue('%kernel.project_dir%/bin/console') + ->end() + ->scalarNode('quantity') + ->info('The number of workers to run. Use 0 to disable the worker.') + ->defaultValue(1) + ->end() + ->end() + ; + } + private function addImageNode(): NodeDefinition { return (new TreeBuilder('image')) diff --git a/core-bundle/src/DependencyInjection/ContaoCoreExtension.php b/core-bundle/src/DependencyInjection/ContaoCoreExtension.php index f97f927e111..d00164f9510 100644 --- a/core-bundle/src/DependencyInjection/ContaoCoreExtension.php +++ b/core-bundle/src/DependencyInjection/ContaoCoreExtension.php @@ -132,6 +132,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('contao.insert_tags.allowed_tags', $config['insert_tags']['allowed_tags']); $container->setParameter('contao.sanitizer.allowed_url_protocols', $config['sanitizer']['allowed_url_protocols']); + $this->handleWorkerConfig($config, $container); $this->handleSearchConfig($config, $container); $this->handleCrawlConfig($config, $container); $this->setPredefinedImageSizes($config, $container); @@ -223,6 +224,17 @@ public function configureFilesystem(FilesystemConfiguration $config): void ; } + private function handleWorkerConfig(array $config, ContainerBuilder $container): void + { + if (!$container->hasDefinition('contao.cron.messenger')) { + return; + } + + $cron = $container->getDefinition('contao.cron.messenger'); + $cron->setArgument(0, $config['worker']['console_path']); + $cron->setArgument(1, $config['worker']['quantity']); + } + private function handleSearchConfig(array $config, ContainerBuilder $container): void { $container diff --git a/core-bundle/src/EventListener/CommandSchedulerListener.php b/core-bundle/src/EventListener/CommandSchedulerListener.php index 68851531e23..de51ac278dd 100644 --- a/core-bundle/src/EventListener/CommandSchedulerListener.php +++ b/core-bundle/src/EventListener/CommandSchedulerListener.php @@ -12,9 +12,7 @@ namespace Contao\CoreBundle\EventListener; -use Contao\Config; use Contao\CoreBundle\Cron\Cron; -use Contao\CoreBundle\Framework\ContaoFramework; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; use Symfony\Component\HttpFoundation\Request; @@ -27,7 +25,6 @@ class CommandSchedulerListener { public function __construct( private Cron $cron, - private ContaoFramework $framework, private Connection $connection, private string $fragmentPath = '_fragment', ) { @@ -38,7 +35,12 @@ public function __construct( */ public function __invoke(TerminateEvent $event): void { - if ($this->framework->isInitialized() && $this->canRunCron($event->getRequest())) { + // If we have a real minutely CLI cron, we don't need this listener. + if ($this->cron->hasMinutelyCliCron()) { + return; + } + + if ($this->canRunCron($event->getRequest())) { $this->cron->run(Cron::SCOPE_WEB); } } @@ -52,9 +54,7 @@ private function canRunCron(Request $request): bool return false; } - $config = $this->framework->getAdapter(Config::class); - - return !$config->get('disableCron') && $this->canRunDbQuery(); + return $this->canRunDbQuery(); } /** diff --git a/core-bundle/src/EventListener/DoctrineSchemaListener.php b/core-bundle/src/EventListener/DoctrineSchemaListener.php index 6a7a4400bad..80360f31f18 100644 --- a/core-bundle/src/EventListener/DoctrineSchemaListener.php +++ b/core-bundle/src/EventListener/DoctrineSchemaListener.php @@ -14,13 +14,15 @@ use Contao\CoreBundle\Doctrine\Schema\DcaSchemaProvider; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Psr\Container\ContainerInterface; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; /** * @internal */ class DoctrineSchemaListener { - public function __construct(private DcaSchemaProvider $provider) + public function __construct(private DcaSchemaProvider $provider, private ContainerInterface $messengerTransportLocator) { } @@ -30,5 +32,19 @@ public function __construct(private DcaSchemaProvider $provider) public function postGenerateSchema(GenerateSchemaEventArgs $event): void { $this->provider->appendToSchema($event->getSchema()); + + foreach (['contao_prio_high', 'contao_prio_medium', 'contao_prio_low'] as $transportName) { + if (!$this->messengerTransportLocator->has($transportName)) { + continue; + } + + $transport = $this->messengerTransportLocator->get($transportName); + + if (!$transport instanceof DoctrineTransport) { + continue; + } + + $transport->configureSchema($event->getSchema(), $event->getEntityManager()->getConnection()); + } } } diff --git a/core-bundle/src/EventListener/SearchIndexListener.php b/core-bundle/src/EventListener/SearchIndexListener.php index b4032729bc2..721457fac61 100644 --- a/core-bundle/src/EventListener/SearchIndexListener.php +++ b/core-bundle/src/EventListener/SearchIndexListener.php @@ -13,12 +13,12 @@ namespace Contao\CoreBundle\EventListener; use Contao\CoreBundle\Crawl\Escargot\Factory; +use Contao\CoreBundle\Messenger\Message\SearchIndexMessage; use Contao\CoreBundle\Search\Document; -use Contao\CoreBundle\Search\Indexer\IndexerException; -use Contao\CoreBundle\Search\Indexer\IndexerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\TerminateEvent; +use Symfony\Component\Messenger\MessageBusInterface; /** * @internal @@ -29,7 +29,7 @@ class SearchIndexListener final public const FEATURE_DELETE = 0b10; public function __construct( - private IndexerInterface $indexer, + private MessageBusInterface $messageBus, private string $fragmentPath = '_fragment', private int $enabledFeatures = self::FEATURE_INDEX | self::FEATURE_DELETE, ) { @@ -50,18 +50,14 @@ public function __invoke(TerminateEvent $event): void $document = Document::createFromRequestResponse($request, $response); $needsIndex = $this->needsIndex($request, $response, $document); - try { - $success = $event->getResponse()->isSuccessful(); + $success = $event->getResponse()->isSuccessful(); - if ($needsIndex && $success && $this->enabledFeatures & self::FEATURE_INDEX) { - $this->indexer->index($document); - } + if ($needsIndex && $success && $this->enabledFeatures & self::FEATURE_INDEX) { + $this->messageBus->dispatch(SearchIndexMessage::createWithIndex($document)); + } - if (!$success && $this->enabledFeatures & self::FEATURE_DELETE) { - $this->indexer->delete($document); - } - } catch (IndexerException) { - // ignore + if (!$success && $this->enabledFeatures & self::FEATURE_DELETE) { + $this->messageBus->dispatch(SearchIndexMessage::createWithDelete($document)); } } diff --git a/core-bundle/src/Messenger/Message/HighPrioMessageInterface.php b/core-bundle/src/Messenger/Message/HighPrioMessageInterface.php new file mode 100644 index 00000000000..3f082181ce1 --- /dev/null +++ b/core-bundle/src/Messenger/Message/HighPrioMessageInterface.php @@ -0,0 +1,17 @@ +action; + } + + public function shouldIndex(): bool + { + return self::ACTION_INDEX === $this->action; + } + + public function getDocument(): Document + { + return $this->document; + } + + public static function createWithDelete(Document $document): self + { + return new self($document, self::ACTION_DELETE); + } + + public static function createWithIndex(Document $document): self + { + return new self($document, self::ACTION_INDEX); + } +} diff --git a/core-bundle/src/Messenger/MessageHandler/SearchIndexMessageHandler.php b/core-bundle/src/Messenger/MessageHandler/SearchIndexMessageHandler.php new file mode 100644 index 00000000000..0c94f4f861e --- /dev/null +++ b/core-bundle/src/Messenger/MessageHandler/SearchIndexMessageHandler.php @@ -0,0 +1,35 @@ +shouldIndex()) { + $this->indexer->index($message->getDocument()); + } + + if ($message->shouldDelete()) { + $this->indexer->delete($message->getDocument()); + } + } +} diff --git a/core-bundle/src/Messenger/Transport/CronFallbackTransport.php b/core-bundle/src/Messenger/Transport/CronFallbackTransport.php new file mode 100644 index 00000000000..c8876a6c700 --- /dev/null +++ b/core-bundle/src/Messenger/Transport/CronFallbackTransport.php @@ -0,0 +1,64 @@ +cron->hasMinutelyCliCron()) { + return $this->target->get(); + } + + return $this->fallback->get(); + } + + public function ack(Envelope $envelope): void + { + if ($this->cron->hasMinutelyCliCron()) { + $this->target->ack($envelope); + + return; + } + + $this->fallback->ack($envelope); + } + + public function reject(Envelope $envelope): void + { + if ($this->cron->hasMinutelyCliCron()) { + $this->target->reject($envelope); + + return; + } + + $this->fallback->reject($envelope); + } + + public function send(Envelope $envelope): Envelope + { + if ($this->cron->hasMinutelyCliCron()) { + return $this->target->send($envelope); + } + + return $this->fallback->send($envelope); + } +} diff --git a/core-bundle/src/Messenger/Transport/CronFallbackTransportFactory.php b/core-bundle/src/Messenger/Transport/CronFallbackTransportFactory.php new file mode 100644 index 00000000000..e988a287921 --- /dev/null +++ b/core-bundle/src/Messenger/Transport/CronFallbackTransportFactory.php @@ -0,0 +1,57 @@ +messengerTransportLocator->has($target)) { + throw new InvalidArgumentException(sprintf('The given Contao Cron Fallback Transport target "%s" is invalid.', $target)); + } + + if (!$this->messengerTransportLocator->has($fallback)) { + throw new InvalidArgumentException(sprintf('The given Contao Cron Fallback Transport fallback "%s" is invalid.', $fallback)); + } + + return new CronFallbackTransport( + $this->cron, + $this->messengerTransportLocator->get($target), + $this->messengerTransportLocator->get($fallback), + ); + } + + public function supports(string $dsn, array $options): bool + { + return str_starts_with($dsn, 'contao_cron_fallback://'); + } +} diff --git a/core-bundle/src/Search/Document.php b/core-bundle/src/Search/Document.php index c625920f361..4888c231d31 100644 --- a/core-bundle/src/Search/Document.php +++ b/core-bundle/src/Search/Document.php @@ -244,4 +244,22 @@ private function filterJsonLdContexts(array $data, array $contexts): array return $found ? $newData : []; } + + public function __serialize(): array + { + return [ + 'uri' => $this->uri, + 'statusCode' => $this->statusCode, + 'headers' => $this->headers, + 'body' => $this->body, + ]; + } + + public function __unserialize(array $data): void + { + $this->uri = $data['uri']; + $this->statusCode = $data['statusCode']; + $this->headers = $data['headers']; + $this->body = $data['body']; + } } diff --git a/core-bundle/tests/Cron/CronTest.php b/core-bundle/tests/Cron/CronTest.php index dd1807d2e0f..9fe929f2e99 100644 --- a/core-bundle/tests/Cron/CronTest.php +++ b/core-bundle/tests/Cron/CronTest.php @@ -20,6 +20,7 @@ use Contao\CoreBundle\Repository\CronJobRepository; use Contao\CoreBundle\Tests\TestCase; use Doctrine\ORM\EntityManagerInterface; +use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerInterface; class CronTest extends TestCase @@ -34,7 +35,8 @@ public function testExecutesAddedCronJob(): void $cron = new Cron( fn () => $this->createMock(CronJobRepository::class), - fn () => $this->createMock(EntityManagerInterface::class) + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) ); $cron->addCronJob(new CronJob($cronjob, '@hourly', 'onHourly')); @@ -51,7 +53,8 @@ public function testExecutesSingleCronJob(): void $cron = new Cron( fn () => $this->createMock(CronJobRepository::class), - fn () => $this->createMock(EntityManagerInterface::class) + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) ); $cron->addCronJob(new CronJob($cronjob, '@hourly', 'onHourly')); @@ -89,6 +92,7 @@ public function testLoggingOfExecutedCronJobs(): void $cron = new Cron( fn () => $this->createMock(CronJobRepository::class), fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class), $logger ); @@ -140,7 +144,11 @@ public function testUpdatesCronEntities(): void ->method('flush') ; - $cron = new Cron(static fn () => $repository, static fn () => $manager); + $cron = new Cron( + static fn () => $repository, + static fn () => $manager, + $this->createMock(CacheItemPoolInterface::class) + ); $cron->addCronJob(new CronJob($cronjob, '@hourly', 'onHourly')); $cron->run(Cron::SCOPE_CLI); } @@ -156,7 +164,8 @@ public function testSetsScope(): void $cron = new Cron( fn () => $this->createMock(CronJobRepository::class), - fn () => $this->createMock(EntityManagerInterface::class) + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) ); $cron->addCronJob(new CronJob($cronjob, '@hourly')); @@ -167,7 +176,8 @@ public function testInvalidArgumentExceptionForScope(): void { $cron = new Cron( fn () => $this->createMock(CronJobRepository::class), - fn () => $this->createMock(EntityManagerInterface::class) + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) ); try { @@ -189,7 +199,8 @@ static function (): never { }, static function (): never { throw new \LogicException(); - } + }, + $this->createMock(CacheItemPoolInterface::class) ); $this->expectException(\LogicException::class); @@ -234,7 +245,11 @@ public function testDoesNotRunCronJobIfAlreadyRun(): void ->method('onHourly') ; - $cron = new Cron(static fn () => $repository, fn () => $this->createMock(EntityManagerInterface::class)); + $cron = new Cron( + static fn () => $repository, + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) + ); $cron->addCronJob(new CronJob($cronjob, '@hourly', 'onHourly')); $cron->run(Cron::SCOPE_CLI); } @@ -276,7 +291,11 @@ public function testForcesCronJobToBeRunIfAlreadyRun(): void ->method('onHourly') ; - $cron = new Cron(static fn () => $repository, fn () => $this->createMock(EntityManagerInterface::class)); + $cron = new Cron( + static fn () => $repository, + fn () => $this->createMock(EntityManagerInterface::class), + $this->createMock(CacheItemPoolInterface::class) + ); $cron->addCronJob(new CronJob($cronjob, '@hourly', 'onHourly')); $cron->run(Cron::SCOPE_CLI, true); } diff --git a/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php b/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php index 33af49e4f97..d1374ef4cb0 100644 --- a/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php +++ b/core-bundle/tests/EventListener/CommandSchedulerListenerTest.php @@ -12,10 +12,8 @@ namespace Contao\CoreBundle\Tests\EventListener; -use Contao\Config; use Contao\CoreBundle\Cron\Cron; use Contao\CoreBundle\EventListener\CommandSchedulerListener; -use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Tests\TestCase; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\DriverException; @@ -37,33 +35,10 @@ public function testRunsTheCommandScheduler(): void ->with(Cron::SCOPE_WEB) ; - $listener = new CommandSchedulerListener($cron, $this->mockContaoFramework(), $this->mockConnection()); + $listener = new CommandSchedulerListener($cron, $this->mockConnection()); $listener($this->getTerminateEvent('contao_frontend')); } - public function testDoesNotRunTheCommandSchedulerIfTheContaoFrameworkIsNotInitialized(): void - { - $cron = $this->createMock(Cron::class); - $cron - ->expects($this->never()) - ->method('run') - ; - - $framework = $this->createMock(ContaoFramework::class); - $framework - ->method('isInitialized') - ->willReturn(false) - ; - - $framework - ->expects($this->never()) - ->method('getAdapter') - ; - - $listener = new CommandSchedulerListener($cron, $framework, $this->mockConnection()); - $listener($this->getTerminateEvent('contao_backend')); - } - public function testDoesNotRunTheCommandSchedulerUponFragmentRequests(): void { $cron = $this->createMock(Cron::class); @@ -72,12 +47,6 @@ public function testDoesNotRunTheCommandSchedulerUponFragmentRequests(): void ->method('run') ; - $framework = $this->mockContaoFramework(); - $framework - ->expects($this->never()) - ->method('getAdapter') - ; - $ref = new \ReflectionClass(Request::class); /** @var Request $request */ @@ -88,48 +57,12 @@ public function testDoesNotRunTheCommandSchedulerUponFragmentRequests(): void $event = new TerminateEvent($this->createMock(KernelInterface::class), $request, new Response()); - $listener = new CommandSchedulerListener($cron, $framework, $this->mockConnection()); + $listener = new CommandSchedulerListener($cron, $this->mockConnection()); $listener($event); } - public function testDoesNotRunTheCommandSchedulerIfCronjobsAreDisabled(): void - { - $cron = $this->createMock(Cron::class); - $cron - ->expects($this->never()) - ->method('run') - ; - - $adapter = $this->mockAdapter(['isComplete', 'get']); - $adapter - ->method('isComplete') - ->willReturn(true) - ; - - $adapter - ->method('get') - ->with('disableCron') - ->willReturn(true) - ; - - $framework = $this->mockContaoFramework([Config::class => $adapter]); - $framework - ->expects($this->never()) - ->method('createInstance') - ; - - $listener = new CommandSchedulerListener($cron, $framework, $this->mockConnection()); - $listener($this->getTerminateEvent('contao_frontend')); - } - public function testDoesNotRunTheCommandSchedulerIfThereIsADatabaseConnectionError(): void { - $framework = $this->mockContaoFramework(); - $framework - ->expects($this->once()) - ->method('getAdapter') - ; - $cron = $this->createMock(Cron::class); $cron ->expects($this->never()) @@ -142,7 +75,7 @@ public function testDoesNotRunTheCommandSchedulerIfThereIsADatabaseConnectionErr ->willThrowException($this->createMock(DriverException::class)) ; - $listener = new CommandSchedulerListener($cron, $framework, $connection); + $listener = new CommandSchedulerListener($cron, $connection); $listener($this->getTerminateEvent('contao_backend')); } diff --git a/core-bundle/tests/EventListener/DoctrineSchemaListenerTest.php b/core-bundle/tests/EventListener/DoctrineSchemaListenerTest.php index b276f8edca3..d2c911b87e8 100644 --- a/core-bundle/tests/EventListener/DoctrineSchemaListenerTest.php +++ b/core-bundle/tests/EventListener/DoctrineSchemaListenerTest.php @@ -18,6 +18,7 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Psr\Container\ContainerInterface; class DoctrineSchemaListenerTest extends DoctrineTestCase { @@ -43,7 +44,7 @@ public function testAppendsToAnExistingSchema(): void $this->mockDoctrineRegistry() ); - $listener = new DoctrineSchemaListener($dcaSchemaProvider); + $listener = new DoctrineSchemaListener($dcaSchemaProvider, $this->createMock(ContainerInterface::class)); $listener->postGenerateSchema($event); $this->assertTrue($schema->hasTable('tl_files')); diff --git a/core-bundle/tests/EventListener/SearchIndexListenerTest.php b/core-bundle/tests/EventListener/SearchIndexListenerTest.php index 025822d6830..85ffdaae439 100644 --- a/core-bundle/tests/EventListener/SearchIndexListenerTest.php +++ b/core-bundle/tests/EventListener/SearchIndexListenerTest.php @@ -14,14 +14,15 @@ use Contao\CoreBundle\Crawl\Escargot\Factory; use Contao\CoreBundle\EventListener\SearchIndexListener; -use Contao\CoreBundle\Search\Document; -use Contao\CoreBundle\Search\Indexer\IndexerInterface; +use Contao\CoreBundle\Messenger\Message\SearchIndexMessage; use Contao\CoreBundle\Tests\TestCase; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBusInterface; class SearchIndexListenerTest extends TestCase { @@ -30,22 +31,26 @@ class SearchIndexListenerTest extends TestCase */ public function testIndexesOrDeletesTheDocument(Request $request, Response $response, int $features, bool $index, bool $delete): void { - $indexer = $this->createMock(IndexerInterface::class); - $indexer - ->expects($index ? $this->once() : $this->never()) - ->method('index') - ->with($this->isInstanceOf(Document::class)) - ; - - $indexer - ->expects($delete ? $this->once() : $this->never()) - ->method('delete') - ->with($this->isInstanceOf(Document::class)) + $dispatchCount = (int) $index + (int) $delete; + $messenger = $this->createMock(MessageBusInterface::class); + + $messenger + ->expects($this->exactly($dispatchCount)) + ->method('dispatch') + ->with($this->callback( + function (SearchIndexMessage $message) use ($index, $delete) { + $this->assertTrue($index === $message->shouldIndex()); + $this->assertTrue($delete === $message->shouldDelete()); + + return true; + } + )) + ->willReturnCallback(static fn (SearchIndexMessage $message) => new Envelope($message)) ; $event = new TerminateEvent($this->createMock(HttpKernelInterface::class), $request, $response); - $listener = new SearchIndexListener($indexer, '_fragment', $features); + $listener = new SearchIndexListener($messenger, '_fragment', $features); $listener($event); } diff --git a/core-bundle/tests/Functional/app/config/config_test.yaml b/core-bundle/tests/Functional/app/config/config_test.yaml index b23e158f9e7..41fa389e6ad 100644 --- a/core-bundle/tests/Functional/app/config/config_test.yaml +++ b/core-bundle/tests/Functional/app/config/config_test.yaml @@ -43,8 +43,6 @@ doctrine: auto_generate_proxy_classes: true contao: - localconfig: - disableCron: true search: default_indexer: enable: false diff --git a/manager-bundle/skeleton/config/config.yaml b/manager-bundle/skeleton/config/config.yaml index 66ab4681ecd..8f3f102f98e 100644 --- a/manager-bundle/skeleton/config/config.yaml +++ b/manager-bundle/skeleton/config/config.yaml @@ -31,10 +31,26 @@ framework: adapter: cache.app doctrine.system_cache_pool: adapter: cache.system + messenger: + transports: + sync: 'sync://' + contao_prio_high: 'contao_cron_fallback://contao_prio_high_doctrine?fallback=sync' + contao_prio_normal: 'contao_cron_fallback://contao_prio_normal_doctrine?fallback=sync' + contao_prio_low: 'contao_cron_fallback://contao_prio_low_doctrine?fallback=sync' + contao_prio_high_doctrine: 'doctrine://default?table_name=contao_queue&queue_name=prio_high&auto_setup=false' + contao_prio_normal_doctrine: 'doctrine://default?table_name=contao_queue&queue_name=prio_normal&auto_setup=false' + contao_prio_low_doctrine: 'doctrine://default?table_name=contao_queue&queue_name=prio_low&auto_setup=false' + routing: + 'Symfony\Component\Mailer\Messenger\SendEmailMessage': contao_prio_low + 'Contao\CoreBundle\Messenger\Message\HighPrioMessageInterface': contao_prio_high + 'Contao\CoreBundle\Messenger\Message\NormalPrioMessageInterface': contao_prio_normal + 'Contao\CoreBundle\Messenger\Message\LowPrioMessageInterface': contao_prio_low # Contao configuration contao: preview_script: /preview.php + worker: + console_path: '%kernel.project_dir%/vendor/bin/contao-console' # Twig configuration twig: