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

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 mixed|\Pimcore\Navigation\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 (func_num_args() > 1)` block to remove the BC layer
  116.         if (func_num_args() > 1) {
  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.         // the cache key consists out of the ID and the class name (eg. for hardlinks) of the root document and the optional html prefix
  138.         $cacheKeys = ['root_id__' $navigationRootDocument->getId(), $htmlMenuIdPrefixget_class($navigationRootDocument)];
  139.         if (Site::isSiteRequest()) {
  140.             $site Site::getCurrentSite();
  141.             $cacheKeys[] = 'site__' $site->getId();
  142.         }
  143.         if (is_string($cache)) {
  144.             $cacheKeys[] = 'custom__' $cache;
  145.         }
  146.         if ($pageCallback instanceof \Closure) {
  147.             $cacheKeys[] = 'pageCallback_' closureHash($pageCallback);
  148.         }
  149.         if ($maxDepth) {
  150.             $cacheKeys[] = 'maxDepth_' $maxDepth;
  151.         }
  152.         $cacheKey 'nav_' md5(serialize($cacheKeys));
  153.         $navigation CacheManager::load($cacheKey);
  154.         if (!$navigation || !$cacheEnabled) {
  155.             $navigation = new \Pimcore\Navigation\Container();
  156.             $this->navCacheTags = ['output''navigation'];
  157.             if ($navigationRootDocument->hasChildren()) {
  158.                 $this->currentLevel 0;
  159.                 $rootPage $this->buildNextLevel($navigationRootDocumenttrue$pageCallback, [], $maxDepth);
  160.                 $navigation->addPages($rootPage);
  161.             }
  162.             // we need to force caching here, otherwise the active classes and other settings will be set and later
  163.             // also written into cache (pass-by-reference) ... when serializing the data directly here, we don't have this problem
  164.             if ($cacheEnabled) {
  165.                 CacheManager::save($navigation$cacheKey$this->navCacheTags$cacheLifetime999true);
  166.             }
  167.         }
  168.         if ($markActiveTrail) {
  169.             $this->markActiveTrail($navigation$activeDocument);
  170.         }
  171.         return $navigation;
  172.     }
  173.     /**
  174.      * @internal
  175.      *
  176.      * @param Container $navigation
  177.      * @param Document|null $activeDocument
  178.      *
  179.      * @return void
  180.      */
  181.     protected function markActiveTrail(Container $navigation, ?Document $activeDocument): void
  182.     {
  183.         $activePages = [];
  184.         if ($this->requestHelper->hasMainRequest()) {
  185.             $request $this->requestHelper->getMainRequest();
  186.             // try to find a page matching exactly the request uri
  187.             $activePages $this->findActivePages($navigation'uri'$request->getRequestUri());
  188.             if (empty($activePages)) {
  189.                 // try to find a page matching the path info
  190.                 $activePages $this->findActivePages($navigation'uri'$request->getPathInfo());
  191.             }
  192.         }
  193.         if ($activeDocument) {
  194.             if (empty($activePages)) {
  195.                 // use the provided pimcore document
  196.                 $activePages $this->findActivePages($navigation'realFullPath'$activeDocument->getRealFullPath());
  197.             }
  198.             if (empty($activePages)) {
  199.                 // find by link target
  200.                 $activePages $this->findActivePages($navigation'uri'$activeDocument->getFullPath());
  201.             }
  202.         }
  203.         $isLink = static fn ($page): bool => $page instanceof DocumentPage && $page->getDocumentType() === 'link';
  204.         // cleanup active pages from links
  205.         // pages have priority, if we don't find any active page, we use all we found
  206.         if ($nonLinkPages array_filter($activePages, static fn ($page): bool => !$isLink($page))) {
  207.             $activePages $nonLinkPages;
  208.         }
  209.         if ($activePages) {
  210.             // we found an active document, so we can build the active trail by getting respectively the parent
  211.             foreach ($activePages as $activePage) {
  212.                 $this->addActiveCssClasses($activePagetrue);
  213.             }
  214.             return;
  215.         }
  216.         if ($activeDocument) {
  217.             // we didn't find the active document, so we try to build the trail on our own
  218.             $allPages = new \RecursiveIteratorIterator($navigation\RecursiveIteratorIterator::SELF_FIRST);
  219.             foreach ($allPages as $page) {
  220.                 if (!$page instanceof Url || !$page->getUri()) {
  221.                     continue;
  222.                 }
  223.                 $uri $page->getUri() . '/';
  224.                 $isActive str_starts_with($activeDocument->getRealFullPath(), $uri)
  225.                     || ($isLink($page) && str_starts_with($activeDocument->getFullPath(), $uri));
  226.                 if ($isActive) {
  227.                     $page->setActive(true);
  228.                     $page->setClass($page->getClass() . ' active active-trail');
  229.                 }
  230.             }
  231.         }
  232.     }
  233.     /**
  234.      * @internal
  235.      *
  236.      * @param Container $navigation navigation container to iterate
  237.      * @param string $property name of property to match against
  238.      * @param string $value value to match property against
  239.      *
  240.      * @return Page[]
  241.      */
  242.     protected function findActivePages(Container $navigationstring $propertystring $value): array
  243.     {
  244.         $filterByPrefix = new PrefixRecursiveFilterIterator($navigation$property$value);
  245.         $flatten = new \RecursiveIteratorIterator($filterByPrefix\RecursiveIteratorIterator::SELF_FIRST);
  246.         $filterMatches = new \CallbackFilterIterator($flatten, static fn (Page $page): bool => $page->get($property) === $value);
  247.         return iterator_to_array($filterMatchesfalse);
  248.     }
  249.     /**
  250.      * @internal
  251.      *
  252.      * @param Page $page
  253.      * @param bool $isActive
  254.      *
  255.      * @throws \Exception
  256.      */
  257.     protected function addActiveCssClasses(Page $page$isActive false)
  258.     {
  259.         $page->setActive(true);
  260.         $parent $page->getParent();
  261.         $isRoot false;
  262.         $classes '';
  263.         if ($parent instanceof DocumentPage) {
  264.             $this->addActiveCssClasses($parent);
  265.         } else {
  266.             $isRoot true;
  267.         }
  268.         $classes .= ' active';
  269.         if (!$isActive) {
  270.             $classes .= ' active-trail';
  271.         }
  272.         if ($isRoot && $isActive) {
  273.             $classes .= ' mainactive';
  274.         }
  275.         $page->setClass($page->getClass() . $classes);
  276.     }
  277.     /**
  278.      * @param string $pageClass
  279.      *
  280.      * @return $this
  281.      */
  282.     public function setPageClass(string $pageClass)
  283.     {
  284.         $this->pageClass $pageClass;
  285.         return $this;
  286.     }
  287.     /**
  288.      * Returns the name of the pageclass
  289.      *
  290.      * @return String
  291.      */
  292.     public function getPageClass()
  293.     {
  294.         return $this->pageClass;
  295.     }
  296.     /**
  297.      * @param Document $parentDocument
  298.      *
  299.      * @return Document[]
  300.      */
  301.     protected function getChildren(Document $parentDocument): array
  302.     {
  303.         // the intention of this function is mainly to be overridden in order to customize the behavior of the navigation
  304.         // e.g. for custom filtering and other very specific use-cases
  305.         return $parentDocument->getChildren();
  306.     }
  307.     /**
  308.      * @internal
  309.      *
  310.      * @param Document $parentDocument
  311.      * @param bool $isRoot
  312.      * @param callable $pageCallback
  313.      * @param array $parents
  314.      * @param int|null $maxDepth
  315.      *
  316.      * @return Page[]
  317.      *
  318.      * @throws \Exception
  319.      */
  320.     protected function buildNextLevel($parentDocument$isRoot false$pageCallback null$parents = [], $maxDepth null)
  321.     {
  322.         $this->currentLevel++;
  323.         $pages = [];
  324.         $childs $this->getChildren($parentDocument);
  325.         $parents[$parentDocument->getId()] = $parentDocument;
  326.         if (!is_array($childs)) {
  327.             return $pages;
  328.         }
  329.         foreach ($childs as $child) {
  330.             $classes '';
  331.             if ($child instanceof Document\Hardlink) {
  332.                 $child Document\Hardlink\Service::wrap($child);
  333.                 if (!$child) {
  334.                     continue;
  335.                 }
  336.             }
  337.             // infinite loop detection, we use array keys here, because key lookups are much faster
  338.             if (isset($parents[$child->getId()])) {
  339.                 Logger::critical('Navigation: Document with ID ' $child->getId() . ' would produce an infinite loop -> skipped, parent IDs (' implode(','array_keys($parents)) . ')');
  340.                 continue;
  341.             }
  342.             if (($child instanceof Document\Folder || $child instanceof Document\Page || $child instanceof Document\Link) && $child->getProperty('navigation_name')) {
  343.                 $path $child->getFullPath();
  344.                 if ($child instanceof Document\Link) {
  345.                     $path $child->getHref();
  346.                 }
  347.                 /** @var DocumentPage $page */
  348.                 $page = new $this->pageClass();
  349.                 if (!$child instanceof Document\Folder) {
  350.                     $page->setUri($path $child->getProperty('navigation_parameters') . $child->getProperty('navigation_anchor'));
  351.                 }
  352.                 $page->setLabel($child->getProperty('navigation_name'));
  353.                 $page->setActive(false);
  354.                 $page->setId($this->htmlMenuIdPrefix $child->getId());
  355.                 $page->setClass($child->getProperty('navigation_class'));
  356.                 $page->setTarget($child->getProperty('navigation_target'));
  357.                 $page->setTitle($child->getProperty('navigation_title'));
  358.                 $page->setAccesskey($child->getProperty('navigation_accesskey'));
  359.                 $page->setTabindex($child->getProperty('navigation_tabindex'));
  360.                 $page->setRelation($child->getProperty('navigation_relation'));
  361.                 $page->setDocument($child);
  362.                 if ($child->getProperty('navigation_exclude') || !$child->getPublished()) {
  363.                     $page->setVisible(false);
  364.                 }
  365.                 if ($isRoot) {
  366.                     $classes .= ' main';
  367.                 }
  368.                 $page->setClass($page->getClass() . $classes);
  369.                 if ($child->hasChildren() && (!$maxDepth || $maxDepth $this->currentLevel)) {
  370.                     $childPages $this->buildNextLevel($childfalse$pageCallback$parents$maxDepth);
  371.                     $page->setPages($childPages);
  372.                 }
  373.                 if ($pageCallback instanceof \Closure) {
  374.                     $pageCallback($page$child);
  375.                 }
  376.                 $this->navCacheTags[] = $page->getDocument()->getCacheTag();
  377.                 $pages[] = $page;
  378.             }
  379.         }
  380.         $this->currentLevel--;
  381.         return $pages;
  382.     }
  383. }