Varianten Storefront Darstellung pro Kategorie

Hallo zusammen

Ich beiss mir die Zähne aus mit dem folgenden Problem.

Ich habe ein Produkt mit mehreren Varianten.
Die Storefront Darstellung ist auf Hauptprodukt eingestellt.
Dies zeigt es auch korrekt im Frontend an. (1 Produkt auf der Kategorieseite)

Nun möchte ich für eine andere Kategorie die Storefront Darstellung auffächern, dass alle vorhanden Varianten dargestellt werden.

Ich versuchte dies zuerst mit ProductListingResultEvent was jedoch nicht funktioniert, da die Produkte bereits geladen wurden und nur das Hauptprodukt zurückgegeben wird. Jedoch konnte ich dort direkt mit dem $request die CategoryId abgreifen, was wiederum gut wäre.

Dann versuchte ich dies mit ProductListingCriteriaEvent, jedoch habe ich festgestellt, auch wenn ich ein grouping hinzufüge, dass dann nur eine Variante dargestellt wird, anstatt alle die existieren.

Soweit ich nachgeforscht habe, ist dies im ProductVariantListing abgespeichert auf dem Produkt ob das Hauptprodukt geladen werden sollte oder nicht.

Dies wierderum wird verwendet im ProductListingLoader (resolve previews)

Umgekehrt habe ich es auch probiert, dass ich alle Varianten darstelle, und dann via Filter ParentID = null. Jedoch hatte dies keinen Effekt.

Ich möchte dieses Verhalten einbauen für die Suchseite und spezifische Kategorie seiten dass direkt Varianten angezeigt werden.

Gibt es eine einfache Lösung (mit wenig Code) im ProductListingResultEvent, wenn alle Varianten dargestellt werden, diese zu filtern dass nur das Hauptprodukt dargestellt wird?

Oder umgekehrt, ob es möglich ist alle Varianten zu laden?

Vielen Dank für eure Antworten :slight_smile:

Ich habs entsprechend mit einem eigenen Plugin gelöst mit einem Decorator für den ProductListingLoader.
Zudem die Kategorien die ich anzeigen möchte Hardcoded.
Suche und Suggest (Suchvorschlag) zeigen nun auch die Varianten statt Hauptartikel an.

Wahrscheinlich nicht die sauberste Lösung aber es funktioniert :slight_smile:

Hallo @ZeiW,

ich suche bereits seit Monaten ein Plugin, welches das Problem löst, dass Du beschreibst…

Ich habe ebenfalls das Problem, dass ich Variantenprodukte in unterschiedlichen Kategorien unterschiedlich anzeigen lassen möchte.
Zum besseren Verständnis ein Beispiel (nicht mein echter UseCase):
Ich möchte in einer Kategorie alle Pullover anzeigen lassen, also nur das Hauptprodukt, da es die Anzeige zu unübersichtlich machen würde, wenn ich jede Farbvariante als einzelne Kachel anzeigen lassen würde.
In einer weiteren Kategorie möchte ich nun alle roten Pullover anzeigen lassen.

Nach intensiven Recherchen habe ich jedoch keine Lösung gefunden, wie ich Varianten anzeigen lassen kann, ohne sie grundsätzlich in allen Kategorein aufzufächern… Natürlich kann ich nach der Farbe rot filtern, erhalte dann im Zweifel aber nicht das Produktbild mit dem roten Pullover…

In Shopware 5 war das noch problemlos möglich.
Ich bin leider kein Entwickler und habe mich erst rudimentär in TWIG eingearbeitet…

Würdest Du eventuell Deine Lösung bzw. Dein Plugin teilen?
Oder kennt jemand ein Plugin, dass für ein Produkt eine Ansicht als Hauptprodukt als auch in aufgefächerte Varianten erlaubt?

Vielen Dank schon mal und viele Grüße!

Ich hab leider kein einzelnes Plugin dazu geschrieben (zu faul), und hab ein Plugin für die ganze Seite von mir gemacht, welches 30-40 Fixes beinhaltet.

Evt mache ich mal ein Plugin dazu, das ich das nicht immer hardcoden muss.

Ich kann dir den Quellcode zur Verfügung stellen, was ich ändern würde:

  1. ist eine CustomField in den Categories hinzufügen
  2. das custom field beinhaltet ob alle Varianten angezeigt werden sollten, oder evt wie du schreibst eine Auswahl von Specifications, welche direkt im Criteria als Filter hinzugefügt werden.
