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

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