custom/plugins/FourtwosixReferenceGallery/src/FourtwosixReferenceGallery.php line 28

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace FourtwosixReferenceGallery;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Content\Seo\Event\SeoUrlUpdateEvent;
  6. use Shopware\Core\Content\Seo\SeoUrlPersister;
  7. use Shopware\Core\Defaults;
  8. use Shopware\Core\Framework\Context;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\MultiInsertQueryQueue;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableTransaction;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
  17. use Shopware\Core\Framework\Plugin;
  18. use Shopware\Core\Framework\Plugin\Context\ActivateContext;
  19. use Shopware\Core\Framework\Plugin\Context\DeactivateContext;
  20. use Shopware\Core\Framework\Plugin\Context\InstallContext;
  21. use Shopware\Core\Framework\Plugin\Context\UninstallContext;
  22. use Shopware\Core\Framework\Uuid\Uuid;
  23. use Shopware\Core\System\CustomField\CustomFieldTypes;
  24. class FourtwosixReferenceGallery extends Plugin
  25. {
  26.     private const TEXT_COMPONENT_NAME 'sw-text-field';
  27.     private const EDITOR_COMPONENT_NAME 'sw-text-editor';
  28.     private const SELECT_COMPONENT_NAME 'sw-single-select';
  29.     private const ENTITY_COMPONENT_NAME 'sw-entity-single-select';
  30.     public const ATTRIBUTE_SET_NAME "fts_media_information";
  31.     public const ATTRIBUTE_SET_LABEL_EN "Media Information (426)";
  32.     public const ATTRIBUTE_SET_LABEL_DE "Media Informazionen (426)";
  33.     public const REFERENCE_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_reference";
  34.     public const REFERENCE_ATTRIBUTE_LABEL_EN "Reference (*required)";
  35.     public const REFERENCE_ATTRIBUTE_LABEL_DE "Referenz (*erforderlich)";
  36.     public const TITLE_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_title";
  37.     public const TITLE_ATTRIBUTE_LABEL_EN "Title (*required)";
  38.     public const TITLE_ATTRIBUTE_LABEL_DE "Titel (*erforderlich)";
  39.     public const DESCRIPTION_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_description";
  40.     public const DESCRIPTION_ATTRIBUTE_LABEL_EN "Description";
  41.     public const DESCRIPTION_ATTRIBUTE_LABEL_DE "Beschreibung";
  42.     public const ALT_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_alt";
  43.     public const ALT_ATTRIBUTE_LABEL_EN "Alt";
  44.     public const ALT_ATTRIBUTE_LABEL_DE "Alt";
  45.     public const PRODUCT_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_product";
  46.     public const PRODUCT_ATTRIBUTE_LABEL_EN "Product";
  47.     public const PRODUCT_ATTRIBUTE_LABEL_DE "Produkt";
  48.     public const POSITIONING_ATTRIBUTE_NAME self::ATTRIBUTE_SET_NAME "_positioning";
  49.     public const POSITIONING_ATTRIBUTE_LABEL_EN "Positioning";
  50.     public const POSITIONING_ATTRIBUTE_LABEL_DE "Posizionierung";
  51.     public const LIMIT 12;
  52.     public const GALLERY_CATEGORY_LABELS = [
  53.         "de" => [
  54.             "categories" => [
  55.                 "private" => "privat",
  56.                 "hotel" => "hotel",
  57.                 "company" => "unternehmen"
  58.             ],
  59.             "locale" => "de-DE",
  60.             "path" => "/geschaeft/referenzen/referenzen-projekte/",
  61.         ],
  62.         "en" => [
  63.             "categories" => [
  64.                 "private" => "private",
  65.                 "hotel" => "hotel",
  66.                 "company" => "company",
  67.             ],
  68.             "locale" => "en-GB",
  69.             "path" => "/shop/references/references-projects/",
  70.         ],
  71.         "it" => [
  72.             "categories" => [
  73.                 "private" => "privati",
  74.                 "hotel" => "albergo",
  75.                 "company" => "azienda",
  76.             ],
  77.             "locale" => "it-IT",
  78.             "path" => "/negozio/riferimento/riferimento-progetti/",
  79.         ],
  80.     ];
  81.     public const GALLERY_CONTROLLER_ROUTE 'frontend.reference.gallery';
  82.     private $connection;
  83.     public function deactivate(DeactivateContext $deactivateContext): void
  84.     {
  85.         $this->removeGallerySeoURLs();
  86.     }
  87.     public function activate(ActivateContext $activateContext): void
  88.     {
  89.         $this->createGallerySeoURLs($activateContext->getContext());
  90.     }
  91.     public function install(InstallContext $installContext): void
  92.     {
  93.         $this->createCustomFields($installContext->getContext());
  94.     }
  95.     public function uninstall(UninstallContext $uninstallContext): void
  96.     {
  97.         if ($uninstallContext->keepUserData()) {
  98.             return;
  99.         }
  100.         $this->deleteCustomFields($uninstallContext->getContext());
  101.     }
  102.     private function createCustomFields(Context $context): void
  103.     {
  104.         $customFieldSetRepository $this->container->get('custom_field_set.repository');
  105.         $criteria = new Criteria();
  106.         $criteria->addFilter(new EqualsFilter('name'self::ATTRIBUTE_SET_NAME));
  107.         if ($customFieldSetRepository->search($criteria$context)->first()) {
  108.             return;
  109.         }
  110.         $customFieldData $this->getCustomFieldData();
  111.         $customFieldSetRepository->upsert([$customFieldData], $context);
  112.     }
  113.     private function deleteCustomFields(Context $context): void
  114.     {
  115.         $customFieldSetRepository $this->container->get('custom_field_set.repository');
  116.         if (!$customFields $this->getCustomFields($customFieldSetRepository$context)) {
  117.             return;
  118.         }
  119.         $customFieldSetRepository->delete(array_values($customFields->getData()), $context);
  120.     }
  121.     private function getCustomFields(EntityRepository &$customFieldSetRepositoryContext &$context): ?IdSearchResult
  122.     {
  123.         $criteria = new Criteria();
  124.         $criteria->addFilter(new EqualsAnyFilter('name', [self::ATTRIBUTE_SET_NAME]));
  125.         $customFields $customFieldSetRepository->searchIds($criteria$context);
  126.         return $customFields->getTotal() > $customFields null;
  127.     }
  128.     private function getCustomFieldData(): array
  129.     {
  130.         return [
  131.             'name' => self::ATTRIBUTE_SET_NAME,
  132.             'config' => [
  133.                 'label' => [
  134.                     'en-GB' => self::ATTRIBUTE_SET_LABEL_EN,
  135.                     'de-DE' => self::ATTRIBUTE_SET_LABEL_DE,
  136.                 ],
  137.             ],
  138.             'customFields' => [
  139.                 $this->getReferenceCustomField(),
  140.                 $this->getTitleCustomField(),
  141.                 $this->getDescriptionCustomField(),
  142.                 $this->getAltCustomField(),
  143.                 $this->getProductCustomField(),
  144.                 $this->getPositioningCustomField(),
  145.             ],
  146.             'relations' => [
  147.                 [
  148.                     'entityName' => 'media',
  149.                 ],
  150.             ],
  151.         ];
  152.     }
  153.     private function getReferenceCustomField(): array
  154.     {
  155.         return $this->getCustomField(
  156.             self::REFERENCE_ATTRIBUTE_NAME,
  157.             CustomFieldTypes::SELECT,
  158.             [
  159.                 'en-GB' => self::REFERENCE_ATTRIBUTE_LABEL_EN,
  160.                 'de-DE' => self::REFERENCE_ATTRIBUTE_LABEL_DE,
  161.             ],
  162.             self::SELECT_COMPONENT_NAME,
  163.             1,
  164.             [
  165.                 'options' => [
  166.                     [
  167.                         'label' => [
  168.                             'en-GB' => 'Private',
  169.                             'de-DE' => 'Privat',
  170.                         ],
  171.                         'value' => 'private',
  172.                     ],
  173.                     [
  174.                         'label' => [
  175.                             'en-GB' => 'Hotel',
  176.                             'de-DE' => 'Hotel',
  177.                         ],
  178.                         'value' => 'hotel',
  179.                     ],
  180.                     [
  181.                         'label' => [
  182.                             'en-GB' => 'Company',
  183.                             'de-DE' => 'Unternehmen',
  184.                         ],
  185.                         'value' => 'company',
  186.                     ],
  187.                 ],
  188.             ]
  189.         );
  190.     }
  191.     private function getTitleCustomField(): array
  192.     {
  193.         return $this->getCustomField(
  194.             self::TITLE_ATTRIBUTE_NAME,
  195.             CustomFieldTypes::TEXT,
  196.             [
  197.                 'en-GB' => self::TITLE_ATTRIBUTE_LABEL_EN,
  198.                 'de-DE' => self::TITLE_ATTRIBUTE_LABEL_DE,
  199.             ],
  200.             self::TEXT_COMPONENT_NAME,
  201.             2
  202.         );
  203.     }
  204.     private function getDescriptionCustomField(): array
  205.     {
  206.         return $this->getCustomField(
  207.             self::DESCRIPTION_ATTRIBUTE_NAME,
  208.             CustomFieldTypes::TEXT,
  209.             [
  210.                 'en-GB' => self::DESCRIPTION_ATTRIBUTE_LABEL_EN,
  211.                 'de-DE' => self::DESCRIPTION_ATTRIBUTE_LABEL_DE,
  212.             ],
  213.             self::EDITOR_COMPONENT_NAME,
  214.             3
  215.         );
  216.     }
  217.     private function getAltCustomField(): array
  218.     {
  219.         return $this->getCustomField(
  220.             self::ALT_ATTRIBUTE_NAME,
  221.             CustomFieldTypes::TEXT,
  222.             [
  223.                 'en-GB' => self::ALT_ATTRIBUTE_LABEL_EN,
  224.                 'de-DE' => self::ALT_ATTRIBUTE_LABEL_DE,
  225.             ],
  226.             self::TEXT_COMPONENT_NAME,
  227.             4
  228.         );
  229.     }
  230.     private function getProductCustomField(): array
  231.     {
  232.         return $this->getCustomField(
  233.             self::PRODUCT_ATTRIBUTE_NAME,
  234.             CustomFieldTypes::ENTITY,
  235.             [
  236.                 'en-GB' => self::PRODUCT_ATTRIBUTE_LABEL_EN,
  237.                 'de-DE' => self::PRODUCT_ATTRIBUTE_LABEL_DE,
  238.             ],
  239.             self::ENTITY_COMPONENT_NAME,
  240.             5,
  241.             [
  242.                 'entity' => 'product'
  243.             ]
  244.         );
  245.     }
  246.     private function getPositioningCustomField(): array
  247.     {
  248.         return $this->getCustomField(
  249.             self::POSITIONING_ATTRIBUTE_NAME,
  250.             CustomFieldTypes::SELECT,
  251.             [
  252.                 'en-GB' => self::POSITIONING_ATTRIBUTE_LABEL_EN,
  253.                 'de-DE' => self::POSITIONING_ATTRIBUTE_LABEL_DE,
  254.             ],
  255.             self::SELECT_COMPONENT_NAME,
  256.             6,
  257.             [
  258.                 'options' => [
  259.                     [
  260.                         'label' => [
  261.                             'en-GB' => 'Center',
  262.                             'de-DE' => 'Mitte',
  263.                         ],
  264.                         'value' => 'center',
  265.                     ],
  266.                     [
  267.                         'label' => [
  268.                             'en-GB' => 'Top',
  269.                             'de-DE' => 'Oben',
  270.                         ],
  271.                         'value' => 'top',
  272.                     ],
  273.                     [
  274.                         'label' => [
  275.                             'en-GB' => 'Right',
  276.                             'de-DE' => 'Rechts',
  277.                         ],
  278.                         'value' => 'right',
  279.                     ],
  280.                     [
  281.                         'label' => [
  282.                             'en-GB' => 'Left',
  283.                             'de-DE' => 'Links',
  284.                         ],
  285.                         'value' => 'left',
  286.                     ],
  287.                     [
  288.                         'label' => [
  289.                             'en-GB' => 'Bottom',
  290.                             'de-DE' => 'Unten',
  291.                         ],
  292.                         'value' => 'bottom',
  293.                     ],
  294.                 ],
  295.             ]
  296.         );
  297.     }
  298.     /**
  299.      * @param string $name
  300.      * @param string $type
  301.      * @param array<string> $label
  302.      * @param string $component
  303.      * @param int $position
  304.      * @param ?array<array<string>> $optional
  305.      */
  306.     private function getCustomField(
  307.         string $name,
  308.         string $type,
  309.         array $label,
  310.         string $component,
  311.         int $position,
  312.         ?array $optional null
  313.     ): array {
  314.         $data = [
  315.             'name' => $name,
  316.             'type' => $type,
  317.             'config' => [
  318.                 'label' => $label,
  319.                 "componentName" => $component,
  320.                 "customFieldType" => $type,
  321.                 'customFieldPosition' => $position,
  322.             ],
  323.         ];
  324.         if ($optional) {
  325.             $data['config'] = array_merge($data['config'], $optional);
  326.         }
  327.         return $data;
  328.     }
  329.     /********************************
  330.      * CREATE / DEACTIVATE SEO URLS
  331.      ********************************/
  332.     private function createGallerySeoURLs(Context $context)
  333.     {
  334.         $urls = [];
  335.         $salesChannelId $this->getStorefrontSalesChannelId($context);
  336.         if (!$salesChannelId) {
  337.             // Might want to throw an error here
  338.             return;
  339.         }
  340.         $connection \Shopware\Core\Kernel::getConnection();
  341.         $connection->executeStatement(
  342.             '
  343.             DELETE FROM `seo_url` WHERE `route_name` = :galleryRoute',
  344.             ['galleryRoute' => self::GALLERY_CONTROLLER_ROUTE]
  345.         );
  346.         foreach (self::GALLERY_CATEGORY_LABELS as $entry) {
  347.             $foreignKeys = [];
  348.             $urls = [];
  349.             $languageId $connection->fetchOne('
  350.             SELECT LOWER(HEX(`language`.id)) as id
  351.             FROM `language`
  352.             INNER JOIN locale
  353.                 ON `language`.`locale_id` = `locale`.`id`
  354.                 AND locale.code = :locale
  355.             ', ['locale' => $entry['locale']]);
  356.             if ($languageId) {
  357.                 foreach ($entry['categories'] as $category => $categoryTranslated) {
  358.                     $foreignKey Uuid::randomHex();
  359.                     $foreignKeys[] = $foreignKey;
  360.                     $urls[] = [
  361.                         'salesChannelId' => $salesChannelId,
  362.                         'foreignKey' => $foreignKey,
  363.                         'languageId' => $languageId,
  364.                         // The name of the route in the respective controller
  365.                         'routeName' => self::GALLERY_CONTROLLER_ROUTE,
  366.                         // The technical path of your custom route, using a given parameter
  367.                         'pathInfo' => "/referenceListing/$category",
  368.                         'isCanonical' => 1,
  369.                         // The SEO URL that you want to use here, in this case just the name
  370.                         'seoPathInfo' => $entry['path'] . $categoryTranslated,
  371.                     ];
  372.                 }
  373.                 $languageContext = new Context(
  374.                     $context->getSource(),
  375.                     $context->getRuleIds(),
  376.                     $context->getCurrencyId(),
  377.                     [$languageId],
  378.                     $context->getVersionId(),
  379.                     $context->getCurrencyFactor(),
  380.                     false,
  381.                     $context->getTaxState(),
  382.                     $context->getRounding()
  383.                 );
  384.                 $this->updateSeoUrls(
  385.                     $languageContext,
  386.                     self::GALLERY_CONTROLLER_ROUTE,
  387.                     array_values($foreignKeys),
  388.                     $urls,
  389.                     $connection
  390.                 );
  391.             }
  392.         }
  393.     }
  394.     private function removeGallerySeoURLs()
  395.     {
  396.         $connection \Shopware\Core\Kernel::getConnection();
  397.         $connection->executeStatement(
  398.             '
  399.             UPDATE `seo_url` SET `is_deleted` = 1 WHERE `route_name` = :galleryRoute',
  400.             ['galleryRoute' => self::GALLERY_CONTROLLER_ROUTE]
  401.         );
  402.     }
  403.     private function getStorefrontSalesChannelId(Context $context): ?string
  404.     {
  405.         $criteria = new Criteria();
  406.         $criteria->addFilter(new EqualsFilter('typeId'Defaults::SALES_CHANNEL_TYPE_STOREFRONT));
  407.         $criteria->addFilter(new EqualsFilter('active'1));
  408.         $salesChannelRepository $this->container->get('sales_channel.repository');
  409.         return $salesChannelRepository->searchIds($criteria$context)->firstId();
  410.     }
  411.     private function updateSeoUrls(
  412.         Context $context,
  413.         string $routeName,
  414.         array $foreignKeys,
  415.         iterable $seoUrls,
  416.         Connection $connection
  417.     ): void {
  418.         $languageId $context->getLanguageId();
  419.         $canonicals $this->findCanonicalPaths($routeName$languageId$foreignKeys$connection);
  420.         $dateTime = (new \DateTimeImmutable())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  421.         $insertQuery = new MultiInsertQueryQueue($connection250falsetrue);
  422.         $updatedFks = [];
  423.         $obsoleted = [];
  424.         $processed = [];
  425.         // should be provided
  426.         $salesChannelId null;
  427.         $updates = [];
  428.         foreach ($seoUrls as $seoUrl) {
  429.             if ($seoUrl instanceof \JsonSerializable) {
  430.                 $seoUrl $seoUrl->jsonSerialize();
  431.             }
  432.             $updates[] = $seoUrl;
  433.             $fk $seoUrl['foreignKey'];
  434.             /** @var string|null $salesChannelId */
  435.             $salesChannelId $seoUrl['salesChannelId'] = $seoUrl['salesChannelId'] ?? null;
  436.             // skip duplicates
  437.             if (isset($processed[$fk][$salesChannelId])) {
  438.                 continue;
  439.             }
  440.             if (!isset($processed[$fk])) {
  441.                 $processed[$fk] = [];
  442.             }
  443.             $processed[$fk][$salesChannelId] = true;
  444.             $updatedFks[] = $fk;
  445.             if (isset($seoUrl['error'])) {
  446.                 continue;
  447.             }
  448.             $existing $canonicals[$fk][$salesChannelId] ?? null;
  449.             if ($existing) {
  450.                 // entity has override or does not change
  451.                 /** @var array{isModified: bool, seoPathInfo: string, salesChannelId: string} $seoUrl */
  452.                 if ($this->skipUpdate($existing$seoUrl)) {
  453.                     continue;
  454.                 }
  455.                 $obsoleted[] = $existing['id'];
  456.             }
  457.             $insert = [];
  458.             $insert['id'] = Uuid::randomBytes();
  459.             if ($salesChannelId) {
  460.                 $insert['sales_channel_id'] = Uuid::fromHexToBytes($salesChannelId);
  461.             }
  462.             $insert['language_id'] = Uuid::fromHexToBytes($languageId);
  463.             $insert['foreign_key'] = Uuid::fromHexToBytes($fk);
  464.             $insert['path_info'] = $seoUrl['pathInfo'];
  465.             $insert['seo_path_info'] = ltrim($seoUrl['seoPathInfo'], '/');
  466.             $insert['route_name'] = $routeName;
  467.             $insert['is_canonical'] = ($seoUrl['isCanonical'] ?? true) ? null;
  468.             $insert['is_modified'] = ($seoUrl['isModified'] ?? false) ? 0;
  469.             $insert['is_deleted'] = ($seoUrl['isDeleted'] ?? true) ? 0;
  470.             $insert['created_at'] = $dateTime;
  471.             $seoUrlRepository $this->container->get('seo_url.repository');
  472.             $insertQuery->addInsert($seoUrlRepository->getDefinition()->getEntityName(), $insert);
  473.         }
  474.         $this->connection $connection;
  475.         RetryableTransaction::retryable(
  476.             $connection,
  477.             function () use ($obsoleted$dateTime$insertQuery$foreignKeys$updatedFks$salesChannelId): void {
  478.                 $this->obsoleteIds($obsoleted$salesChannelId$this->connection);
  479.                 $insertQuery->execute();
  480.                 $deletedIds array_diff($foreignKeys$updatedFks);
  481.                 $notDeletedIds array_unique(array_intersect($foreignKeys$updatedFks));
  482.                 $this->markAsDeleted(true$deletedIds$dateTime$salesChannelId$this->connection);
  483.                 $this->markAsDeleted(false$notDeletedIds$dateTime$salesChannelId$this->connection);
  484.             }
  485.         );
  486.         $this->container->get('event_dispatcher')->dispatch(new SeoUrlUpdateEvent($updates));
  487.     }
  488.     /**
  489.      * @param list<string> $foreignKeys
  490.      *
  491.      * @return array<string, mixed>
  492.      */
  493.     private function findCanonicalPaths(
  494.         string $routeName,
  495.         string $languageId,
  496.         array $foreignKeys,
  497.         Connection $connection
  498.     ): array {
  499.         $fks Uuid::fromHexToBytesList($foreignKeys);
  500.         $languageId Uuid::fromHexToBytes($languageId);
  501.         $query $connection->createQueryBuilder();
  502.         $query->select([
  503.             'LOWER(HEX(seo_url.id)) as id',
  504.             'LOWER(HEX(seo_url.foreign_key)) foreignKey',
  505.             'LOWER(HEX(seo_url.sales_channel_id)) salesChannelId',
  506.             'seo_url.is_modified as isModified',
  507.             'seo_url.seo_path_info seoPathInfo',
  508.         ]);
  509.         $query->from('seo_url''seo_url');
  510.         $query->andWhere('seo_url.route_name = :routeName');
  511.         $query->andWhere('seo_url.language_id = :language_id');
  512.         $query->andWhere('seo_url.is_canonical = 1');
  513.         $query->andWhere('seo_url.foreign_key IN (:foreign_keys)');
  514.         $query->setParameter('routeName'$routeName);
  515.         $query->setParameter('language_id'$languageId);
  516.         $query->setParameter('foreign_keys'$fksConnection::PARAM_STR_ARRAY);
  517.         $rows $query->executeQuery()->fetchAllAssociative();
  518.         $canonicals = [];
  519.         foreach ($rows as $row) {
  520.             $row['isModified'] = (bool) $row['isModified'];
  521.             $foreignKey = (string) $row['foreignKey'];
  522.             if (!isset($canonicals[$foreignKey])) {
  523.                 $canonicals[$foreignKey] = [$row['salesChannelId'] => $row];
  524.                 continue;
  525.             }
  526.             $canonicals[$foreignKey][$row['salesChannelId']] = $row;
  527.         }
  528.         return $canonicals;
  529.     }
  530.     /**
  531.      * @param array{isModified: bool, seoPathInfo: string, salesChannelId: string} $existing
  532.      * @param array{isModified: bool, seoPathInfo: string, salesChannelId: string} $seoUrl
  533.      */
  534.     private function skipUpdate(array $existing, array $seoUrl): bool
  535.     {
  536.         if ($existing['isModified'] && !($seoUrl['isModified'] ?? false) && trim($seoUrl['seoPathInfo']) !== '') {
  537.             return true;
  538.         }
  539.         return $seoUrl['seoPathInfo'] === $existing['seoPathInfo']
  540.             && $seoUrl['salesChannelId'] === $existing['salesChannelId'];
  541.     }
  542.     /**
  543.      * @internal (flag:FEATURE_NEXT_13410) Parameter $salesChannelId will be required
  544.      *
  545.      * @param list<string> $ids
  546.      */
  547.     private function obsoleteIds(array $ids, ?string $salesChannelIdConnection $connection): void
  548.     {
  549.         if (empty($ids)) {
  550.             return;
  551.         }
  552.         $ids Uuid::fromHexToBytesList($ids);
  553.         $query $connection->createQueryBuilder()
  554.             ->update('seo_url')
  555.             ->set('is_canonical''NULL')
  556.             ->where('id IN (:ids)')
  557.             ->setParameter('ids'$idsConnection::PARAM_STR_ARRAY);
  558.         if ($salesChannelId) {
  559.             $query->andWhere('sales_channel_id = :salesChannelId');
  560.             $query->setParameter('salesChannelId'Uuid::fromHexToBytes($salesChannelId));
  561.         }
  562.         RetryableQuery::retryable($connection, function () use ($query): void {
  563.             $query->execute();
  564.         });
  565.     }
  566.     /**
  567.      * @internal (flag:FEATURE_NEXT_13410) Parameter $salesChannelId will be required
  568.      *
  569.      * @param list<string> $ids
  570.      */
  571.     private function markAsDeleted(
  572.         bool $deleted,
  573.         array $ids,
  574.         string $dateTime,
  575.         ?string $salesChannelId,
  576.         Connection $connection
  577.     ): void {
  578.         if (empty($ids)) {
  579.             return;
  580.         }
  581.         $ids Uuid::fromHexToBytesList($ids);
  582.         $query $connection->createQueryBuilder()
  583.             ->update('seo_url')
  584.             ->set('is_deleted'$deleted '1' '0')
  585.             ->where('foreign_key IN (:fks)')
  586.             ->setParameter('fks'$idsConnection::PARAM_STR_ARRAY);
  587.         if ($salesChannelId) {
  588.             $query->andWhere('sales_channel_id = :salesChannelId');
  589.             $query->setParameter('salesChannelId'Uuid::fromHexToBytes($salesChannelId));
  590.         }
  591.         $query->execute();
  592.     }
  593. }