<?php
declare(strict_types=1);
namespace CioItpThemeFilters\Subscriber;
use Shopware\Core\Content\Category\Event\NavigationLoadedEvent;
use Shopware\Core\Content\Category\Tree\Tree;
use Shopware\Core\Content\Category\CategoryEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\Context;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class NavigationFilterSubscriber implements EventSubscriberInterface
{
private RequestStack $requestStack;
private EntityRepositoryInterface $productRepository;
private EntityRepositoryInterface $cmsPageRepository;
private EntityRepositoryInterface $categoryRepository;
/** @var array<string, string|null> */
private array $cmsPageTypeCache = [];
public function __construct(
RequestStack $requestStack,
EntityRepositoryInterface $productRepository,
EntityRepositoryInterface $cmsPageRepository,
EntityRepositoryInterface $categoryRepository
) {
$this->requestStack = $requestStack;
$this->productRepository = $productRepository;
$this->cmsPageRepository = $cmsPageRepository;
$this->categoryRepository = $categoryRepository;
}
public static function getSubscribedEvents(): array
{
return [
NavigationLoadedEvent::class => 'onNavigationLoaded'
];
}
public function onNavigationLoaded(NavigationLoadedEvent $event): void
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
return;
}
// Hole die Property-ID(s) aus den Query-Parametern (einzeln, Array oder "a|b|c")
$propertiesParam = $request->query->get('properties');
$propertyIds = [];
if (is_array($propertiesParam)) {
$propertyIds = array_values(array_filter($propertiesParam, static fn ($v) => (string)$v !== ''));
} elseif (is_string($propertiesParam) && $propertiesParam !== '') {
// Unterstützt sowohl Pipe- als auch Komma-separierte Werte
$normalized = str_replace(',', '|', $propertiesParam);
$propertyIds = array_filter(explode('|', $normalized), static fn ($v) => (string)$v !== '');
}
$propertyIds = array_values(array_unique($propertyIds));
if (empty($propertyIds)) {
return;
}
// Hole die Navigation
$navigation = $event->getNavigation();
// Filtere die Navigation
$this->filterNavigation($navigation, $propertyIds, $event->getContext());
}
private function filterNavigation(Tree $tree, array $propertyIds, Context $context): void
{
$items = $tree->getTree();
$filteredItems = [];
foreach ($items as $key => $item) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('categoryTree', $item->getCategory()->getId()));
if (\count($propertyIds) > 1) {
$criteria->addFilter(new EqualsAnyFilter('properties.id', $propertyIds));
} else {
$criteria->addFilter(new EqualsFilter('properties.id', $propertyIds[0]));
}
$productCount = $this->productRepository->search($criteria, $context)->getTotal();
$keepAlways = $this->shouldKeepCategory($item->getCategory(), $context);
// Kinder immer prüfen, damit Folder mit gültigen (z. B. CMS-)Kindern sichtbar bleiben
$children = [];
if ($item->getChildren()) {
$children = $this->filterTreeItems($item->getChildren(), $propertyIds, $context);
if (!empty($children)) {
$item->setChildren($children);
}
}
if (!empty($children) || $productCount || $keepAlways) {
$filteredItems[] = $item;
}
}
$tree->setTree($filteredItems);
}
private function filterTreeItems(array $items, array $propertyIds, Context $context): array
{
$filteredItems = [];
foreach ($items as $key => $item) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('categoryTree', $item->getCategory()->getId()));
if (\count($propertyIds) > 1) {
$criteria->addFilter(new EqualsAnyFilter('properties.id', $propertyIds));
} else {
$criteria->addFilter(new EqualsFilter('properties.id', $propertyIds[0]));
}
$productCount = $this->productRepository->search($criteria, $context)->getTotal();
$keepAlways = $this->shouldKeepCategory($item->getCategory(), $context);
// Kinder immer prüfen, um Ordner mit gültigen Nachkommen zu behalten
$children = [];
if ($item->getChildren()) {
$children = $this->filterTreeItems($item->getChildren(), $propertyIds, $context);
if (!empty($children)) {
$item->setChildren($children);
}
}
if (!empty($children) || $productCount || $keepAlways) {
$filteredItems[] = $item;
}
}
return $filteredItems;
}
private function shouldKeepCategory(CategoryEntity $category, Context $context): bool
{
$type = $category->getType();
$external = method_exists($category, 'getExternalLink') ? (string) $category->getExternalLink() : '';
$hasCmsRef = (string) ($category->getCmsPageId() ?? '') !== '' || (bool) $category->getCmsPage();
// Wenn wesentliche Infos fehlen, einmal aus der DB nachladen
if (($type === '') || (!$hasCmsRef && $external === '')) {
try {
$criteria = new Criteria([$category->getId()]);
$criteria->addAssociation('cmsPage');
$loaded = $this->categoryRepository->search($criteria, $context)->first();
if ($loaded) {
$category = $loaded; // lokal für die Berechnung ersetzen
$type = $loaded->getType();
$external = method_exists($loaded, 'getExternalLink') ? (string) $loaded->getExternalLink() : $external;
$hasCmsRef = (string) ($loaded->getCmsPageId() ?? '') !== '' || (bool) $loaded->getCmsPage();
}
} catch (\Throwable $e) {
// defensiv: Ignorieren und mit bisherigen Daten weiterarbeiten
}
}
// Externe Links immer behalten
if ($external !== '') {
return true;
}
if ($type === 'link') {
return true;
}
if ($type === 'page') {
// Alle Nicht-Listing CMS-Seiten behalten (z. B. Landingpage, Formulare, Content)
$pageType = null;
$cmsPage = $category->getCmsPage();
if ($cmsPage && method_exists($cmsPage, 'getType')) {
$pageType = $cmsPage->getType();
} else {
$cmsPageId = $category->getCmsPageId();
if ($cmsPageId) {
$pageType = $this->getCmsPageType($cmsPageId, $context);
}
}
if ($pageType !== null) {
return $pageType !== 'product_list';
}
// Fallback: Wenn eine CMS-Seite referenziert ist, aber der Typ nicht ermittelt werden konnte,
// Kategorie trotzdem behalten (betrifft u. a. Form-/FAQ-Seiten).
if ($category->getCmsPageId()) {
return true;
}
}
return false;
}
private function getCmsPageType(string $cmsPageId, Context $context): ?string
{
if (isset($this->cmsPageTypeCache[$cmsPageId])) {
return $this->cmsPageTypeCache[$cmsPageId];
}
$criteria = new Criteria([$cmsPageId]);
$criteria->setLimit(1);
$page = $this->cmsPageRepository->search($criteria, $context)->first();
$type = $page ? $page->getType() : null;
$this->cmsPageTypeCache[$cmsPageId] = $type;
return $type;
}
}