<?php // src/Decorator/ProductListingLoaderDecorator.php

namespace TwoMoons\Decorator;

use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingLoader;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Psr\Log\LoggerInterface;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\HttpFoundation\RequestStack;


#[AsDecorator('inventory')]
class ProductListingLoaderDecorator extends ProductListingLoader
{
    private $decoratedService;
    private LoggerInterface $logger;
    private readonly SalesChannelRepository $productRepository;
    private readonly SystemConfigService $systemConfigService;
    private readonly AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory;

    public function __construct(
        #[AutowireDecorated]
        ProductListingLoader $decoratedService,
        LoggerInterface $logger,
        SalesChannelRepository $productRepository,
        SystemConfigService $systemConfigService,
        AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory,
        RequestStack $requestStack
        
        )
    {
        $this->decoratedService = $decoratedService;
        $this->logger = $logger;
        $this->productRepository = $productRepository;

        $this->systemConfigService = $systemConfigService;
        $this->productCloseoutFilterFactory = $productCloseoutFilterFactory;
        $this->requestStack = $requestStack;
        
    }

    public function load(Criteria $criteria, SalesChannelContext $context): EntitySearchResult
    {
        // adding children for searching no stock (ausverkauft flag)
        //$criteria->addAssociation('children');
        $request = $this->requestStack->getCurrentRequest();
        if (!$request) {
            return $this->decoratedService->load($criteria, $context);
        }
        $categoryId = $request->attributes->get('navigationId');
        // Check by route name
        $routeName = $request->attributes->get('resolved-uri');
        $route_name = $request->attributes->get('_route');

        if (
            // sales
            $categoryId != '018edbc8e193762a9dcc503af432057d' && 
            // hier einfach mehr categoryIds abfragen

            $route_name != 'frontend.detail.page' && 
            $routeName != '/search' && 
            $routeName != '/suggest' && 
            $routeName != '/widgets/search/filter' && 
            $routeName != '/widgets/search') {
            return $this->decoratedService->load($criteria, $context);
        };
        if ($routeName == '/suggest') {
            $criteria->setLimit(5);
        }

        $criteria->addFilter(
            new MultiFilter(
                MultiFilter::CONNECTION_OR,
                [
                    new EqualsFilter('product.childCount', 0),
                    new EqualsFilter('product.childCount', null),
                ]
            )
        );
        $this->handleAvailableStock($criteria, $context);

        $ids = $this->productRepository->searchIds($criteria, $context);
        $aggregations = $this->productRepository->aggregate($criteria, $context);

        // used to get variation names
        $criteria->addAssociation('options.group');
        $searchResult = $this->productRepository->search($criteria, $context);

        $result = new EntitySearchResult(ProductDefinition::ENTITY_NAME, $ids->getTotal(), $searchResult->getEntities(), $aggregations, $criteria, $context->getContext());
        $result->addState(...$ids->getStates());

        return $result;

    }

    // copy of ProductListingLoader.php function
    private function handleAvailableStock(Criteria $criteria, SalesChannelContext $context): void
    {
        $salesChannelId = $context->getSalesChannel()->getId();

        $hide = $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock', $salesChannelId);

        if (!$hide) {
            return;
        }

        $closeoutFilter = $this->productCloseoutFilterFactory->create($context);
        $criteria->addFilter($closeoutFilter);
    }
}
?>

Die services.xml

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="twomoons.logger" class="Monolog\Logger">
            <factory service="Shopware\Core\Framework\Log\LoggerFactory" method="createRotating"/>
                <argument type="string">my-custom-logger</argument>
        </service>
        <service id="TwoMoons\Decorator\ProductListingLoaderDecorator" decorates="Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingLoader">
            <argument type="service" id="TwoMoons\Decorator\ProductListingLoaderDecorator.inner"/>
            <argument type="service" id="twomoons.logger" />
            <argument type="service" id="sales_channel.product.repository"/>
            <argument type="service" id="Shopware\Core\System\SystemConfig\SystemConfigService" />
            <argument type="service" id="Shopware\Core\Content\Product\SalesChannel\ProductCloseoutFilterFactory" />
            <argument type="service" id="Symfony\Component\HttpFoundation\RequestStack"/>
        </service>
    </services>
</container>