vendor/pimcore/pimcore/lib/Navigation/Builder.php line 197

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Navigation;
  15. use Pimcore\Cache as CacheManager;
  16. use Pimcore\Http\RequestHelper;
  17. use Pimcore\Logger;
  18. use Pimcore\Model\Document;
  19. use Pimcore\Model\Site;
  20. use Pimcore\Navigation\Iterator\PrefixRecursiveFilterIterator;
  21. use Pimcore\Navigation\Page\Document as DocumentPage;
  22. use Pimcore\Navigation\Page\Url;
  23. use Symfony\Component\OptionsResolver\OptionsResolver;
  24. class Builder
  25. {
  26.     /**
  27.      * @var RequestHelper
  28.      */
  29.     private $requestHelper;
  30.     /**
  31.      * @internal
  32.      *
  33.      * @var string
  34.      */
  35.     protected $htmlMenuIdPrefix;
  36.     /**
  37.      * @internal
  38.      *
  39.      * @var string
  40.      */
  41.     protected $pageClass DocumentPage::class;
  42.     /**
  43.      * @var int
  44.      */
  45.     private $currentLevel 0;
  46.     /**
  47.      * @var array
  48.      */
  49.     private $navCacheTags = [];
  50.     /**
  51.      * @var OptionsResolver
  52.      */
  53.     private $optionsResolver;
  54.     /**
  55.      * @param RequestHelper $requestHelper
  56.      * @param string|null $pageClass
  57.      */
  58.     public function __construct(RequestHelper $requestHelper, ?string $pageClass null)
  59.     {
  60.         $this->requestHelper $requestHelper;
  61.         if (null !== $pageClass) {
  62.             $this->pageClass $pageClass;
  63.         }
  64.         $this->optionsResolver = new OptionsResolver();
  65.         $this->configureOptions($this->optionsResolver);
  66.     }
  67.     /**
  68.      * @param OptionsResolver $options
  69.      */
  70.     protected function configureOptions(OptionsResolver $options)
  71.     {
  72.         $options->setDefaults([
  73.             'root' => null,
  74.             'htmlMenuPrefix' => null,
  75.             'pageCallback' => null,
  76.             'cache' => true,
  77.             'cacheLifetime' => null,
  78.             'maxDepth' => null,
  79.             'active' => null,
  80.             'markActiveTrail' => true,
  81.         ]);
  82.         $options->setAllowedTypes('root', [Document::class, 'null']);
  83.         $options->setAllowedTypes('htmlMenuPrefix', ['string''null']);
  84.         $options->setAllowedTypes('pageCallback', ['callable''null']);
  85.         $options->setAllowedTypes('cache', ['string''bool']);
  86.         $options->setAllowedTypes('cacheLifetime', ['int''null']);
  87.         $options->setAllowedTypes('maxDepth', ['int''null']);
  88.         $options->setAllowedTypes('active', [Document::class, 'null']);
  89.         $options->setAllowedTypes('markActiveTrail', ['bool']);
  90.     }
  91.     /**
  92.      * @param array $options
  93.      *
  94.      * @return array
  95.      */
  96.     protected function resolveOptions(array $options): array
  97.     {
  98.         return $this->optionsResolver->resolve($options);
  99.     }
  100.     /**
  101.      * @param array|Document|null $activeDocument
  102.      * @param Document|null $navigationRootDocument
  103.      * @param string|null $htmlMenuIdPrefix
  104.      * @param \Closure|null $pageCallback
  105.      * @param bool|string $cache
  106.      * @param int|null $maxDepth
  107.      * @param int|null $cacheLifetime
  108.      *
  109.      * @return Container
  110.      *
  111.      * @throws \Exception
  112.      */
  113.     public function getNavigation($activeDocument null$navigationRootDocument null$htmlMenuIdPrefix null$pageCallback null$cache true, ?int $maxDepth null, ?int $cacheLifetime null)
  114.     {
  115.         //TODO Pimcore 11: remove the `if (...)` block to remove the BC layer
  116.         if (func_num_args() > || ($activeDocument !== null && !is_array($activeDocument))) {
  117.             trigger_deprecation('pimcore/pimcore''10.5''Calling Pimcore\Navigation\Builder::getNavigation() using extra arguments is deprecated and will be removed in Pimcore 11.' .
  118.             'Instead, specify the arguments as an array');
  119.         } else {
  120.             [
  121.                 'root' => $navigationRootDocument,
  122.                 'htmlMenuPrefix' => $htmlMenuIdPrefix,
  123.                 'pageCallback' => $pageCallback,
  124.                 'cache' => $cache,
  125.                 'cacheLifetime' => $cacheLifetime,
  126.                 'maxDepth' => $maxDepth,
  127.                 'active' => $activeDocument,
  128.                 'markActiveTrail' => $markActiveTrail,
  129.             ] = $this->resolveOptions($activeDocument);
  130.         }
  131.         $markActiveTrail ??= true//TODO Pimcore 11: remove with the BC layer
  132.         $cacheEnabled $cache !== false;
  133.         $this->htmlMenuIdPrefix $htmlMenuIdPrefix;
  134.         if (!$navigationRootDocument) {
  135.             $navigationRootDocument Document::getById(1);
  136.         }
  137.         $navigation null;
  138.         $cacheKey null;
  139.         if ($cacheEnabled) {
  140.             // the cache key consists out of the ID and the class name (eg. for hardlinks) of the root document and the optional html prefix
  141.             $cacheKeys = ['root_id__' $navigationRootDocument->getId(), $htmlMenuIdPrefixget_class($navigationRootDocument)];
  142.             if (Site::isSiteRequest()) {
  143.                 $site Site::getCurrentSite();
  144.                 $cacheKeys[] = 'site__' $site->getId();
  145.             }
  146.             if (is_string($cache)) {
  147.                 $cacheKeys[] = 'custom__' $cache;
  148.             }
  149.             if ($pageCallback instanceof \Closure) {
  150.                 $cacheKeys[] = 'pageCallback_' closureHash($pageCallback);
  151.             }
  152.             if ($maxDepth) {
  153.                 $cacheKeys[] = 'maxDepth_' $maxDepth;
  154.             }
  155.             $cacheKey 'nav_' md5(serialize($cacheKeys));
  156.             $navigation CacheManager::load($cacheKey);
  157.         }
  158.         if (!$navigation instanceof Container) {
  159.             $navigation = new Container();
  160.             $this->navCacheTags = ['output''navigation'];
  161.             if ($navigationRootDocument->hasChildren()) {
  162.                 $this->currentLevel 0;
  163.                 $rootPage $this->buildNextLevel($navigationRootDocumenttrue$pageCallback, [], $maxDepth);
  164.                 $navigation->addPages($rootPage);
  165.             }
  166.             // we need to force caching here, otherwise the active classes and other settings will be set and later
  167.             // also written into cache (pass-by-reference) ... when serializing the data directly here, we don't have this problem
  168.             if ($cacheEnabled) {
  169.                 CacheManager::save($navigation$cacheKey$this->navCacheTags$cacheLifetime999true);
  170.             }
  171.         }
  172.         if ($markActiveTrail) {
  173.             $this->markActiveTrail($navigation$activeDocument);
  174.         }
  175.         return $navigation;
  176.     }
  177.     /**
  178.      * @internal
  179.      *
  180.      * @param Container $navigation
  181.      * @param Document|null $activeDocument
  182.      *
  183.      * @return void
  184.      */
  185.     protected function markActiveTrail(Container $navigation, ?Document $activeDocument): void
  186.     {
  187.         $activePages = [];
  188.         if ($this->requestHelper->hasMainRequest()) {
  189.             $request $this->requestHelper->getMainRequest();
  190.             // try to find a page matching exactly the request uri
  191.             $activePages $this->findActivePages($navigation'uri'$request->getRequestUri());
  192.             if (empty($activePages)) {
  193.                 // try to find a page matching the path info
  194.                 $activePages $this->findActivePages($navigation'uri'$request->getPathInfo());
  195.             }
  196.         }
  197.         if ($activeDocument) {
  198.             if (empty($activePages)) {
  199.                 // use the provided pimcore document
  200.                 $activePages $this->findActivePages($navigation'realFullPath'$activeDocument->getRealFullPath());
  201.             }
  202.             if (empty($activePages)) {
  203.                 // find by link target
  204.                 $activePages $this->findActivePages($navigation'uri'$activeDocument->getFullPath());
  205.             }
  206.         }
  207.         $isLink = static fn ($page): bool => $page instanceof DocumentPage && $page->getDocumentType() === 'link';
  208.         // cleanup active pages from links
  209.         // pages have priority, if we don't find any active page, we use all we found
  210.         if ($nonLinkPages array_filter($activePages, static fn ($page): bool => !$isLink($page))) {
  211.             $activePages $nonLinkPages;
  212.         }
  213.         if ($activePages) {
  214.             // we found an active document, so we can build the active trail by getting respectively the parent
  215.             foreach ($activePages as $activePage) {
  216.                 $this->addActiveCssClasses($activePagetrue);
  217.             }
  218.             return;
  219.         }
  220.         if ($activeDocument) {
  221.             // we didn't find the active document, so we try to build the trail on our own
  222.             $allPages = new \RecursiveIteratorIterator($navigation\RecursiveIteratorIterator::SELF_FIRST);
  223.             foreach ($allPages as $page) {
  224.                 if (!$page instanceof Url || !$page->getUri()) {
  225.                     continue;
  226.                 }
  227.                 $uri $page->getUri() . '/';
  228.                 $isActive str_starts_with($activeDocument->getRealFullPath(), $uri)
  229.                     || ($isLink($page) && str_starts_with($activeDocument->getFullPath(), $uri));
  230.                 if ($isActive) {
  231.                     $page->setActive(true);
  232.                     $page->setClass($page->getClass() . ' active active-trail');
  233.                 }
  234.             }
  235.         }
  236.     }
  237.     /**
  238.      * @internal
  239.      *
  240.      * @param Container $navigation navigation container to iterate
  241.      * @param string $property name of property to match against
  242.      * @param string $value value to match property against
  243.      *
  244.      * @return Page[]
  245.      */
  246.     protected function findActivePages(Container $navigationstring $propertystring $value): array
  247.     {
  248.         $filterByPrefix = new PrefixRecursiveFilterIterator($navigation$property$value);
  249.         $flatten = new \RecursiveIteratorIterator($filterByPrefix\RecursiveIteratorIterator::SELF_FIRST);
  250.         $filterMatches = new \CallbackFilterIterator($flatten, static fn (Page $page): bool => $page->get($property) === $value);
  251.         return iterator_to_array($filterMatchesfalse);
  252.     }
  253.     /**
  254.      * @internal
  255.      *
  256.      * @param Page $page
  257.      * @param bool $isActive
  258.      *
  259.      * @throws \Exception
  260.      */
  261.     protected function addActiveCssClasses(Page $page$isActive false)
  262.     {
  263.         $page->setActive(true);
  264.         $parent $page->getParent();
  265.         $isRoot false;
  266.         $classes '';
  267.         if ($parent instanceof DocumentPage) {
  268.             $this->addActiveCssClasses($parent);
  269.         } else {
  270.             $isRoot true;
  271.         }
  272.         $classes .= ' active';
  273.         if (!$isActive) {
  274.             $classes .= ' active-trail';
  275.         }
  276.         if ($isRoot && $isActive) {
  277.             $classes .= ' mainactive';
  278.         }
  279.         $page->setClass($page->getClass() . $classes);
  280.     }
  281.     /**
  282.      * @param string $pageClass
  283.      *
  284.      * @return $this
  285.      */
  286.     public function setPageClass(string $pageClass)
  287.     {
  288.         $this->pageClass $pageClass;
  289.         return $this;
  290.     }
  291.     /**
  292.      * Returns the name of the pageclass
  293.      *
  294.      * @return String
  295.      */
  296.     public function getPageClass()
  297.     {
  298.         return $this->pageClass;
  299.     }
  300.     /**
  301.      * @param Document $parentDocument
  302.      *
  303.      * @return Document[]
  304.      */
  305.     protected function getChildren(Document $parentDocument): array
  306.     {
  307.         // the intention of this function is mainly to be overridden in order to customize the behavior of the navigation
  308.         // e.g. for custom filtering and other very specific use-cases
  309.         return $parentDocument->getChildren();
  310.     }
  311.     /**
  312.      * @internal
  313.      *
  314.      * @param Document $parentDocument
  315.      * @param bool $isRoot
  316.      * @param callable $pageCallback
  317.      * @param array $parents
  318.      * @param int|null $maxDepth
  319.      *
  320.      * @return Page[]
  321.      *
  322.      * @throws \Exception
  323.      */
  324.     protected function buildNextLevel($parentDocument$isRoot false$pageCallback null$parents = [], $maxDepth null)
  325.     {
  326.         $this->currentLevel++;
  327.         $pages = [];
  328.         $childs $this->getChildren($parentDocument);
  329.         $parents[$parentDocument->getId()] = $parentDocument;
  330.         if (!is_array($childs)) {
  331.             return $pages;
  332.         }
  333.         foreach ($childs as $child) {
  334.             $classes '';
  335.             if ($child instanceof Document\Hardlink) {
  336.                 $child Document\Hardlink\Service::wrap($child);
  337.                 if (!$child) {
  338.                     continue;
  339.                 }
  340.             }
  341.             // infinite loop detection, we use array keys here, because key lookups are much faster
  342.             if (isset($parents[$child->getId()])) {
  343.                 Logger::critical('Navigation: Document with ID ' $child->getId() . ' would produce an infinite loop -> skipped, parent IDs (' implode(','array_keys($parents)) . ')');
  344.                 continue;
  345.             }
  346.             if ($child instanceof Document\Folder || $child instanceof Document\Page || $child instanceof Document\Link) {
  347.                 $path $child->getFullPath();
  348.                 if ($child instanceof Document\Link) {
  349.                     $path $child->getHref();
  350.                 }
  351.                 /** @var DocumentPage $page */
  352.                 $page = new $this->pageClass();
  353.                 if (!$child instanceof Document\Folder) {
  354.                     $page->setUri($path $child->getProperty('navigation_parameters') . $child->getProperty('navigation_anchor'));
  355.                 }
  356.                 $page->setLabel($child->getProperty('navigation_name'));
  357.                 $page->setActive(false);
  358.                 $page->setId($this->htmlMenuIdPrefix $child->getId());
  359.                 $page->setClass($child->getProperty('navigation_class'));
  360.                 $page->setTarget($child->getProperty('navigation_target'));
  361.                 $page->setTitle($child->getProperty('navigation_title'));
  362.                 $page->setAccesskey($child->getProperty('navigation_accesskey'));
  363.                 $page->setTabindex($child->getProperty('navigation_tabindex'));
  364.                 $page->setRelation($child->getProperty('navigation_relation'));
  365.                 $page->setDocument($child);
  366.                 if (trim((string)$child->getProperty('navigation_name')) === '' || $child->getProperty('navigation_exclude') || !$child->getPublished()) {
  367.                     $page->setVisible(false);
  368.                 }
  369.                 if ($isRoot) {
  370.                     $classes .= ' main';
  371.                 }
  372.                 $page->setClass($page->getClass() . $classes);
  373.                 if ($child->hasChildren() && (!$maxDepth || $maxDepth $this->currentLevel)) {
  374.                     $childPages $this->buildNextLevel($childfalse$pageCallback$parents$maxDepth);
  375.                     $page->setPages($childPages);
  376.                 }
  377.                 if ($pageCallback instanceof \Closure) {
  378.                     $pageCallback($page$child);
  379.                 }
  380.                 $this->navCacheTags[] = $page->getDocument()->getCacheTag();
  381.                 $pages[] = $page;
  382.             }
  383.         }
  384.         $this->currentLevel--;
  385.         return $pages;
  386.     }
  387. }