diff --git a/composer.json b/composer.json index e6e0fdc..ec1abc2 100644 --- a/composer.json +++ b/composer.json @@ -14,14 +14,13 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpstan/extension-installer": "^1.4.1", - "phpstan/phpstan": "^1.11.9", - "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.3", + "phpstan/phpstan-deprecation-rules": "^1.2.1", "phpstan/phpstan-strict-rules": "^1.6.0", - "rector/rector": "^1.2.2", - "roave/security-advisories": "dev-latest", - "sentry/sentry": "^4.8.1", - "symfony/http-client": "^6.4.10" + "rector/rector": "^1.2.5", + "sentry/sentry": "^4.9.0", + "symfony/http-client": "^6.4.11" }, "license": "MIT", "authors": [ diff --git a/src/Command/DoPopulateIndex.php b/src/Command/DoPopulateIndex.php deleted file mode 100644 index 500bbdf..0000000 --- a/src/Command/DoPopulateIndex.php +++ /dev/null @@ -1,166 +0,0 @@ -setName(CommandConstants::COMMAND_DO_POPULATE_INDEX) - ->setHidden(true) - ->setDescription('[INTERNAL]') - ->addOption(CommandConstants::OPTION_CONFIG, mode: InputOption::VALUE_REQUIRED) - ->addOption(CommandConstants::OPTION_INDEX, mode: InputOption::VALUE_REQUIRED) - ->addOption(CommandConstants::OPTION_BATCH_NUMBER, mode: InputOption::VALUE_REQUIRED) - ->addOption(CommandConstants::OPTION_LISTING_COUNT, mode: InputOption::VALUE_REQUIRED) - ->addOption(CommandConstants::OPTION_DOCUMENT, mode: InputOption::VALUE_REQUIRED) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $indexConfig = $this->getIndex(); - - if (!$indexConfig instanceof IndexInterface) { - return self::FAILURE; - } - - return $this->populateIndex($indexConfig, $indexConfig->getBlueGreenInactiveElasticaIndex()); - } - - private function getIndex(): ?IndexInterface - { - foreach ($this->indexRepository->flattenedAll() as $indexConfig) { - if ($indexConfig->getName() === $this->input->getOption(CommandConstants::OPTION_CONFIG)) { - return $indexConfig; - } - } - - return null; - } - - private function populateIndex(IndexInterface $indexConfig, ElasticaIndex $esIndex): int - { - ProgressBar::setFormatDefinition('custom', "%percent%%\t%remaining%\t%memory%\n%message%"); - - $batchNumber = (int) $this->input->getOption(CommandConstants::OPTION_BATCH_NUMBER); - $listingCount = (int) $this->input->getOption(CommandConstants::OPTION_LISTING_COUNT); - - $allowedDocuments = $indexConfig->getAllowedDocuments(); - $document = $this->input->getOption(CommandConstants::OPTION_DOCUMENT); - - if (!in_array($document, $allowedDocuments, true)) { - return self::FAILURE; - } - - $progressBar = new ProgressBar($this->output, $listingCount > 0 ? $listingCount : 1); - $progressBar->setMessage($document); - $progressBar->setFormat('custom'); - $progressBar->setProgress($batchNumber * $indexConfig->getBatchSize()); - - if (!$indexConfig->shouldPopulateInSubprocesses()) { - $numberOfBatches = ceil($listingCount / $indexConfig->getBatchSize()); - - for ($batch = 0; $batch < $numberOfBatches; $batch++) { - $exitCode = $this->doPopulateIndex($esIndex, $indexConfig, $progressBar, $document, $batch); - - if ($exitCode !== self::SUCCESS) { - return $exitCode; - } - } - } else { - return $this->doPopulateIndex($esIndex, $indexConfig, $progressBar, $document, $batchNumber); - } - - return self::SUCCESS; - } - - private function doPopulateIndex( - ElasticaIndex $esIndex, - IndexInterface $indexConfig, - ProgressBar $progressBar, - string $document, - int $batchNumber, - ): int { - $documentInstance = $this->documentRepository->get($document); - - $this->documentHelper->setTenantIfNeeded($documentInstance, $indexConfig); - - $batchSize = $indexConfig->getBatchSize(); - - $listing = $documentInstance->getListingInstance($indexConfig); - $listing->setOffset($batchNumber * $batchSize); - $listing->setLimit($batchSize); - - $esDocuments = []; - - foreach ($listing->getData() ?? [] as $dataObject) { - try { - if (!$documentInstance->shouldIndex($dataObject)) { - continue; - } - $progressBar->advance(); - - $esDocuments[] = $this->documentHelper->elementToDocument($documentInstance, $dataObject); - } catch (\Throwable $throwable) { - $this->displayDocumentError($indexConfig, $document, $dataObject, $throwable); - - if (!$this->configurationRepository->shouldSkipFailingDocuments()) { - throw new DocumentFailedException($throwable); - } - } - } - - if (count($esDocuments) > 0) { - $esIndex->addDocuments($esDocuments); - $esDocuments = []; - } - - if ($indexConfig->refreshIndexAfterEveryDocumentWhenPopulating()) { - $esIndex->refresh(); - } - - return self::SUCCESS; - } - - private function displayDocumentError( - IndexInterface $indexConfig, - string $document, - AbstractElement $dataObject, - \Throwable $throwable, - ): void { - $this->output->writeln(''); - $this->output->writeln(sprintf( - 'Error while populating index %s, processing documents of type %s, last processed element ID %s.', - $indexConfig::class, - $document, - $dataObject->getId() - )); - $this->displayThrowable($throwable); - } -} diff --git a/src/Command/Index.php b/src/Command/Index.php index 2bfe880..c29be79 100644 --- a/src/Command/Index.php +++ b/src/Command/Index.php @@ -4,20 +4,34 @@ namespace Valantic\ElasticaBridgeBundle\Command; -use Elastica\Index as ElasticaIndex; +use Pimcore\Model\Element\AbstractElement; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Process\Process; +use Symfony\Component\Lock\Key; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBusInterface; use Valantic\ElasticaBridgeBundle\Constant\CommandConstants; use Valantic\ElasticaBridgeBundle\Elastica\Client\ElasticsearchClient; use Valantic\ElasticaBridgeBundle\Enum\IndexBlueGreenSuffix; +use Valantic\ElasticaBridgeBundle\Exception\Command\DocumentFailedException; +use Valantic\ElasticaBridgeBundle\Exception\Command\IndexingFailedException; use Valantic\ElasticaBridgeBundle\Exception\Index\BlueGreenIndicesIncorrectlySetupException; use Valantic\ElasticaBridgeBundle\Index\IndexInterface; +use Valantic\ElasticaBridgeBundle\Messenger\Handler\CreateDocumentHandler; +use Valantic\ElasticaBridgeBundle\Messenger\Handler\SwitchIndexHandler; +use Valantic\ElasticaBridgeBundle\Messenger\Message\CreateDocument; +use Valantic\ElasticaBridgeBundle\Messenger\Message\ReleaseIndexLock; +use Valantic\ElasticaBridgeBundle\Model\Event\CallbackEvent; +use Valantic\ElasticaBridgeBundle\Model\Event\ElasticaBridgeEvents; +use Valantic\ElasticaBridgeBundle\Repository\ConfigurationRepository; +use Valantic\ElasticaBridgeBundle\Repository\DocumentRepository; use Valantic\ElasticaBridgeBundle\Repository\IndexRepository; +use Valantic\ElasticaBridgeBundle\Service\DocumentHelper; use Valantic\ElasticaBridgeBundle\Service\LockService; use Valantic\ElasticaBridgeBundle\Util\ElasticsearchResponse; @@ -27,13 +41,24 @@ class Index extends BaseCommand private const OPTION_DELETE = 'delete'; private const OPTION_POPULATE = 'populate'; private const OPTION_LOCK_RELEASE = 'lock-release'; + private const SYNC = 'sync'; + private const ASYNC = 'async'; public static bool $isPopulating = false; + public static ?bool $isAsync = null; + private bool $async; public function __construct( private readonly IndexRepository $indexRepository, private readonly ElasticsearchClient $esClient, private readonly KernelInterface $kernel, private readonly LockService $lockService, + private readonly MessageBusInterface $messageBus, + private readonly DocumentRepository $documentRepository, + private readonly DocumentHelper $documentHelper, + private readonly ConfigurationRepository $configurationRepository, + private readonly CreateDocumentHandler $createDocumentHandler, + private readonly SwitchIndexHandler $switchIndexHandler, + private readonly EventDispatcherInterface $eventDispatcher, ) { parent::__construct(); } @@ -64,9 +89,35 @@ protected function configure(): void 'l', InputOption::VALUE_NONE, 'Force all indexing locks to be released' + ) + ->addOption( + self::SYNC, + 's', + InputOption::VALUE_NONE, + 'Force sync mode', + ) + ->addOption( + self::ASYNC, + 'a', + InputOption::VALUE_NONE, + 'Force async mode', ); } + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + + $sync = $input->getOption(self::SYNC) === true; + $async = $input->getOption(self::ASYNC) === true; + + if ($sync && $async) { + throw new \InvalidArgumentException('Cannot use both sync and async mode at the same time.'); + } + + self::$isAsync = $this->async = ($async || !($sync || !$this->configurationRepository->shouldPopulateAsync())); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $skippedIndices = []; @@ -82,7 +133,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - $lock = $this->lockService->getIndexingLock($indexConfig); + $key = $this->lockService->getIndexingKey($indexConfig); + $lock = $this->lockService->createLockFromKey($key); if (!$lock->acquire()) { if ($this->input->getOption(self::OPTION_LOCK_RELEASE) === true) { @@ -105,9 +157,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - try { - $this->processIndex($indexConfig); - } finally { + $this->processIndex($indexConfig, $key); + + if ($this->input->getOption(self::OPTION_POPULATE) !== true) { $lock->release(); } } @@ -122,7 +174,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } - private function processIndex(IndexInterface $indexConfig): void + private function processIndex(IndexInterface $indexConfig, Key $key): void { $this->output->writeln(sprintf('Index: %s', $indexConfig->getName())); @@ -134,62 +186,157 @@ private function processIndex(IndexInterface $indexConfig): void $currentIndex = $indexConfig->getBlueGreenInactiveElasticaIndex(); } - if ($this->input->getOption(self::OPTION_POPULATE) === true) { - if ($indexConfig->usesBlueGreenIndices()) { - $this->output->writeln('-> Re-created inactive blue/green index'); - $currentIndex->delete(); - $currentIndex->create($indexConfig->getCreateArguments()); - } + $this->output->writeln(''); + + if ($this->input->getOption(self::OPTION_POPULATE) !== true) { + return; + } + + if ($indexConfig->usesBlueGreenIndices()) { + $currentIndex->delete(); + $currentIndex->create($indexConfig->getCreateArguments()); + $this->output->writeln('-> Re-created inactive blue/green index'); + } + + ['messagesDispatched' => $dispatchedMessages, 'listingCount' => $listingCount] = $this->populateIndex($indexConfig); - $this->populateIndex($indexConfig, $currentIndex); + $currentIndex->refresh(); + $indexCount = $currentIndex->count(); - $currentIndex->refresh(); - $indexCount = $currentIndex->count(); + if ($this->async) { + $this->output->writeln('-> Indexing is done asynchronously'); + $this->output->writeln(sprintf('-> %d dispatched messages out of %d documents', $dispatchedMessages, $listingCount)); + } else { $this->output->writeln(sprintf('-> %d documents', $indexCount)); + } - if ($indexConfig->usesBlueGreenIndices()) { - $oldIndex = $indexConfig->getBlueGreenActiveElasticaIndex(); - $newIndex = $indexConfig->getBlueGreenInactiveElasticaIndex(); + if (!$indexConfig->usesBlueGreenIndices()) { + return; + } - $newIndex->flush(); - $oldIndex->removeAlias($indexConfig->getName()); - $newIndex->addAlias($indexConfig->getName()); - $oldIndex->flush(); + $message = new ReleaseIndexLock($indexConfig->getName(), $key); - $this->output->writeln( - sprintf('-> %s is now active', $newIndex->getName()) - ); - } + if ($this->async) { + $this->messageBus->dispatch($message); + + return; } - $this->output->writeln(''); + $this->switchIndexHandler->__invoke($message); } - private function populateIndex(IndexInterface $indexConfig, ElasticaIndex $esIndex): void + /** + * @return array{messagesDispatched: int, listingCount: int} + */ + private function populateIndex(IndexInterface $indexConfig): array { self::$isPopulating = true; - $process = new Process( - [ - 'bin/console', CommandConstants::COMMAND_POPULATE_INDEX, - '--' . CommandConstants::OPTION_CONFIG, $indexConfig->getName(), - '--' . CommandConstants::OPTION_INDEX, $esIndex->getName(), - ...array_filter([$this->output->isVerbose() ? '-v' : null, - $this->output->isVeryVerbose() ? '-vv' : null, - $this->output->isDebug() ? '-vvv' : null, - ]), - ], - $this->kernel->getProjectDir(), - timeout: null - ); - - $process->run(function($type, $buffer): void { - if ($type === Process::ERR && $this->output instanceof ConsoleOutput) { - $this->output->getErrorOutput()->write($buffer); - } else { - $this->output->write($buffer); + $messagesDispatched = 0; + $blueGreenKey = $this->lockService->lockSwitchBlueGreen($indexConfig); + + try { + foreach ($indexConfig->getAllowedDocuments() as $document) { + $documentInstance = $this->documentRepository->get($document); + $this->documentHelper->setTenantIfNeeded($documentInstance, $indexConfig); + + $listing = $documentInstance->getListingInstance($indexConfig); + $listingCount = $listing->getTotalCount(); + ProgressBar::setFormatDefinition('custom', "%percent%%\t%remaining%\t%memory%\n%message%"); + + $progressBar = new ProgressBar($this->output, $listingCount); + + $progressBar->setMessage($document); + $progressBar->setFormat('custom'); + + $batchSize = $indexConfig->getBatchSize(); + $numberOfBatches = ceil($listingCount / $batchSize); + $this->output->getFormatter()->setDecorated(true); + $this->output->writeln(''); + + if ( + $listingCount > 10000 + && !$indexConfig->shouldPopulateInSubprocesses() + && $this->kernel->getEnvironment() === 'dev' + ) { + $this->output->writeln( + 'For large indices please consider to implement `shouldPopulateInSubprocesses` to prevent memory exhaustion.', + ); + $numberOfBatches = 1; + } else { + $this->output->writeln(sprintf( + 'Populating index %s with %d documents in %d subprocesses.', + $indexConfig::class, + $listingCount, + $numberOfBatches + )); + } + + $batchNumber = 0; + + while ($batchNumber < $numberOfBatches) { + $listing->setOffset($batchNumber * $batchSize); + $listing->setLimit($batchSize); + $data = $listing->getData(); + + foreach ($data ?? [] as $key => $dataObject) { + if ($batchNumber === $numberOfBatches - 1 && $key === array_key_last($data ?? [])) { + $message = new ReleaseIndexLock($indexConfig->getName(), $blueGreenKey); + $this->messageBus->dispatch($message); + } + + try { + $progressBar->advance(); + + if (!$documentInstance->shouldIndex($dataObject)) { + continue; + } + + $callback = $this->eventDispatcher->dispatch(new CallbackEvent(), ElasticaBridgeEvents::CALLBACK_EVENT); + + $message = new CreateDocument($dataObject->getId(), $dataObject::class, $document, $indexConfig->getName(), $callback); + $messagesDispatched++; + + if ($this->async) { + $envelope = new Envelope($message, []); + $this->messageBus->dispatch($envelope); + + continue; + } + + $this->createDocumentHandler->__invoke($message); + } catch (\Throwable $throwable) { + $this->displayDocumentError($indexConfig, $document, $dataObject, $throwable); + + if (!$this->configurationRepository->shouldSkipFailingDocuments()) { + throw new DocumentFailedException($throwable); + } + } + } + + \Pimcore::collectGarbage(); + + $batchNumber++; + } + + $progressBar->finish(); + } + } catch (\Throwable $throwable) { + $this->displayIndexError($indexConfig, $throwable); + + throw new IndexingFailedException($throwable); + } finally { + if (isset($documentInstance)) { + $this->documentHelper->setTenantIfNeeded($documentInstance, $indexConfig); } - }); + } + + $this->output->writeln(''); self::$isPopulating = false; + + return [ + 'messagesDispatched' => $messagesDispatched, + 'listingCount' => $listingCount ?? 0, + ]; } private function ensureCorrectIndexSetup(IndexInterface $indexConfig): void @@ -257,4 +404,31 @@ private function ensureCorrectBlueGreenIndexSetup(IndexInterface $indexConfig): $this->output->writeln('-> Ensured indices are correctly set up with alias'); } + + private function displayDocumentError( + IndexInterface $indexConfig, + string $document, + AbstractElement $dataObject, + \Throwable $throwable, + ): void { + $this->output->writeln(''); + $this->output->writeln(sprintf( + 'Error while populating index %s, processing documents of type %s, last processed element ID %s.', + $indexConfig::class, + $document, + $dataObject->getId() + )); + $this->displayThrowable($throwable); + } + + private function displayIndexError(IndexInterface $indexConfig, \Throwable $throwable): void + { + $this->output->writeln(''); + $this->output->writeln(sprintf( + 'Error while populating index %s.', + $indexConfig::class, + )); + + $this->displayThrowable($throwable); + } } diff --git a/src/Command/PopulateIndex.php b/src/Command/PopulateIndex.php deleted file mode 100644 index f584458..0000000 --- a/src/Command/PopulateIndex.php +++ /dev/null @@ -1,157 +0,0 @@ -setName(CommandConstants::COMMAND_POPULATE_INDEX) - ->setHidden(true) - ->setDescription('[INTERNAL]') - ->addOption(CommandConstants::OPTION_CONFIG, mode: InputOption::VALUE_REQUIRED) - ->addOption(CommandConstants::OPTION_INDEX, mode: InputOption::VALUE_REQUIRED) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $indexConfig = $this->getIndex(); - - if (!$indexConfig instanceof IndexInterface) { - return self::FAILURE; - } - - $index = $indexConfig->getBlueGreenInactiveElasticaIndex(); - - return $this->populateIndex($indexConfig, $index); - } - - private function getIndex(): ?IndexInterface - { - foreach ($this->indexRepository->flattenedAll() as $indexConfig) { - if ($indexConfig->getName() === $this->input->getOption(CommandConstants::OPTION_CONFIG)) { - return $indexConfig; - } - } - - return null; - } - - private function populateIndex(IndexInterface $indexConfig, ElasticaIndex $esIndex): int - { - try { - foreach ($indexConfig->getAllowedDocuments() as $document) { - $documentInstance = $this->documentRepository->get($document); - - $this->documentHelper->setTenantIfNeeded($documentInstance, $indexConfig); - - $listingCount = $documentInstance->getListingInstance($indexConfig)->count(); - $numberOfBatches = ceil($listingCount / $indexConfig->getBatchSize()); - - $this->output->getFormatter()->setDecorated(true); - $this->output->writeln(''); - - if ( - !$indexConfig->shouldPopulateInSubprocesses() - && $this->kernel->getEnvironment() === 'dev' - && $listingCount > 10000 - ) { - $this->output->writeln( - 'For large indices please consider to implement `shouldPopulateInSubprocesses` to prevent memory exhaustion.', - ); - $numberOfBatches = 1; - } else { - $this->output->writeln(sprintf( - 'Populating index %s with %d documents in %d subprocesses.', - $indexConfig::class, - $listingCount, - $numberOfBatches - )); - } - - for ($batchNumber = 0; $batchNumber < $numberOfBatches; $batchNumber++) { - $this->output->writeln(''); - $this->output->writeln(sprintf('-> Populating index %s batch %d/%d.', $indexConfig::class, $batchNumber + 1, $numberOfBatches)); - $this->output->writeln(''); - $process = new Process( - [ - 'bin/console', CommandConstants::COMMAND_DO_POPULATE_INDEX, - '--' . CommandConstants::OPTION_CONFIG, $indexConfig->getName(), - '--' . CommandConstants::OPTION_INDEX, $esIndex->getName(), - '--' . CommandConstants::OPTION_BATCH_NUMBER, $batchNumber, - '--' . CommandConstants::OPTION_LISTING_COUNT, $listingCount, - '--' . CommandConstants::OPTION_DOCUMENT, $document, - ...array_filter([$this->output->isVerbose() ? '-v' : null, - $this->output->isVeryVerbose() ? '-vv' : null, - $this->output->isDebug() ? '-vvv' : null, - ]), - ], - $this->kernel->getProjectDir(), - timeout: null - ); - - $exitCode = $process->run(function($type, $buffer): void { - if ($type === Process::ERR && $this->output instanceof ConsoleOutput) { - $this->output->getErrorOutput()->write($buffer); - } else { - $this->output->write($buffer); - } - }); - - if ($exitCode !== self::SUCCESS) { - return self::FAILURE; - } - } - } - } catch (\Throwable $throwable) { - $this->displayIndexError($indexConfig, $throwable); - - throw new IndexingFailedException($throwable); - } finally { - if (isset($documentInstance)) { - $this->documentHelper->setTenantIfNeeded($documentInstance, $indexConfig); - } - } - - $this->output->writeln(''); - - return self::SUCCESS; - } - - private function displayIndexError(IndexInterface $indexConfig, \Throwable $throwable): void - { - $this->output->writeln(''); - $this->output->writeln(sprintf( - 'Error while populating index %s.', - $indexConfig::class, - )); - - $this->displayThrowable($throwable); - } -} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c9f7fad..f02ec1e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -25,6 +25,7 @@ public function getConfigTreeBuilder() ->children() ->integerNode('lock_timeout')->defaultValue(5 * 60)->info('To prevent overlapping indexing jobs. Set to a value higher than the slowest index. Value is specified in seconds.')->end() ->booleanNode('should_skip_failing_documents')->defaultFalse()->info('If true, when a document fails to be indexed, it will be skipped and indexing continue with the next document. If false, indexing that index will be aborted.')->end() + ->booleanNode('populate_async')->defaultFalse()->info('If true, documents to populate are being sent to the queue.')->end() ->end() ->end() ->arrayNode('events') diff --git a/src/Document/AbstractDocument.php b/src/Document/AbstractDocument.php index a125b3e..cb9ed33 100644 --- a/src/Document/AbstractDocument.php +++ b/src/Document/AbstractDocument.php @@ -6,9 +6,9 @@ use Pimcore\Model\Asset; use Pimcore\Model\DataObject; +use Pimcore\Model\DataObject\Listing; use Pimcore\Model\Document as PimcoreDocument; use Pimcore\Model\Element\AbstractElement; -use Pimcore\Model\Listing\AbstractListing; use Valantic\ElasticaBridgeBundle\Enum\DocumentType; use Valantic\ElasticaBridgeBundle\Exception\DocumentType\PimcoreListingClassNotFoundException; use Valantic\ElasticaBridgeBundle\Exception\DocumentType\UnknownPimcoreElementType; @@ -26,12 +26,11 @@ public function treatObjectVariantsAsDocuments(): bool return false; } - public function getListingInstance(IndexInterface $index): AbstractListing + public function getListingInstance(IndexInterface $index): Listing|PimcoreDocument\Listing { - /** @var class-string $listingClass */ + /** @var class-string $listingClass */ $listingClass = $this->getListingClass(); - /** @var AbstractListing $listingInstance */ $listingInstance = new $listingClass(); if ($this->getIndexListingCondition() !== null) { @@ -39,12 +38,12 @@ public function getListingInstance(IndexInterface $index): AbstractListing } if (in_array($this->getType(), DocumentType::casesPublishedState(), true)) { - /** @var PimcoreDocument\Listing|DataObject\Listing $listingInstance */ + /** @var PimcoreDocument\Listing|Listing $listingInstance */ $listingInstance->setUnpublished($this->includeUnpublishedElementsInListing()); } if ($this->getType() === DocumentType::DATA_OBJECT) { - /** @var DataObject\Listing $listingInstance */ + /** @var Listing $listingInstance */ if ($this->treatObjectVariantsAsDocuments()) { $listingInstance->setObjectTypes([ DataObject\AbstractObject::OBJECT_TYPE_OBJECT, @@ -211,7 +210,7 @@ private function getDataObjectListingClass(): string $subType = $this->getSubType(); if ($subType === null) { - return DataObject\Listing::class; + return Listing::class; } $className = $subType . '\Listing'; diff --git a/src/Document/DocumentInterface.php b/src/Document/DocumentInterface.php index 84fce99..ec834e6 100644 --- a/src/Document/DocumentInterface.php +++ b/src/Document/DocumentInterface.php @@ -4,8 +4,9 @@ namespace Valantic\ElasticaBridgeBundle\Document; +use Pimcore\Model\DataObject\Listing; +use Pimcore\Model\Document as PimcoreDocument; use Pimcore\Model\Element\AbstractElement; -use Pimcore\Model\Listing\AbstractListing; use Valantic\ElasticaBridgeBundle\Enum\DocumentType; use Valantic\ElasticaBridgeBundle\Index\IndexInterface; @@ -92,7 +93,7 @@ public function shouldIndex(AbstractElement $element): bool; /** * @see ListingTrait */ - public function getListingInstance(IndexInterface $index): AbstractListing; + public function getListingInstance(IndexInterface $index): Listing|PimcoreDocument\Listing; /** * Whether Elasticsearch documents should be created for object variants. diff --git a/src/Elastica/Client/ElasticsearchClientFactory.php b/src/Elastica/Client/ElasticsearchClientFactory.php index 629fe21..943ad6e 100644 --- a/src/Elastica/Client/ElasticsearchClientFactory.php +++ b/src/Elastica/Client/ElasticsearchClientFactory.php @@ -17,7 +17,7 @@ public function __invoke( ): ElasticsearchClient { $logger = null; - if ($this->configurationRepository->shouldAddSentryBreadcrumbs() && class_exists('\Sentry\Breadcrumb')) { + if ($this->configurationRepository->shouldAddSentryBreadcrumbs() && class_exists(\Sentry\Breadcrumb::class)) { $logger = (new SentryBreadcrumbLogger()); } diff --git a/src/Messenger/Handler/CreateDocumentHandler.php b/src/Messenger/Handler/CreateDocumentHandler.php new file mode 100644 index 0000000..9432941 --- /dev/null +++ b/src/Messenger/Handler/CreateDocumentHandler.php @@ -0,0 +1,71 @@ +lockService->isExecutionLocked($message->esIndex)) { + return; + } + + $message->callback->run(); + + $documentInstance = $this->documentRepository->get($message->document); + $dataObject = $message->objectType::getById($message->objectId) ?? throw new \RuntimeException('DataObject not found'); + $esDocuments = [$this->documentHelper->elementToDocument($documentInstance, $dataObject)]; + + $esIndex = $this->indexRepository->flattenedGet($message->esIndex)->getBlueGreenInactiveElasticaIndex(); + + if (count($esDocuments) > 0) { + try { + $esIndex->addDocuments($esDocuments); + } catch (\Throwable $throwable) { + if (!$this->configurationRepository->shouldSkipFailingDocuments()) { + $key = $this->lockService->lockExecution($message->esIndex); + $envelope = new Envelope(new ReleaseIndexLock($message->esIndex, $key), [new DelayStamp(5000)]); + $this->messageBus->dispatch($envelope); + } + + if ($this->configurationRepository->shouldPopulateAsync()) { + throw new UnrecoverableMessageHandlingException($throwable->getMessage(), previous: $throwable); + } + } + } + + \Pimcore::collectGarbage(); + } +} diff --git a/src/Messenger/Handler/SwitchIndexHandler.php b/src/Messenger/Handler/SwitchIndexHandler.php new file mode 100644 index 0000000..765c117 --- /dev/null +++ b/src/Messenger/Handler/SwitchIndexHandler.php @@ -0,0 +1,56 @@ +lockService->isExecutionLocked($message->indexName)) { + return; + } + $this->lockService->waitForFinish($message->indexName); + $indexConfig = $this->indexRepository->flattenedGet($message->indexName); + $oldIndex = $indexConfig->getBlueGreenActiveElasticaIndex(); + $newIndex = $indexConfig->getBlueGreenInactiveElasticaIndex(); + + $newIndex->flush(); + $oldIndex->removeAlias($indexConfig->getName()); + $newIndex->addAlias($indexConfig->getName()); + $oldIndex->flush(); + } finally { + + $key = $message->key; + + $lock = $this->lockFactory->createLockFromKey($key); + $lock->release(); + + \Pimcore::collectGarbage(); + } + } +} diff --git a/src/Messenger/Message/CreateDocument.php b/src/Messenger/Message/CreateDocument.php new file mode 100644 index 0000000..f90ceb7 --- /dev/null +++ b/src/Messenger/Message/CreateDocument.php @@ -0,0 +1,24 @@ +closure; + + if (is_callable($callable)) { + $callable(); + } + } + + public function setClosure(\Closure $closure): void + { + $this->closure = $closure; + } +} diff --git a/src/Model/Event/ElasticaBridgeEvents.php b/src/Model/Event/ElasticaBridgeEvents.php index 2cb2c6b..82fe722 100644 --- a/src/Model/Event/ElasticaBridgeEvents.php +++ b/src/Model/Event/ElasticaBridgeEvents.php @@ -10,4 +10,6 @@ interface ElasticaBridgeEvents public const POST_REFRESH_ELEMENT = 'valantic.elastica_bridge.post_refreshed_element'; public const PRE_REFRESH_ELEMENT_IN_INDEX = 'valantic.elastica_bridge.pre_refreshed_element_in_index'; public const POST_REFRESH_ELEMENT_IN_INDEX = 'valantic.elastica_bridge.post_refreshed_element_in_index'; + public const CALLBACK_EVENT = 'valantic.elastica_bridge.populate.callback_event'; + } diff --git a/src/Repository/ConfigurationRepository.php b/src/Repository/ConfigurationRepository.php index 7875ca9..8ea84af 100644 --- a/src/Repository/ConfigurationRepository.php +++ b/src/Repository/ConfigurationRepository.php @@ -15,6 +15,11 @@ public function __construct( private readonly ContainerBagInterface $containerBag, ) {} + public function shouldPopulateAsync(): bool + { + return $this->containerBag->get('valantic_elastica_bridge')['indexing']['populate_async']; + } + public function getClientDsn(): string { return $this->containerBag->get('valantic_elastica_bridge')['client']['dsn']; diff --git a/src/Resources/config/pimcore/messenger.yaml b/src/Resources/config/pimcore/messenger.yaml index 66393aa..b301882 100644 --- a/src/Resources/config/pimcore/messenger.yaml +++ b/src/Resources/config/pimcore/messenger.yaml @@ -1,8 +1,14 @@ framework: messenger: + failure_transport: elastica_bridge_failed enabled: true transports: elastica_bridge_index: 'doctrine://default?queue_name=elastica_bridge_index' + elastica_bridge_populate: 'doctrine://default?queue_name=elastica_bridge_populate' +# elastica_bridge_populate: 'sync://' + elastica_bridge_failed: 'doctrine://default?queue_name=elastica_bridge_failed' routing: Valantic\ElasticaBridgeBundle\Messenger\Message\RefreshElement: elastica_bridge_index Valantic\ElasticaBridgeBundle\Messenger\Message\RefreshElementInIndex: elastica_bridge_index + Valantic\ElasticaBridgeBundle\Messenger\Message\CreateDocument: elastica_bridge_populate + Valantic\ElasticaBridgeBundle\Messenger\Message\ReleaseIndexLock: elastica_bridge_populate diff --git a/src/Service/LockService.php b/src/Service/LockService.php index 425832b..75e8621 100644 --- a/src/Service/LockService.php +++ b/src/Service/LockService.php @@ -4,8 +4,10 @@ namespace Valantic\ElasticaBridgeBundle\Service; +use Symfony\Component\Lock\Key; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; +use Valantic\ElasticaBridgeBundle\Command\Index; use Valantic\ElasticaBridgeBundle\Index\IndexInterface; use Valantic\ElasticaBridgeBundle\Repository\ConfigurationRepository; @@ -21,9 +23,68 @@ public function __construct( public function getIndexingLock(IndexInterface $indexConfig): LockInterface { return $this->lockFactory - ->createLock( - sprintf('%s:indexing:%s', self::LOCK_PREFIX, $indexConfig->getName()), - ttl: $this->configurationRepository->getIndexingLockTimeout() + ->createLockFromKey( + $this->getIndexingKey($indexConfig), + ttl: $this->configurationRepository->getIndexingLockTimeout(), + autoRelease: false ); } + + public function getIndexingKey(IndexInterface $indexConfig): Key + { + return $this->getKey($indexConfig->getName(), 'indexing'); + } + + public function getKey(string $name, string $task): Key + { + return new Key(sprintf('%s:%s:%s', self::LOCK_PREFIX, $task, $name)); + } + + public function createLockFromKey(Key $key): LockInterface + { + return $this->lockFactory->createLockFromKey( + $key, + ttl: $this->configurationRepository->getIndexingLockTimeout(), + autoRelease: false + ); + } + + public function lockExecution(string $document): Key + { + $key = $this->getKey($document, 'failure'); + $lock = $this->lockFactory->createLockFromKey($key, ttl: $this->configurationRepository->getIndexingLockTimeout(), autoRelease: false); + $lock->acquire(); + + return $key; + } + + public function isExecutionLocked(string $document): bool + { + $key = $this->getKey($document, 'failure'); + + return !$this->lockFactory->createLockFromKey($key)->acquire(); + } + + public function lockSwitchBlueGreen(IndexInterface $indexConfig): Key + { + $key = $this->getKey($indexConfig->getName(), 'switch-blue-green'); + $lock = $this->lockFactory->createLockFromKey($key, ttl: $this->configurationRepository->getIndexingLockTimeout(), autoRelease: false); + $lock->acquire(); + + return $key; + } + + public function waitForFinish(string $indexName): void + { + if ( + Index::$isAsync === false + || (Index::$isAsync === null && $this->configurationRepository->shouldPopulateAsync() === false) + ) { + return; + } + + $key = $this->getKey($indexName, 'switch-blue-green'); + $lock = $this->lockFactory->createLockFromKey($key); + $lock->acquire(true); + } } diff --git a/vendor-bin/phpcs/composer.json b/vendor-bin/phpcs/composer.json index 3eebd48..17cca5f 100644 --- a/vendor-bin/phpcs/composer.json +++ b/vendor-bin/phpcs/composer.json @@ -1,5 +1,5 @@ { "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1" + "friendsofphp/php-cs-fixer": "^3.64.0" } } diff --git a/vendor-bin/phpcs/composer.lock b/vendor-bin/phpcs/composer.lock index 03f92f8..745de8d 100644 --- a/vendor-bin/phpcs/composer.lock +++ b/vendor-bin/phpcs/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3df17db298eaddd7db1b8588f4b19f6a", + "content-hash": "b6591bd88cbafd16a6fbae2daa39fd2f", "packages": [], "packages-dev": [ { @@ -73,26 +73,26 @@ }, { "name": "composer/pcre", - "version": "3.2.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.8" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan": "^1.11.10", "phpstan/phpstan-strict-rules": "^1.1", "phpunit/phpunit": "^8 || ^9" }, @@ -132,7 +132,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" + "source": "https://github.com/composer/pcre/tree/3.3.1" }, "funding": [ { @@ -148,7 +148,7 @@ "type": "tidelift" } ], - "time": "2024-07-25T09:36:02+00:00" + "time": "2024-08-27T18:44:43+00:00" }, { "name": "composer/semver", @@ -346,16 +346,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -395,7 +395,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -403,20 +403,20 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.61.1", + "version": "v3.64.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" + "reference": "58dd9c931c785a79739310aef5178928305ffa67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", "shasum": "" }, "require": { @@ -498,7 +498,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" }, "funding": [ { @@ -506,7 +506,7 @@ "type": "github" } ], - "time": "2024-07-31T14:33:15+00:00" + "time": "2024-08-30T23:09:38+00:00" }, { "name": "psr/container", @@ -613,16 +613,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -657,9 +657,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "react/cache", @@ -1260,16 +1260,16 @@ }, { "name": "symfony/console", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9" + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", - "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", + "url": "https://api.github.com/repos/symfony/console/zipball/1eed7af6961d763e7832e874d7f9b21c3ea9c111", + "reference": "1eed7af6961d763e7832e874d7f9b21c3ea9c111", "shasum": "" }, "require": { @@ -1333,7 +1333,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.3" + "source": "https://github.com/symfony/console/tree/v7.1.4" }, "funding": [ { @@ -1349,7 +1349,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:41:01+00:00" + "time": "2024-08-15T22:48:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1642,16 +1642,16 @@ }, { "name": "symfony/finder", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "717c6329886f32dc65e27461f80f2a465412fdca" + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/717c6329886f32dc65e27461f80f2a465412fdca", - "reference": "717c6329886f32dc65e27461f80f2a465412fdca", + "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823", + "reference": "d95bbf319f7d052082fb7af147e0f835a695e823", "shasum": "" }, "require": { @@ -1686,7 +1686,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.3" + "source": "https://github.com/symfony/finder/tree/v7.1.4" }, "funding": [ { @@ -1702,7 +1702,7 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:08:44+00:00" + "time": "2024-08-13T14:28:19+00:00" }, { "name": "symfony/options-resolver", @@ -1773,20 +1773,20 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1832,7 +1832,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -1848,24 +1848,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -1910,7 +1910,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -1926,24 +1926,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -1991,7 +1991,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -2007,24 +2007,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -2071,7 +2071,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -2087,24 +2087,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -2151,7 +2151,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -2167,24 +2167,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -2227,7 +2227,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -2243,7 +2243,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", @@ -2453,16 +2453,16 @@ }, { "name": "symfony/string", - "version": "v7.1.3", + "version": "v7.1.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07" + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ea272a882be7f20cad58d5d78c215001617b7f07", - "reference": "ea272a882be7f20cad58d5d78c215001617b7f07", + "url": "https://api.github.com/repos/symfony/string/zipball/6cd670a6d968eaeb1c77c2e76091c45c56bc367b", + "reference": "6cd670a6d968eaeb1c77c2e76091c45c56bc367b", "shasum": "" }, "require": { @@ -2520,7 +2520,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.3" + "source": "https://github.com/symfony/string/tree/v7.1.4" }, "funding": [ { @@ -2536,7 +2536,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:25:37+00:00" + "time": "2024-08-12T09:59:40+00:00" } ], "aliases": [],