vendor/shopware/core/Content/Seo/SeoUrlGenerator.php line 77

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Seo;
  3. use Shopware\Core\Content\Seo\Exception\InvalidTemplateException;
  4. use Shopware\Core\Content\Seo\SeoUrl\SeoUrlEntity;
  5. use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlMapping;
  6. use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteConfig;
  7. use Shopware\Core\Content\Seo\SeoUrlRoute\SeoUrlRouteInterface;
  8. use Shopware\Core\Framework\Adapter\Twig\TwigVariableParser;
  9. use Shopware\Core\Framework\Context;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityDefinitionQueryHelper;
  12. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  14. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  15. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  19. use Shopware\Core\Framework\Log\Package;
  20. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  21. use Symfony\Component\HttpFoundation\RequestStack;
  22. use Symfony\Component\Routing\RouterInterface;
  23. use Twig\Environment;
  24. use Twig\Error\SyntaxError;
  25. use Twig\Loader\ArrayLoader;
  26. #[Package('sales-channel')]
  27. class SeoUrlGenerator
  28. {
  29.     public const ESCAPE_SLUGIFY 'slugifyurlencode';
  30.     private RouterInterface $router;
  31.     private Environment $twig;
  32.     private DefinitionInstanceRegistry $definitionRegistry;
  33.     private RequestStack $requestStack;
  34.     private TwigVariableParser $twigVariableParser;
  35.     /**
  36.      * @internal
  37.      */
  38.     public function __construct(
  39.         DefinitionInstanceRegistry $definitionRegistry,
  40.         RouterInterface $router,
  41.         RequestStack $requestStack,
  42.         Environment $environment,
  43.         TwigVariableParser $twigVariableParser
  44.     ) {
  45.         $this->definitionRegistry $definitionRegistry;
  46.         $this->router $router;
  47.         $this->requestStack $requestStack;
  48.         $this->twig $environment;
  49.         $this->twigVariableParser $twigVariableParser;
  50.     }
  51.     /**
  52.      * @feature-deprecated (flag:FEATURE_NEXT_13410) Parameter $salesChannel will be required
  53.      *
  54.      * @param array<string|array<string, string>> $ids
  55.      *
  56.      * @return iterable<SeoUrlEntity>
  57.      */
  58.     public function generate(array $idsstring $templateSeoUrlRouteInterface $routeContext $context, ?SalesChannelEntity $salesChannel): iterable
  59.     {
  60.         $criteria = new Criteria($ids);
  61.         $route->prepareCriteria($criteria$salesChannel);
  62.         $config $route->getConfig();
  63.         $repository $this->definitionRegistry->getRepository($config->getDefinition()->getEntityName());
  64.         $associations $this->getAssociations($template$repository->getDefinition());
  65.         $criteria->addAssociations($associations);
  66.         $criteria->setLimit(50);
  67.         /** @var RepositoryIterator $iterator */
  68.         $iterator $context->enableInheritance(static function (Context $context) use ($repository$criteria) {
  69.             return new RepositoryIterator($repository$context$criteria);
  70.         });
  71.         $this->setTwigTemplate($config$template);
  72.         while ($entities $iterator->fetch()) {
  73.             yield from $this->generateUrls($route$config$salesChannel$entities);
  74.         }
  75.     }
  76.     /**
  77.      * @internal (flag:FEATURE_NEXT_13410) Parameter $salesChannel will be required
  78.      *
  79.      * @param EntityCollection<Entity> $entities
  80.      *
  81.      * @return iterable<SeoUrlEntity>
  82.      */
  83.     private function generateUrls(SeoUrlRouteInterface $seoUrlRouteSeoUrlRouteConfig $config, ?SalesChannelEntity $salesChannelEntityCollection $entities): iterable
  84.     {
  85.         $request $this->requestStack->getMainRequest();
  86.         $basePath $request $request->getBasePath() : '';
  87.         /** @var Entity $entity */
  88.         foreach ($entities as $entity) {
  89.             $seoUrl = new SeoUrlEntity();
  90.             $seoUrl->setForeignKey($entity->getUniqueIdentifier());
  91.             $seoUrl->setIsCanonical(true);
  92.             $seoUrl->setIsModified(false);
  93.             $seoUrl->setIsDeleted(false);
  94.             $copy = clone $seoUrl;
  95.             $mapping $seoUrlRoute->getMapping($entity$salesChannel);
  96.             $copy->setError($mapping->getError());
  97.             $pathInfo $this->router->generate($config->getRouteName(), $mapping->getInfoPathContext());
  98.             $pathInfo $this->removePrefix($pathInfo$basePath);
  99.             $copy->setPathInfo($pathInfo);
  100.             $seoPathInfo $this->getSeoPathInfo($mapping$config);
  101.             if ($seoPathInfo === null || $seoPathInfo === '') {
  102.                 continue;
  103.             }
  104.             $copy->setSeoPathInfo($seoPathInfo);
  105.             if ($salesChannel !== null) {
  106.                 $copy->setSalesChannelId($salesChannel->getId());
  107.             } else {
  108.                 $copy->setSalesChannelId(null);
  109.             }
  110.             yield $copy;
  111.         }
  112.     }
  113.     private function getSeoPathInfo(SeoUrlMapping $mappingSeoUrlRouteConfig $config): ?string
  114.     {
  115.         try {
  116.             return trim($this->twig->render('template'$mapping->getSeoPathInfoContext()));
  117.         } catch (\Throwable $error) {
  118.             if (!$config->getSkipInvalid()) {
  119.                 throw $error;
  120.             }
  121.             return null;
  122.         }
  123.     }
  124.     private function setTwigTemplate(SeoUrlRouteConfig $configstring $template): void
  125.     {
  126.         $template '{% autoescape \'' self::ESCAPE_SLUGIFY "' %}$template{% endautoescape %}";
  127.         $this->twig->setLoader(new ArrayLoader(['template' => $template]));
  128.         try {
  129.             $this->twig->loadTemplate($this->twig->getTemplateClass('template'), 'template');
  130.         } catch (SyntaxError $syntaxError) {
  131.             if (!$config->getSkipInvalid()) {
  132.                 throw new InvalidTemplateException('Syntax error: ' $syntaxError->getMessage());
  133.             }
  134.         }
  135.     }
  136.     private function removePrefix(string $subjectstring $prefix): string
  137.     {
  138.         if (!$prefix || mb_strpos($subject$prefix) !== 0) {
  139.             return $subject;
  140.         }
  141.         return mb_substr($subjectmb_strlen($prefix));
  142.     }
  143.     /**
  144.      * @return list<string>
  145.      */
  146.     private function getAssociations(string $templateEntityDefinition $definition): array
  147.     {
  148.         try {
  149.             $variables $this->twigVariableParser->parse($template);
  150.         } catch (\Exception $e) {
  151.             $e = new InvalidTemplateException($e->getMessage());
  152.             throw $e;
  153.         }
  154.         $associations = [];
  155.         foreach ($variables as $variable) {
  156.             $fields EntityDefinitionQueryHelper::getFieldsOfAccessor($definition$variabletrue);
  157.             /** @var Field|null $lastField */
  158.             $lastField end($fields);
  159.             $runtime = new Runtime();
  160.             if ($lastField && $lastField->getFlag(Runtime::class)) {
  161.                 $associations array_merge($associations$runtime->getDepends());
  162.             }
  163.             $associations[] = EntityDefinitionQueryHelper::getAssociationPath($variable$definition);
  164.         }
  165.         return array_filter(array_unique($associations));
  166.     }
  167. }