src/Core/Content/Sitemap/Provider/ProductUrlProvider.php line 53

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Sitemap\Provider;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Content\Product\ProductDefinition;
  5. use Shopware\Core\Content\Product\ProductEntity;
  6. use Shopware\Core\Content\Sitemap\Service\ConfigHandler;
  7. use Shopware\Core\Content\Sitemap\Struct\Url;
  8. use Shopware\Core\Content\Sitemap\Struct\UrlResult;
  9. use Shopware\Core\Defaults;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\IteratorFactory;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
  12. use Shopware\Core\Framework\Log\Package;
  13. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  16. use Shopware\Core\System\SystemConfig\SystemConfigService;
  17. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  18. use Symfony\Component\Routing\RouterInterface;
  19. #[Package('sales-channel')]
  20. class ProductUrlProvider extends AbstractUrlProvider
  21. {
  22.     final public const CHANGE_FREQ 'hourly';
  23.     private const CONFIG_HIDE_AFTER_CLOSEOUT 'core.listing.hideCloseoutProductsWhenOutOfStock';
  24.     /**
  25.      * @internal
  26.      */
  27.     public function __construct(private readonly ConfigHandler $configHandler, private readonly Connection $connection, private readonly ProductDefinition $definition, private readonly IteratorFactory $iteratorFactory, private readonly RouterInterface $router, private readonly SystemConfigService $systemConfigService)
  28.     {
  29.     }
  30.     public function getDecorated(): AbstractUrlProvider
  31.     {
  32.         throw new DecorationPatternException(self::class);
  33.     }
  34.     public function getName(): string
  35.     {
  36.         return 'product';
  37.     }
  38.     /**
  39.      * {@inheritdoc}
  40.      *
  41.      * @throws \Exception
  42.      */
  43.     public function getUrls(SalesChannelContext $contextint $limit, ?int $offset null): UrlResult
  44.     {
  45.         $products $this->getProducts($context$limit$offset);
  46.         if (empty($products)) {
  47.             return new UrlResult([], null);
  48.         }
  49.         $keys FetchModeHelper::keyPair($products);
  50.         $seoUrls $this->getSeoUrls(array_values($keys), 'frontend.detail.page'$context$this->connection);
  51.         $seoUrls FetchModeHelper::groupUnique($seoUrls);
  52.         $urls = [];
  53.         $url = new Url();
  54.         foreach ($products as $product) {
  55.             $lastMod $product['updated_at'] ?: $product['created_at'];
  56.             $lastMod = (new \DateTime($lastMod))->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  57.             $newUrl = clone $url;
  58.             if (isset($seoUrls[$product['id']])) {
  59.                 $newUrl->setLoc($seoUrls[$product['id']]['seo_path_info']);
  60.             } else {
  61.                 $newUrl->setLoc($this->router->generate('frontend.detail.page', ['productId' => $product['id']], UrlGeneratorInterface::ABSOLUTE_PATH));
  62.             }
  63.             $newUrl->setLastmod(new \DateTime($lastMod));
  64.             $newUrl->setChangefreq(self::CHANGE_FREQ);
  65.             $newUrl->setResource(ProductEntity::class);
  66.             $newUrl->setIdentifier($product['id']);
  67.             $urls[] = $newUrl;
  68.         }
  69.         $keys array_keys($keys);
  70.         /** @var int|null $nextOffset */
  71.         $nextOffset array_pop($keys);
  72.         return new UrlResult($urls$nextOffset);
  73.     }
  74.     private function getProducts(SalesChannelContext $contextint $limit, ?int $offset): array
  75.     {
  76.         $lastId null;
  77.         if ($offset) {
  78.             $lastId = ['offset' => $offset];
  79.         }
  80.         $iterator $this->iteratorFactory->createIterator($this->definition$lastId);
  81.         $query $iterator->getQuery();
  82.         $query->setMaxResults($limit);
  83.         $showAfterCloseout = !$this->systemConfigService->get(self::CONFIG_HIDE_AFTER_CLOSEOUT$context->getSalesChannelId());
  84.         $query->addSelect([
  85.             '`product`.created_at as created_at',
  86.             '`product`.updated_at as updated_at',
  87.         ]);
  88.         $query->leftJoin('`product`''`product`''parent''`product`.parent_id = parent.id');
  89.         $query->innerJoin('`product`''product_visibility''visibilities''product.visibilities = visibilities.product_id');
  90.         $query->andWhere('`product`.version_id = :versionId');
  91.         if ($showAfterCloseout) {
  92.             $query->andWhere('(`product`.available = 1 OR `product`.is_closeout)');
  93.         } else {
  94.             $query->andWhere('`product`.available = 1');
  95.         }
  96.         $query->andWhere('IFNULL(`product`.active, parent.active) = 1');
  97.         $query->andWhere('(`product`.child_count = 0 OR `product`.parent_id IS NOT NULL)');
  98.         $query->andWhere('(`product`.parent_id IS NULL OR parent.canonical_product_id IS NULL OR parent.canonical_product_id = `product`.id)');
  99.         $query->andWhere('visibilities.product_version_id = :versionId');
  100.         $query->andWhere('visibilities.sales_channel_id = :salesChannelId');
  101.         $excludedProductIds $this->getExcludedProductIds($context);
  102.         if (!empty($excludedProductIds)) {
  103.             $query->andWhere('`product`.id NOT IN (:productIds)');
  104.             $query->setParameter('productIds'Uuid::fromHexToBytesList($excludedProductIds), Connection::PARAM_STR_ARRAY);
  105.         }
  106.         $query->setParameter('versionId'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  107.         $query->setParameter('salesChannelId'Uuid::fromHexToBytes($context->getSalesChannelId()));
  108.         return $query->executeQuery()->fetchAllAssociative();
  109.     }
  110.     private function getExcludedProductIds(SalesChannelContext $salesChannelContext): array
  111.     {
  112.         $salesChannelId $salesChannelContext->getSalesChannel()->getId();
  113.         $excludedUrls $this->configHandler->get(ConfigHandler::EXCLUDED_URLS_KEY);
  114.         if (empty($excludedUrls)) {
  115.             return [];
  116.         }
  117.         $excludedUrls array_filter($excludedUrls, static function (array $excludedUrl) use ($salesChannelId) {
  118.             if ($excludedUrl['resource'] !== ProductEntity::class) {
  119.                 return false;
  120.             }
  121.             if ($excludedUrl['salesChannelId'] !== $salesChannelId) {
  122.                 return false;
  123.             }
  124.             return true;
  125.         });
  126.         return array_column($excludedUrls'identifier');
  127.     }
  128. }