vendor/shopware/core/Content/Product/DataAbstractionLayer/SearchKeywordUpdater.php line 86

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\DataAbstractionLayer;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Product\Aggregate\ProductKeywordDictionary\ProductKeywordDictionaryDefinition;
  5. use Shopware\Core\Content\Product\Aggregate\ProductSearchKeyword\ProductSearchKeywordDefinition;
  6. use Shopware\Core\Content\Product\ProductEntity;
  7. use Shopware\Core\Content\Product\SearchKeyword\ProductSearchKeywordAnalyzerInterface;
  8. use Shopware\Core\Defaults;
  9. use Shopware\Core\Framework\Api\Context\SystemSource;
  10. use Shopware\Core\Framework\Context;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  15. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NandFilter;
  22. use Shopware\Core\Framework\Log\Package;
  23. use Shopware\Core\Framework\Uuid\Uuid;
  24. use Shopware\Core\System\Language\LanguageCollection;
  25. use Shopware\Core\System\Language\LanguageEntity;
  26. use Symfony\Contracts\Service\ResetInterface;
  27. #[Package('core')]
  28. class SearchKeywordUpdater implements ResetInterface
  29. {
  30.     private Connection $connection;
  31.     private EntityRepositoryInterface $languageRepository;
  32.     private EntityRepositoryInterface $productRepository;
  33.     private ProductSearchKeywordAnalyzerInterface $analyzer;
  34.     /**
  35.      * @var array[]
  36.      */
  37.     private array $config = [];
  38.     /**
  39.      * @internal
  40.      */
  41.     public function __construct(
  42.         Connection $connection,
  43.         EntityRepositoryInterface $languageRepository,
  44.         EntityRepositoryInterface $productRepository,
  45.         ProductSearchKeywordAnalyzerInterface $analyzer
  46.     ) {
  47.         $this->connection $connection;
  48.         $this->languageRepository $languageRepository;
  49.         $this->productRepository $productRepository;
  50.         $this->analyzer $analyzer;
  51.     }
  52.     public function update(array $idsContext $context): void
  53.     {
  54.         if (empty($ids)) {
  55.             return;
  56.         }
  57.         $criteria = new Criteria();
  58.         $criteria->addFilter(new NandFilter([new EqualsFilter('salesChannelDomains.id'null)]));
  59.         /** @var LanguageCollection $languages */
  60.         $languages $this->languageRepository->search($criteriaContext::createDefaultContext())->getEntities();
  61.         $languages $this->sortLanguages($languages);
  62.         $products = [];
  63.         foreach ($languages as $language) {
  64.             $languageContext = new Context(
  65.                 new SystemSource(),
  66.                 [],
  67.                 Defaults::CURRENCY,
  68.                 array_filter([$language->getId(), $language->getParentId(), Defaults::LANGUAGE_SYSTEM]),
  69.                 $context->getVersionId()
  70.             );
  71.             $existingProducts $products[$language->getParentId() ?? Defaults::LANGUAGE_SYSTEM] ?? [];
  72.             $products[$language->getId()] = $this->updateLanguage($ids$languageContext$existingProducts);
  73.         }
  74.     }
  75.     public function reset(): void
  76.     {
  77.         $this->config = [];
  78.     }
  79.     /**
  80.      * @return ProductEntity[]
  81.      */
  82.     private function updateLanguage(array $idsContext $context, array $existingProducts): array
  83.     {
  84.         $configFields $this->getConfigFields($context->getLanguageId());
  85.         $versionId Uuid::fromHexToBytes($context->getVersionId());
  86.         $languageId Uuid::fromHexToBytes($context->getLanguageId());
  87.         $now = (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  88.         $this->delete($ids$context->getLanguageId(), $context->getVersionId());
  89.         $keywords = [];
  90.         $dictionary = [];
  91.         $iterator $this->getIterator($ids$context$configFields);
  92.         while ($products $iterator->fetch()) {
  93.             /** @var ProductEntity $product */
  94.             foreach ($products as $product) {
  95.                 // overwrite fetched products if translations for that product exists
  96.                 // otherwise we use the already fetched product from the parent language
  97.                 $existingProducts[$product->getId()] = $product;
  98.             }
  99.         }
  100.         foreach ($existingProducts as $product) {
  101.             $analyzed $this->analyzer->analyze($product$context$configFields);
  102.             $productId Uuid::fromHexToBytes($product->getId());
  103.             foreach ($analyzed as $keyword) {
  104.                 $keywords[] = [
  105.                     'id' => Uuid::randomBytes(),
  106.                     'version_id' => $versionId,
  107.                     'product_version_id' => $versionId,
  108.                     'language_id' => $languageId,
  109.                     'product_id' => $productId,
  110.                     'keyword' => $keyword->getKeyword(),
  111.                     'ranking' => $keyword->getRanking(),
  112.                     'created_at' => $now,
  113.                 ];
  114.                 $key $keyword->getKeyword() . $languageId;
  115.                 $dictionary[$key] = [
  116.                     'id' => Uuid::randomBytes(),
  117.                     'language_id' => $languageId,
  118.                     'keyword' => $keyword->getKeyword(),
  119.                 ];
  120.             }
  121.         }
  122.         $this->insertKeywords($keywords);
  123.         $this->insertDictionary($dictionary);
  124.         return $existingProducts;
  125.     }
  126.     private function getIterator(array $idsContext $context, array $configFields): RepositoryIterator
  127.     {
  128.         $context->setConsiderInheritance(true);
  129.         $criteria = new Criteria($ids);
  130.         $criteria->setLimit(50);
  131.         $this->buildCriteria(array_column($configFields'field'), $criteria$context);
  132.         return new RepositoryIterator($this->productRepository$context$criteria);
  133.     }
  134.     private function delete(array $idsstring $languageIdstring $versionId): void
  135.     {
  136.         $bytes Uuid::fromHexToBytesList($ids);
  137.         $params = [
  138.             'ids' => $bytes,
  139.             'language' => Uuid::fromHexToBytes($languageId),
  140.             'versionId' => Uuid::fromHexToBytes($versionId),
  141.         ];
  142.         RetryableQuery::retryable($this->connection, function () use ($params): void {
  143.             $this->connection->executeStatement(
  144.                 'DELETE FROM product_search_keyword WHERE product_id IN (:ids) AND language_id = :language AND version_id = :versionId',
  145.                 $params,
  146.                 ['ids' => Connection::PARAM_STR_ARRAY]
  147.             );
  148.         });
  149.     }
  150.     private function insertKeywords(array $keywords): void
  151.     {
  152.         $queue = new MultiInsertQueryQueue($this->connection50true);
  153.         foreach ($keywords as $insert) {
  154.             $queue->addInsert(ProductSearchKeywordDefinition::ENTITY_NAME$insert);
  155.         }
  156.         $queue->execute();
  157.     }
  158.     private function insertDictionary(array $dictionary): void
  159.     {
  160.         $queue = new MultiInsertQueryQueue($this->connection50truetrue);
  161.         foreach ($dictionary as $insert) {
  162.             $queue->addInsert(ProductKeywordDictionaryDefinition::ENTITY_NAME$insert);
  163.         }
  164.         $queue->execute();
  165.     }
  166.     private function buildCriteria(array $accessorsCriteria $criteriaContext $context): void
  167.     {
  168.         $definition $this->productRepository->getDefinition();
  169.         // Filter for products that have translations in the given language
  170.         // if there are no translations, we copy the keywords of the parent language without fetching the product
  171.         $filters = [
  172.             new EqualsFilter('translations.languageId'$context->getLanguageId()),
  173.             new EqualsFilter('parent.translations.languageId'$context->getLanguageId()),
  174.         ];
  175.         foreach ($accessors as $accessor) {
  176.             $fields EntityDefinitionQueryHelper::getFieldsOfAccessor($definition$accessor);
  177.             $fields array_filter($fields, function (Field $field) {
  178.                 return $field instanceof AssociationField;
  179.             });
  180.             if (empty($fields)) {
  181.                 continue;
  182.             }
  183.             $lastAssociationField $fields[\count($fields) - 1];
  184.             $path array_map(function (Field $field) {
  185.                 return $field->getPropertyName();
  186.             }, $fields);
  187.             $association implode('.'$path);
  188.             if ($criteria->hasAssociation($association)) {
  189.                 continue;
  190.             }
  191.             $criteria->addAssociation($association);
  192.             $translationField $lastAssociationField->getReferenceDefinition()->getTranslationField();
  193.             if (!$translationField) {
  194.                 continue;
  195.             }
  196.             // filter the associations that have no translations in given language,
  197.             // as we automatically use the parent languages keywords for those
  198.             $translationLanguageAccessor sprintf(
  199.                 '%s.%s.languageId',
  200.                 $association,
  201.                 $translationField->getPropertyName()
  202.             );
  203.             $filters[] = new EqualsFilter($translationLanguageAccessor$context->getLanguageId());
  204.         }
  205.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR$filters));
  206.     }
  207.     private function getConfigFields(string $languageId): array
  208.     {
  209.         if (isset($this->config[$languageId])) {
  210.             return $this->config[$languageId];
  211.         }
  212.         $query $this->connection->createQueryBuilder();
  213.         $query->select('configField.field''configField.tokenize''configField.ranking''LOWER(HEX(config.language_id)) as language_id');
  214.         $query->from('product_search_config''config');
  215.         $query->join('config''product_search_config_field''configField''config.id = configField.product_search_config_id');
  216.         $query->andWhere('config.language_id IN (:languageIds)');
  217.         $query->andWhere('configField.searchable = 1');
  218.         $query->setParameter('languageIds'Uuid::fromHexToBytesList([$languageIdDefaults::LANGUAGE_SYSTEM]), Connection::PARAM_STR_ARRAY);
  219.         $all $query->executeQuery()->fetchAllAssociative();
  220.         $fields array_filter($all, function (array $field) use ($languageId) {
  221.             return $field['language_id'] === $languageId;
  222.         });
  223.         if (!empty($fields)) {
  224.             return $this->config[$languageId] = $fields;
  225.         }
  226.         $fields array_filter($all, function (array $field) {
  227.             return $field['language_id'] === Defaults::LANGUAGE_SYSTEM;
  228.         });
  229.         return $this->config[$languageId] = $fields;
  230.     }
  231.     /**
  232.      * Sort languages so default language comes first, then languages that don't inherit and last inherited languages
  233.      *
  234.      * @return LanguageEntity[]
  235.      */
  236.     private function sortLanguages(LanguageCollection $languages): array
  237.     {
  238.         $defaultLanguage $languages->get(Defaults::LANGUAGE_SYSTEM);
  239.         $languages->remove(Defaults::LANGUAGE_SYSTEM);
  240.         return array_filter(array_merge(
  241.             [$defaultLanguage],
  242.             $languages->filterByProperty('parentId'null)->getElements(),
  243.             $languages->filter(function (LanguageEntity $language) {
  244.                 return $language->getParentId() !== null;
  245.             })->getElements()
  246.         ));
  247.     }
  248. }