vendor/shopware/core/Content/Seo/SeoUrlPersister.php line 45

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Seo;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Seo\Event\SeoUrlUpdateEvent;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\Context;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableTransaction;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  12. use Shopware\Core\Framework\Uuid\Uuid;
  13. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. class SeoUrlPersister
  16. {
  17.     private Connection $connection;
  18.     private EntityRepositoryInterface $seoUrlRepository;
  19.     private EventDispatcherInterface $eventDispatcher;
  20.     /**
  21.      * @internal
  22.      */
  23.     public function __construct(
  24.         Connection $connection,
  25.         EntityRepositoryInterface $seoUrlRepository,
  26.         EventDispatcherInterface $eventDispatcher
  27.     ) {
  28.         $this->connection $connection;
  29.         $this->seoUrlRepository $seoUrlRepository;
  30.         $this->eventDispatcher $eventDispatcher;
  31.     }
  32.     /**
  33.      * @feature-deprecated (flag:FEATURE_NEXT_13410) Parameter $salesChannel will be required
  34.      *
  35.      * @param list<string> $foreignKeys
  36.      * @param iterable<array<mixed>|Entity> $seoUrls
  37.      */
  38.     public function updateSeoUrls(Context $contextstring $routeName, array $foreignKeysiterable $seoUrls/*, SalesChannelEntity $salesChannel*/): void
  39.     {
  40.         /** @var SalesChannelEntity|null $salesChannel */
  41.         $salesChannel \func_num_args() === func_get_arg(4) : null;
  42.         $languageId $context->getLanguageId();
  43.         $canonicals $this->findCanonicalPaths($routeName$languageId$foreignKeys);
  44.         $dateTime = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  45.         $insertQuery = new MultiInsertQueryQueue($this->connection250falsetrue);
  46.         $updatedFks = [];
  47.         $obsoleted = [];
  48.         $processed = [];
  49.         // should be provided
  50.         $salesChannelId $salesChannel $salesChannel->getId() : null;
  51.         $updates = [];
  52.         foreach ($seoUrls as $seoUrl) {
  53.             if ($seoUrl instanceof \JsonSerializable) {
  54.                 $seoUrl $seoUrl->jsonSerialize();
  55.             }
  56.             $updates[] = $seoUrl;
  57.             $fk $seoUrl['foreignKey'];
  58.             /** @var string|null $salesChannelId */
  59.             $salesChannelId $seoUrl['salesChannelId'] = $seoUrl['salesChannelId'] ?? null;
  60.             // skip duplicates
  61.             if (isset($processed[$fk][$salesChannelId])) {
  62.                 continue;
  63.             }
  64.             if (!isset($processed[$fk])) {
  65.                 $processed[$fk] = [];
  66.             }
  67.             $processed[$fk][$salesChannelId] = true;
  68.             $updatedFks[] = $fk;
  69.             if (isset($seoUrl['error'])) {
  70.                 continue;
  71.             }
  72.             $existing $canonicals[$fk][$salesChannelId] ?? null;
  73.             if ($existing) {
  74.                 // entity has override or does not change
  75.                 /** @var array{isModified: bool, seoPathInfo: string, salesChannelId: string} $seoUrl */
  76.                 if ($this->skipUpdate($existing$seoUrl)) {
  77.                     continue;
  78.                 }
  79.                 $obsoleted[] = $existing['id'];
  80.             }
  81.             $insert = [];
  82.             $insert['id'] = Uuid::randomBytes();
  83.             if ($salesChannelId) {
  84.                 $insert['sales_channel_id'] = Uuid::fromHexToBytes($salesChannelId);
  85.             }
  86.             $insert['language_id'] = Uuid::fromHexToBytes($languageId);
  87.             $insert['foreign_key'] = Uuid::fromHexToBytes($fk);
  88.             $insert['path_info'] = $seoUrl['pathInfo'];
  89.             $insert['seo_path_info'] = ltrim($seoUrl['seoPathInfo'], '/');
  90.             $insert['route_name'] = $routeName;
  91.             $insert['is_canonical'] = ($seoUrl['isCanonical'] ?? true) ? null;
  92.             $insert['is_modified'] = ($seoUrl['isModified'] ?? false) ? 0;
  93.             $insert['is_deleted'] = ($seoUrl['isDeleted'] ?? true) ? 0;
  94.             $insert['created_at'] = $dateTime;
  95.             $insertQuery->addInsert($this->seoUrlRepository->getDefinition()->getEntityName(), $insert);
  96.         }
  97.         RetryableTransaction::retryable($this->connection, function () use ($obsoleted$dateTime$insertQuery$foreignKeys$updatedFks$salesChannelId): void {
  98.             $this->obsoleteIds($obsoleted$salesChannelId);
  99.             $insertQuery->execute();
  100.             $deletedIds array_diff($foreignKeys$updatedFks);
  101.             $notDeletedIds array_unique(array_intersect($foreignKeys$updatedFks));
  102.             $this->markAsDeleted(true$deletedIds$dateTime$salesChannelId);
  103.             $this->markAsDeleted(false$notDeletedIds$dateTime$salesChannelId);
  104.         });
  105.         $this->eventDispatcher->dispatch(new SeoUrlUpdateEvent($updates));
  106.     }
  107.     /**
  108.      * @param array{isModified: bool, seoPathInfo: string, salesChannelId: string} $existing
  109.      * @param array{isModified: bool, seoPathInfo: string, salesChannelId: string} $seoUrl
  110.      */
  111.     private function skipUpdate(array $existing, array $seoUrl): bool
  112.     {
  113.         if ($existing['isModified'] && !($seoUrl['isModified'] ?? false) && trim($seoUrl['seoPathInfo']) !== '') {
  114.             return true;
  115.         }
  116.         return $seoUrl['seoPathInfo'] === $existing['seoPathInfo']
  117.             && $seoUrl['salesChannelId'] === $existing['salesChannelId'];
  118.     }
  119.     /**
  120.      * @param list<string> $foreignKeys
  121.      *
  122.      * @return array<string, mixed>
  123.      */
  124.     private function findCanonicalPaths(string $routeNamestring $languageId, array $foreignKeys): array
  125.     {
  126.         $fks Uuid::fromHexToBytesList($foreignKeys);
  127.         $languageId Uuid::fromHexToBytes($languageId);
  128.         $query $this->connection->createQueryBuilder();
  129.         $query->select([
  130.             'LOWER(HEX(seo_url.id)) as id',
  131.             'LOWER(HEX(seo_url.foreign_key)) foreignKey',
  132.             'LOWER(HEX(seo_url.sales_channel_id)) salesChannelId',
  133.             'seo_url.is_modified as isModified',
  134.             'seo_url.seo_path_info seoPathInfo',
  135.         ]);
  136.         $query->from('seo_url''seo_url');
  137.         $query->andWhere('seo_url.route_name = :routeName');
  138.         $query->andWhere('seo_url.language_id = :language_id');
  139.         $query->andWhere('seo_url.is_canonical = 1');
  140.         $query->andWhere('seo_url.foreign_key IN (:foreign_keys)');
  141.         $query->setParameter('routeName'$routeName);
  142.         $query->setParameter('language_id'$languageId);
  143.         $query->setParameter('foreign_keys'$fksConnection::PARAM_STR_ARRAY);
  144.         $rows $query->executeQuery()->fetchAllAssociative();
  145.         $canonicals = [];
  146.         foreach ($rows as $row) {
  147.             $row['isModified'] = (bool) $row['isModified'];
  148.             $foreignKey = (string) $row['foreignKey'];
  149.             if (!isset($canonicals[$foreignKey])) {
  150.                 $canonicals[$foreignKey] = [$row['salesChannelId'] => $row];
  151.                 continue;
  152.             }
  153.             $canonicals[$foreignKey][$row['salesChannelId']] = $row;
  154.         }
  155.         return $canonicals;
  156.     }
  157.     /**
  158.      * @internal (flag:FEATURE_NEXT_13410) Parameter $salesChannelId will be required
  159.      *
  160.      * @param list<string> $ids
  161.      */
  162.     private function obsoleteIds(array $ids, ?string $salesChannelId): void
  163.     {
  164.         if (empty($ids)) {
  165.             return;
  166.         }
  167.         $ids Uuid::fromHexToBytesList($ids);
  168.         $query $this->connection->createQueryBuilder()
  169.             ->update('seo_url')
  170.             ->set('is_canonical''NULL')
  171.             ->where('id IN (:ids)')
  172.             ->setParameter('ids'$idsConnection::PARAM_STR_ARRAY);
  173.         if ($salesChannelId) {
  174.             $query->andWhere('sales_channel_id = :salesChannelId');
  175.             $query->setParameter('salesChannelId'Uuid::fromHexToBytes($salesChannelId));
  176.         }
  177.         RetryableQuery::retryable($this->connection, function () use ($query): void {
  178.             $query->execute();
  179.         });
  180.     }
  181.     /**
  182.      * @internal (flag:FEATURE_NEXT_13410) Parameter $salesChannelId will be required
  183.      *
  184.      * @param list<string> $ids
  185.      */
  186.     private function markAsDeleted(bool $deleted, array $idsstring $dateTime, ?string $salesChannelId): void
  187.     {
  188.         if (empty($ids)) {
  189.             return;
  190.         }
  191.         $ids Uuid::fromHexToBytesList($ids);
  192.         $query $this->connection->createQueryBuilder()
  193.             ->update('seo_url')
  194.             ->set('is_deleted'$deleted '1' '0')
  195.             ->where('foreign_key IN (:fks)')
  196.             ->setParameter('fks'$idsConnection::PARAM_STR_ARRAY);
  197.         if ($salesChannelId) {
  198.             $query->andWhere('sales_channel_id = :salesChannelId');
  199.             $query->setParameter('salesChannelId'Uuid::fromHexToBytes($salesChannelId));
  200.         }
  201.         $query->execute();
  202.     }
  203. }