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

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. /**
  24.  * @internal
  25.  */
  26. class Builder
  27. {
  28.     /**
  29.      * @var RequestHelper
  30.      */
  31.     private $requestHelper;
  32.     /**
  33.      * @var string
  34.      */
  35.     protected $htmlMenuIdPrefix;
  36.     /**
  37.      * @var string
  38.      */
  39.     protected $pageClass DocumentPage::class;
  40.     /**
  41.      * @var int
  42.      */
  43.     private $currentLevel 0;
  44.     /**
  45.      * @var array
  46.      */
  47.     private $navCacheTags = [];
  48.     /**
  49.      * @param RequestHelper $requestHelper
  50.      * @param string|null $pageClass
  51.      */
  52.     public function __construct(RequestHelper $requestHelperstring $pageClass null)
  53.     {
  54.         $this->requestHelper $requestHelper;
  55.         if (null !== $pageClass) {
  56.             $this->pageClass $pageClass;
  57.         }
  58.     }
  59.     /**
  60.      * @param Document|null $activeDocument
  61.      * @param Document|null $navigationRootDocument
  62.      * @param string|null $htmlMenuIdPrefix
  63.      * @param \Closure|null $pageCallback
  64.      * @param bool|string $cache
  65.      * @param int|null $maxDepth
  66.      * @param int|null $cacheLifetime
  67.      *
  68.      * @return mixed|\Pimcore\Navigation\Container
  69.      *
  70.      * @throws \Exception
  71.      */
  72.     public function getNavigation($activeDocument null$navigationRootDocument null$htmlMenuIdPrefix null$pageCallback null$cache true, ?int $maxDepth null, ?int $cacheLifetime null)
  73.     {
  74.         $cacheEnabled $cache !== false;
  75.         $this->htmlMenuIdPrefix $htmlMenuIdPrefix;
  76.         if (!$navigationRootDocument) {
  77.             $navigationRootDocument Document::getById(1);
  78.         }
  79.         // the cache key consists out of the ID and the class name (eg. for hardlinks) of the root document and the optional html prefix
  80.         $cacheKeys = ['root_id__' $navigationRootDocument->getId(), $htmlMenuIdPrefixget_class($navigationRootDocument)];
  81.         if (Site::isSiteRequest()) {
  82.             $site Site::getCurrentSite();
  83.             $cacheKeys[] = 'site__' $site->getId();
  84.         }
  85.         if (is_string($cache)) {
  86.             $cacheKeys[] = 'custom__' $cache;
  87.         }
  88.         if ($pageCallback instanceof \Closure) {
  89.             $cacheKeys[] = 'pageCallback_' closureHash($pageCallback);
  90.         }
  91.         if ($maxDepth) {
  92.             $cacheKeys[] = 'maxDepth_' $maxDepth;
  93.         }
  94.         $cacheKey 'nav_' md5(serialize($cacheKeys));
  95.         $navigation CacheManager::load($cacheKey);
  96.         if (!$navigation || !$cacheEnabled) {
  97.             $navigation = new \Pimcore\Navigation\Container();
  98.             $this->navCacheTags = ['output''navigation'];
  99.             if ($navigationRootDocument->hasChildren()) {
  100.                 $this->currentLevel 0;
  101.                 $rootPage $this->buildNextLevel($navigationRootDocumenttrue$pageCallback, [], $maxDepth);
  102.                 $navigation->addPages($rootPage);
  103.             }
  104.             // we need to force caching here, otherwise the active classes and other settings will be set and later
  105.             // also written into cache (pass-by-reference) ... when serializing the data directly here, we don't have this problem
  106.             if ($cacheEnabled) {
  107.                 CacheManager::save($navigation$cacheKey$this->navCacheTags$cacheLifetime999true);
  108.             }
  109.         }
  110.         // set active path
  111.         $activePages = [];
  112.         if ($this->requestHelper->hasMainRequest()) {
  113.             $request $this->requestHelper->getMainRequest();
  114.             // try to find a page matching exactly the request uri
  115.             $activePages $this->findActivePages($navigation'uri'$request->getRequestUri());
  116.             if (empty($activePages)) {
  117.                 // try to find a page matching the path info
  118.                 $activePages $this->findActivePages($navigation'uri'$request->getPathInfo());
  119.             }
  120.         }
  121.         if ($activeDocument instanceof Document) {
  122.             if (empty($activePages)) {
  123.                 // use the provided pimcore document
  124.                 $activePages $this->findActivePages($navigation'realFullPath'$activeDocument->getRealFullPath());
  125.             }
  126.             if (empty($activePages)) {
  127.                 // find by link target
  128.                 $activePages $this->findActivePages($navigation'uri'$activeDocument->getFullPath());
  129.             }
  130.         }
  131.         // cleanup active pages from links
  132.         // pages have priority, if we don't find any active page, we use all we found
  133.         $tmpPages = [];
  134.         foreach ($activePages as $page) {
  135.             if ($page instanceof DocumentPage && $page->getDocumentType() !== 'link') {
  136.                 $tmpPages[] = $page;
  137.             }
  138.         }
  139.         if (count($tmpPages)) {
  140.             $activePages $tmpPages;
  141.         }
  142.         if (!empty($activePages)) {
  143.             // we found an active document, so we can build the active trail by getting respectively the parent
  144.             foreach ($activePages as $activePage) {
  145.                 $this->addActiveCssClasses($activePagetrue);
  146.             }
  147.         } elseif ($activeDocument instanceof Document) {
  148.             // we didn't find the active document, so we try to build the trail on our own
  149.             $allPages = new \RecursiveIteratorIterator($navigation\RecursiveIteratorIterator::SELF_FIRST);
  150.             foreach ($allPages as $page) {
  151.                 $activeTrail false;
  152.                 if ($page instanceof Url && $page->getUri()) {
  153.                     if (str_starts_with($activeDocument->getRealFullPath(), $page->getUri() . '/')) {
  154.                         $activeTrail true;
  155.                     } elseif (
  156.                         $page instanceof DocumentPage &&
  157.                         $page->getDocumentType() === 'link' &&
  158.                         str_starts_with($activeDocument->getFullPath(), $page->getUri() . '/')
  159.                     ) {
  160.                         $activeTrail true;
  161.                     }
  162.                 }
  163.                 if ($activeTrail) {
  164.                     $page->setActive(true);
  165.                     $page->setClass($page->getClass() . ' active active-trail');
  166.                 }
  167.             }
  168.         }
  169.         return $navigation;
  170.     }
  171.     /**
  172.      * @param Container $navigation navigation container to iterate
  173.      * @param string $property name of property to match against
  174.      * @param string $value value to match property against
  175.      *
  176.      * @return Page[]
  177.      */
  178.     protected function findActivePages(Container $navigationstring $propertystring $value): array
  179.     {
  180.         $filterByPrefix = new PrefixRecursiveFilterIterator($navigation$property$value);
  181.         $flatten = new \RecursiveIteratorIterator($filterByPrefix\RecursiveIteratorIterator::SELF_FIRST);
  182.         $filterMatches = new \CallbackFilterIterator($flatten, static fn (Page $page): bool => $page->get($property) === $value);
  183.         return iterator_to_array($filterMatchesfalse);
  184.     }
  185.     /**
  186.      * @param Page $page
  187.      * @param bool $isActive
  188.      *
  189.      * @throws \Exception
  190.      */
  191.     protected function addActiveCssClasses(Page $page$isActive false)
  192.     {
  193.         $page->setActive(true);
  194.         $parent $page->getParent();
  195.         $isRoot false;
  196.         $classes '';
  197.         if ($parent instanceof DocumentPage) {
  198.             $this->addActiveCssClasses($parent);
  199.         } else {
  200.             $isRoot true;
  201.         }
  202.         $classes .= ' active';
  203.         if (!$isActive) {
  204.             $classes .= ' active-trail';
  205.         }
  206.         if ($isRoot && $isActive) {
  207.             $classes .= ' mainactive';
  208.         }
  209.         $page->setClass($page->getClass() . $classes);
  210.     }
  211.     /**
  212.      * @param string $pageClass
  213.      *
  214.      * @return $this
  215.      */
  216.     public function setPageClass(string $pageClass)
  217.     {
  218.         $this->pageClass $pageClass;
  219.         return $this;
  220.     }
  221.     /**
  222.      * Returns the name of the pageclass
  223.      *
  224.      * @return String
  225.      */
  226.     public function getPageClass()
  227.     {
  228.         return $this->pageClass;
  229.     }
  230.     /**
  231.      * @param Document $parentDocument
  232.      *
  233.      * @return Document[]
  234.      */
  235.     protected function getChildren(Document $parentDocument): array
  236.     {
  237.         // the intention of this function is mainly to be overridden in order to customize the behavior of the navigation
  238.         // e.g. for custom filtering and other very specific use-cases
  239.         return $parentDocument->getChildren();
  240.     }
  241.     /**
  242.      * @param Document $parentDocument
  243.      * @param bool $isRoot
  244.      * @param callable $pageCallback
  245.      * @param array $parents
  246.      * @param int|null $maxDepth
  247.      *
  248.      * @return Page[]
  249.      *
  250.      * @throws \Exception
  251.      */
  252.     protected function buildNextLevel($parentDocument$isRoot false$pageCallback null$parents = [], $maxDepth null)
  253.     {
  254.         $this->currentLevel++;
  255.         $pages = [];
  256.         $childs $this->getChildren($parentDocument);
  257.         $parents[$parentDocument->getId()] = $parentDocument;
  258.         if (!is_array($childs)) {
  259.             return $pages;
  260.         }
  261.         foreach ($childs as $child) {
  262.             $classes '';
  263.             if ($child instanceof Document\Hardlink) {
  264.                 $child Document\Hardlink\Service::wrap($child);
  265.                 if (!$child) {
  266.                     continue;
  267.                 }
  268.             }
  269.             // infinite loop detection, we use array keys here, because key lookups are much faster
  270.             if (isset($parents[$child->getId()])) {
  271.                 Logger::critical('Navigation: Document with ID ' $child->getId() . ' would produce an infinite loop -> skipped, parent IDs (' implode(','array_keys($parents)) . ')');
  272.                 continue;
  273.             }
  274.             if (($child instanceof Document\Folder || $child instanceof Document\Page || $child instanceof Document\Link) && $child->getProperty('navigation_name')) {
  275.                 $path $child->getFullPath();
  276.                 if ($child instanceof Document\Link) {
  277.                     $path $child->getHref();
  278.                 }
  279.                 /** @var DocumentPage $page */
  280.                 $page = new $this->pageClass();
  281.                 if (!$child instanceof Document\Folder) {
  282.                     $page->setUri($path $child->getProperty('navigation_parameters') . $child->getProperty('navigation_anchor'));
  283.                 }
  284.                 $page->setLabel($child->getProperty('navigation_name'));
  285.                 $page->setActive(false);
  286.                 $page->setId($this->htmlMenuIdPrefix $child->getId());
  287.                 $page->setClass($child->getProperty('navigation_class'));
  288.                 $page->setTarget($child->getProperty('navigation_target'));
  289.                 $page->setTitle($child->getProperty('navigation_title'));
  290.                 $page->setAccesskey($child->getProperty('navigation_accesskey'));
  291.                 $page->setTabindex($child->getProperty('navigation_tabindex'));
  292.                 $page->setRelation($child->getProperty('navigation_relation'));
  293.                 $page->setDocument($child);
  294.                 if ($child->getProperty('navigation_exclude') || !$child->getPublished()) {
  295.                     $page->setVisible(false);
  296.                 }
  297.                 if ($isRoot) {
  298.                     $classes .= ' main';
  299.                 }
  300.                 $page->setClass($page->getClass() . $classes);
  301.                 if ($child->hasChildren() && (!$maxDepth || $maxDepth $this->currentLevel)) {
  302.                     $childPages $this->buildNextLevel($childfalse$pageCallback$parents$maxDepth);
  303.                     $page->setPages($childPages);
  304.                 }
  305.                 if ($pageCallback instanceof \Closure) {
  306.                     $pageCallback($page$child);
  307.                 }
  308.                 $this->navCacheTags[] = $page->getDocument()->getCacheTag();
  309.                 $pages[] = $page;
  310.             }
  311.         }
  312.         $this->currentLevel--;
  313.         return $pages;
  314.     }
  315. }