<?php declare(strict_types=1);
namespace S360\MegaMenu\Service;
use S360\MegaMenu\Core\Content\Menu\MenuCollection;
use S360\MegaMenu\Core\Content\Menu\MenuEntity;
use S360\MegaMenu\Core\Content\MenuItem\MenuItemCollection;
use S360\MegaMenu\Core\Content\MenuItem\MenuItemEntity;
use S360\MegaMenu\Struct\Tree;
use S360\MegaMenu\Struct\TreeItem;
use Shopware\Core\Content\Category\CategoryEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\HttpFoundation\Request;
/**
* Loader for MegaMenu.
* @package S360\MegaMenu\Service
*/
class MenuLoader
{
public const CONTENT_TYPES_WITH_PSEUDO_CHILDREN = [
'custom', 'categories'
];
public const ROOT_ID_QUERY_PARAM = 's360MegamenuItemId';
public const ACTIVE_PROPERTY_ITEM_ID = 'id';
public const ACTIVE_PROPERTY_CATEGORY_ID = 'categoryLinkId';
/**
* @var EntityRepositoryInterface
*/
private $menuRepository;
/**
* @var TreeItem
*/
private $treeItem;
/**
* @param EntityRepositoryInterface $menuRepository
*/
public function __construct(EntityRepositoryInterface $menuRepository)
{
$this->menuRepository = $menuRepository;
$this->treeItem = new TreeItem(null, []);
}
public function load(
SalesChannelContext $context,
Request $request,
?string $activeId,
string $activeProperty
): MenuCollection {
$criteria = new Criteria();
$criteria
->addAssociation('items.categoryLink.children.children.children.media')
->addAssociation('items.categoryLink.children.children.media')
->addAssociation('items.categoryLink.children.media')
->addAssociation('items.categoryLink.media')
->addAssociation('items.customIcon')
->addFilter(new OrFilter([
new EqualsFilter('salesChannelId', $context->getSalesChannel()->getId()),
new EqualsFilter('salesChannelId', null),
]));
// Only load active menues if not in preview mode.
if (empty($request->get('menu_preview'))) {
$criteria->addFilter(new EqualsFilter('isActive', 1));
}
// Load Preview Menu
if (!empty($request->get('menu_preview'))) {
$criteria->addFilter(new EqualsFilter('id', $request->get('menu_preview')));
}
$root = $request->query->get(self::ROOT_ID_QUERY_PARAM, null);
/** @var MenuCollection $collection */
$collection = $this->menuRepository->search($criteria, $context->getContext())->getEntities();
foreach ($collection->getIterator() as $menu) {
// Remove inactive menue if it is not currently previewed.
if ($request->get('menu_preview') != $menu->getId() && !$menu->getIsActive()) {
$collection->remove($menu->getId());
}
// Build Tree
$tree = $this->getTree($root, $menu->getItems() ?? new MenuItemCollection(), $activeId, $activeProperty);
$menu->setTree($tree);
// Check if active menu has pseudo children (ie a dropdown but no direct children of type MenuItem)
$active = $tree->getActive();
if ($active && in_array($active->getContentType(), self::CONTENT_TYPES_WITH_PSEUDO_CHILDREN)) {
$menu->getTree()->setPseudoChildrenParent(new TreeItem($menu->getTree()->getActive(), []));
}
// Checks if we have a active category or child of a menu item
// If so set its parent (only necessary for mobile menu)
if (!$active && $menu->getType() == MenuCollection::MOBILE_MENU_TYPE) {
$this->fetchPseudoChildParent($menu, $activeId);
}
}
return $collection;
}
/**
* Get a deeply nested category by its id
*
* @param CategoryEntity|null $category
* @param string $key
* @return CategoryEntity|null
*/
public function getDeepCategory(?CategoryEntity $category, string $key): ?CategoryEntity
{
if ($category) {
if ($category->getId() == $key) {
return $category;
}
if ($category->getChildCount() > 0 && $category->getChildren()) {
foreach ($category->getChildren() as $child) {
$childCat = $this->getDeepCategory($child, $key);
if ($childCat) {
return $childCat;
}
}
}
}
return null;
}
/**
* Load the active category entity.
*
* @param Tree|null $tree
* @param string $navigationId
* @return CategoryEntity|null
*/
public function loadActiveCategory(?Tree $tree, string $navigationId): ?CategoryEntity
{
if (empty($tree) || !empty($tree->getActive()) || empty($tree->getPseudoChildrenParent())) {
return null;
}
$activeCat = null;
$item = $tree->getPseudoChildrenParent()->getItem()->getCategoryLink();
if (empty($item)) {
return null;
}
if ($item->getId() == $navigationId) {
$activeCat = $item;
}
if ($item->getChildren()) {
foreach ($item->getChildren() as $child) {
$activeCat = $this->getDeepCategory($child, $navigationId);
if ($activeCat) {
break;
}
}
}
return $activeCat;
}
/**
* Sort Category
*
* @param CategoryEntity|null $active
* @return void
*/
public function sortCategories(?CategoryEntity $category): void
{
if ($category && $category->getChildren()) {
$category->getChildren()->sortByPosition();
// Sort Children of Child
foreach ($category->getChildren() as $child) {
$this->sortCategories($child);
}
}
}
/**
* Fetch a pseudo child parent of a category child
*
* @param MenuEntity $menu
* @param string $activeId
* @return void
*/
private function fetchPseudoChildParent(MenuEntity $menu, string $activeId): void
{
$items = $menu->getItems()->filter(function ($item) use ($activeId) {
/** @var MenuItemEntity $item */
if (!in_array($item->getContentType(), self::CONTENT_TYPES_WITH_PSEUDO_CHILDREN)) {
return false;
}
if ($item->getCategoryLinkId() == $activeId) {
return true;
}
if ($item->getCategoryLink() && $item->getCategoryLink()->getChildCount()) {
foreach ($item->getCategoryLink()->getChildren() as $child) {
$activeCat = $this->getDeepCategory($child, $activeId);
if ($activeCat) {
return true;
}
}
}
});
$item = $items->first();
if ($item && in_array($item->getContentType(), self::CONTENT_TYPES_WITH_PSEUDO_CHILDREN)) {
$menu->getTree()->setTree([]);
// $menu->getTree()->setActive($item);
$menu->getTree()->setPseudoChildrenParent(new TreeItem($item, []));
}
}
/**
* @param string|null $parentId
* @param MenuItemCollection $items
* @param string|null $activeId
* @return Tree
*/
private function getTree(?string $parentId, MenuItemCollection $items, ?string $activeId, $activeProperty): Tree
{
$active = null;
if ($activeProperty === self::ACTIVE_PROPERTY_ITEM_ID) {
$active = $items->get($activeId);
}
return new Tree(
$active ?? $items->getFirstBy($activeProperty, $activeId),
$this->buildTree($parentId, $items->getElements())
);
}
/**
* @param string|null $parentId
* @param MenuItemEntity[] $items
* @return TreeItem[]
*/
private function buildTree(?string $parentId, array $items): array
{
$children = new MenuItemCollection();
foreach ($items as $key => $item) {
if ($item->getParentId() !== $parentId) {
continue;
}
unset($items[$key]);
$children->add($item);
}
$children->sortByPosition();
$tree = [];
foreach ($children->getIterator() as $child) {
if (!$child->getActive()) {
continue;
}
$this->sortCategories($child->getCategoryLink());
$item = clone $this->treeItem;
$item->setItem($child);
$item->setChildren(
$this->buildTree($child->getId(), $items)
);
$tree[$child->getId()] = $item;
}
return $tree;
}
}