custom/plugins/DreiwmBrandstetterPlugin/src/Subscriber/BrandstetterSubscriber.php line 245

Open in your IDE?
  1. <?php
  2. namespace DreiwmBrandstetterPlugin\Subscriber;
  3. use DateMalformedStringException;
  4. use DateTime;
  5. use DreiwmBrandstetterPlugin\Core\Checkout\Cart\Custom\Error\CustomerTooLateForPackstationError;
  6. use DreiwmBrandstetterPlugin\DreiwmBrandstetterPlugin;
  7. use DreiwmBrandstetterPlugin\Service\DateValidator;
  8. use DreiwmBrandstetterPlugin\Service\PackingStationService;
  9. use Psr\Log\LoggerInterface;
  10. use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
  11. use Shopware\Core\Content\Category\CategoryEvents;
  12. use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
  13. use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
  14. use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
  15. use Shopware\Core\Content\Product\ProductEntity;
  16. use Shopware\Core\Content\Product\SalesChannel\ProductListResponse;
  17. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductCollection;
  18. use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
  19. use Shopware\Core\Framework\Context;
  20. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  29. use Shopware\Core\Framework\Log\LoggerFactory;
  30. use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
  31. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  32. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  33. use Shopware\Storefront\Framework\Routing\RequestTransformer;
  34. use Shopware\Storefront\Framework\Routing\StorefrontResponse;
  35. use Shopware\Storefront\Page\GenericPageLoadedEvent;
  36. use Shopware\Storefront\Page\Navigation\NavigationPage;
  37. use Shopware\Storefront\Page\Navigation\NavigationPageLoadedEvent;
  38. use Shopware\Storefront\Page\Product\Configurator\ProductCombinationFinder;
  39. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  40. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  41. use Symfony\Component\HttpFoundation\RedirectResponse;
  42. use Symfony\Component\HttpFoundation\RequestStack;
  43. use Symfony\Component\HttpKernel\Event\RequestEvent;
  44. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  45. use Symfony\Component\HttpKernel\KernelEvents;
  46. use Symfony\Component\VarDumper\VarDumper;
  47. class BrandstetterSubscriber implements EventSubscriberInterface
  48. {
  49.     private RequestStack $requestStack;
  50.     private SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler;
  51.     private EntityRepository $freeLockerRepository;
  52.     /**
  53.      * @deprecated tag:v6.5.0 - will be removed
  54.      */
  55.     private ProductCombinationFinder $productCombinationFinder;
  56.     private $salesChannelContextFactory;
  57.     private PackingStationService $packingStationService;
  58.     private DateValidator $dateValidator;
  59.     private LoggerInterface $logger;
  60.     private EntityRepository $productRepository;
  61.     public function __construct(
  62.         RequestStack $requestStack,
  63.         ProductCombinationFinder $productCombinationFinder,
  64.         SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler,
  65.         AbstractSalesChannelContextFactory $salesChannelContextFactory,
  66.         PackingStationService $packingStationService,
  67.         $freeLockerRepository,
  68.         DateValidator $dateValidator,
  69.         EntityRepository $productRepository,
  70.         LoggerFactory $loggerFactory
  71.     ) {
  72.         $this->requestStack $requestStack;
  73.         $this->productCombinationFinder $productCombinationFinder;
  74.         $this->seoUrlPlaceholderHandler $seoUrlPlaceholderHandler;
  75.         $this->salesChannelContextFactory $salesChannelContextFactory;
  76.         $this->packingStationService $packingStationService;
  77.         $this->freeLockerRepository $freeLockerRepository;
  78.         $this->dateValidator $dateValidator;
  79.         $this->productRepository $productRepository;
  80.         $this->logger $loggerFactory->createRotating('dreiwm_brandstetter_subscriber'7);
  81.     }
  82.     public static function getSubscribedEvents(): array
  83.     {
  84.         return [
  85.             KernelEvents::REQUEST => 'setVariantIdToDisplayFilter',
  86.             KernelEvents::RESPONSE => 'setVariantIdToDisplay',
  87.             AfterLineItemAddedEvent::class => 'addPaketinLocker',
  88.             ProductListingCriteriaEvent::class => ['productListingResult'500],
  89.         ];
  90.     }
  91.     /**
  92.      * Filtere die Produkte nach Verfügbarkeit
  93.      * @param ProductListingCriteriaEvent $event
  94.      * @return void
  95.      * @throws DateMalformedStringException
  96.      */
  97.     public function productListingResult(ProductListingCriteriaEvent $event): void
  98.     {
  99. //        return;
  100.         $this->frontendProductAssociation($event->getCriteria());
  101.         // reload the page
  102.         // --- 1. Wochentag aus dem aktuellen Datum ermitteln ---
  103.         // hole das aktuelle Datum aus dem Cookie
  104.         $currentDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  105.         // hole die ausgewählte Versandart aus dem Cookie
  106.         $customerSelectedDeliveryId $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDeliveryId');
  107.         // Wenn Post ausgewählt ist, dann setze das gewählte Datum auf heute
  108.         if ($customerSelectedDeliveryId == DreiwmBrandstetterPlugin::POST_ID) {
  109. //            return;
  110.             $currentDate = (new DateTime())->format('Y-m-d');
  111.         }
  112.         // wenn kein aktuelles Datum gesetzt ist und die Versandart nicht Post ist, dann breche ab
  113.         if ($currentDate == null && $customerSelectedDeliveryId !== DreiwmBrandstetterPlugin::POST_ID) {
  114.             return;
  115.         }
  116.         // Datum in DateTime umwandeln
  117.         $currentDate = (new DateTime($currentDate))->format('Y-m-d');
  118.         // Filtere Produkte mit Lagerbestand
  119.         $event->getCriteria()->addFilter(new EqualsFilter('product.available'1));
  120.         // Erstelle ein DateTime-Objekt aus dem aktuellen Datum
  121.         $dt = new DateTime($currentDate);
  122.         // Ermittle den englischen Wochentag, z. B. "Mon", "Tue", etc.
  123.         $englishDay $dt->format('D');
  124.         // Mappen des englischen Wochentags auf das deutsche Kürzel
  125.         $dayMap = [
  126.             'Mon' => 'Mo',
  127.             'Tue' => 'Di',
  128.             'Wed' => 'Mi',
  129.             'Thu' => 'Do',
  130.             'Fri' => 'Fr',
  131.             'Sat' => 'Sa',
  132.             'Sun' => 'So'
  133.         ];
  134.         $desiredDay $dayMap[$englishDay] ?? null;
  135.         // --- 2. Gemeinsamer Datum-Filter ("common date filter") ---
  136.         // Dieser Filter deckt die üblichen Fälle ab, wie z.B.:
  137.         // - Produkte, die sowohl 'product_available_from' als auch 'product_available_until' gesetzt haben und in den Zeitraum fallen,
  138.         // - Produkte, bei denen nur eines der Felder gesetzt ist,
  139.         // - Produkte ohne beide Felder (immer verfügbar).
  140.         $commonDateFilter = new OrFilter([
  141.             // Fall 1: Beide Felder vorhanden und aktuelles Datum liegt dazwischen
  142.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  143.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  144.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  145.             ]),
  146.             // Fall 2: Nur 'product_available_from' vorhanden
  147.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  148.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  149.                 new EqualsFilter('customFields.product_available_until'null),
  150.             ]),
  151.             // Fall 3: Nur 'product_available_until' vorhanden
  152.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  153.                 new EqualsFilter('customFields.product_available_from'null),
  154.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  155.             ]),
  156.             // Fall 4: Keine Datumsfelder gesetzt
  157.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  158.                 new EqualsFilter('customFields.product_available_from'null),
  159.                 new EqualsFilter('customFields.product_available_until'null),
  160.             ]),
  161.         ]);
  162.         // --- 3. Filter für Produkte ohne baking_days (Gruppe A) ---
  163.         // Diese Produkte dürfen kein baking_days-Feld gesetzt haben und müssen nur den Datumskriterien genügen.
  164.         $filterWithoutBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  165.             new EqualsFilter('customFields.baking_days'null),
  166.             $commonDateFilter,
  167.         ]);
  168.         // 4. Gruppe B (mit baking_days)
  169.         $filterWithBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  170.             // baking_days gesetzt
  171.             new NotFilter(NotFilter::CONNECTION_AND, [
  172.                 new EqualsFilter('customFields.baking_days'null),
  173.             ]),
  174.             // enthält gewähltes Kürzel? (z. B. "Wed")
  175.             new ContainsFilter('customFields.baking_days'$desiredDay),
  176.             // Datumskriterien
  177.             $commonDateFilter,
  178.         ]);
  179.         // 5. Final immer beide Gruppen zusammenführen:
  180.         $finalFilter = new OrFilter([
  181.             $filterWithoutBakingDays,
  182.             $filterWithBakingDays,
  183.         ]);
  184.         $event->getCriteria()->addFilter($finalFilter);
  185.     }
  186.     private function loadProductCustomField(ProductListingCriteriaEvent $eventstring $productNumber): array
  187.     {
  188.         // ganz simple EINE Abfrage nur für Debug:
  189.         $criteria = new Criteria();
  190.         $criteria->addFilter(new EqualsFilter('product.productNumber'$productNumber));
  191.         $criteria->addAssociation('customFields');
  192.         $product $this->productRepository->search($criteria$event->getContext())->first();
  193.         return $product $product->getCustomFields() : [];
  194.     }
  195.     /**
  196.      * Füge die Association für die Frontend-Produktanzeige hinzu
  197.      * @param $criteria
  198.      */
  199.     private function frontendProductAssociation($criteria): void
  200.     {
  201.         $criteria->addAssociation('properties');
  202.         $criteria->addAssociation('properties.group');
  203.     }
  204.     /**
  205.      * Erstelle einen SalesChannelContext
  206.      * @param string $salesChannelId
  207.      * @param string $languageId
  208.      * @return SalesChannelContext $salesChannelContext
  209.      */
  210.     public function createSalesChannelContext(string $salesChannelIdstring $languageId): SalesChannelContext
  211.     {
  212.         return $this->salesChannelContextFactory->create(''$salesChannelId,
  213.             [SalesChannelContextService::LANGUAGE_ID => $languageId]);
  214.     }
  215.     /**
  216.      * Leite auf der Detail-Seite um, wenn eine Variante ausgewählt wurde.
  217.      * @param ResponseEvent $event
  218.      * @return void
  219.      */
  220.     public function setVariantIdToDisplay(ResponseEvent $event): void
  221.     {
  222.         // prüfe ob die Route frontend.detail.page ist
  223.         $currentRoute $event->getRequest()->attributes->get('_route');
  224.         // wenn nicht -> nicht weiterleiten
  225.         if ($currentRoute !== 'frontend.detail.page') {
  226.             return;
  227.         }
  228.         /**@var StorefrontResponse $storefrontResponse */
  229.         $storefrontResponse $event->getResponse();
  230.         if ($storefrontResponse->getStatusCode() !== 200) {
  231.             return;
  232.         }
  233.         // hole die parentId aus der Route
  234.         $parentProductId $storefrontResponse->getData()['page']->getProduct()->getParentId();
  235.         // hole die productId aus der Route
  236.         $currentProductId $storefrontResponse->getData()['page']->getProduct()->getId();
  237.         // wenn parentId == null -> dann ist es ein Produkt ohne Varianten -> nicht weiterleiten
  238.         if ($parentProductId == null) {
  239.             return;
  240.         }
  241.         // SalesChannelContext holen
  242.         $salesChannelContext $event->getRequest()->attributes->get('sw-sales-channel-context');
  243.         // schaue, welche Variante im Cookie gespeichert ist
  244.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  245.         // wenn keine Variante im Cookie gespeichert ist -> nicht weiterleiten
  246.         if ($variantIdToDisplay == null) {
  247.             return;
  248.         }
  249.         // setze die PropertyGroup und die Optionen für den ProductCombinationFinder um die richtige Variante zu finden
  250.         $options = [
  251.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID => $variantIdToDisplay,
  252.         ];
  253.         // hole das gefundene Produkt
  254.         $finderResponse $this->productCombinationFinder->find(
  255.             $parentProductId,
  256.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID,
  257.             $options ?? [],
  258.             $salesChannelContext
  259.         );
  260.         
  261.         $parentProductId $finderResponse->getVariantId();
  262.         // gefundenes Produkt ist das aktuelle Produkt -> leite nicht um
  263.         if ($parentProductId == $currentProductId) {
  264.             return;
  265.         }
  266.         // redirect to the new URL
  267.         $host $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
  268.             . $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
  269.         $url $this->seoUrlPlaceholderHandler->replace(
  270.             $this->seoUrlPlaceholderHandler->generate(
  271.                 'frontend.detail.page',
  272.                 ['productId' => $parentProductId]
  273.             ),
  274.             $host,
  275.             $salesChannelContext
  276.         );
  277.         $response = new RedirectResponse($url);
  278.         $event->setResponse($response);
  279.     }
  280.     /**
  281.      * Wähle die Variante aus, die angezeigt werden soll. Die ausgewählte Variante wird in einem Cookie gespeichert.
  282.      * Greift auf der Listing-Seite
  283.      * @param RequestEvent $event
  284.      */
  285.     public function setVariantIdToDisplayFilter(RequestEvent $event): void
  286.     {
  287.         // hole das Cookie für die ausgewählte Variante
  288.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  289.         // hole das Cookie für die ausgewählte Versandart
  290.         $customerAvailableShippingMethodPropertyId $this->requestStack->getCurrentRequest()->cookies->get('customerAvailableShippingMethodProperty');
  291.         // wird für das Listing benötigt
  292.         // nur ausführen, wenn die ausgewählte Variante und die ausgewählte Versandart gesetzt sind
  293.         if ($variantIdToDisplay and $customerAvailableShippingMethodPropertyId) {
  294.             // hole die Properties aus der URL
  295.             $lx_properties $event->getRequest()->query->get('properties');
  296.             // wenn Properties gesetzt sind, dann hänge die ausgewählte Variante und die ausgewählte Versandart an
  297.             if ($lx_properties !== null) {
  298.                 $ls_properties $lx_properties '|' $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  299.                 // wenn Properties nicht gesetzt sind, dann hänge nur die ausgewählte Variante und die ausgewählte Versandart an
  300.             } else {
  301.                 $ls_properties $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  302.             }
  303.             if ($ls_properties !== '') {
  304.                 $event->getRequest()->query->set('properties'$ls_properties);
  305.             }
  306.         }
  307.     }
  308.     /**
  309.      * Beim Hinzufügen eines Produkts zum Warenkorb wird geprüft, ob es sich um eine Paketstation handelt.
  310.      * Wenn ja, wird ein Fach reserviert und die Daten in der Datenbank gespeichert
  311.      * @param AfterLineItemAddedEvent $event
  312.      * @return void
  313.      * @throws \Exception
  314.      */
  315.     public function addPaketinLocker(AfterLineItemAddedEvent $event): void
  316.     {
  317.         $shippingMethodId $event->getCart()->getDeliveries()->first()->getShippingMethod()->getId();
  318.         // wenn nicht Paketstation → nichts machen
  319.         if ($shippingMethodId !== DreiwmBrandstetterPlugin::PAKETSTATION_ID) {
  320.             return;
  321.         }
  322.         // hole das Datum aus dem Cookie
  323.         $desiredDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  324.         // prüfe, ob schon ein Fach reserviert wurde
  325.         $context Context::createDefaultContext();
  326.         $criteria = new Criteria();
  327.         // filtere nach dem Cart-Token und dem Datum
  328.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  329.             new EqualsFilter('date'$desiredDate),
  330.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  331.         ]));
  332.         $reservedLocker $this->freeLockerRepository->searchIds($criteria$context)->firstId();
  333.         // wenn schon ein Fach reserviert wurde → nichts machen
  334.         if ($reservedLocker) {
  335.             return;
  336.         }
  337.         // alle Datensätze löschen, die den gleichen Cart-Token haben aber ein anderes Datum
  338.         $deleteCriteria = new Criteria();
  339.         $deleteCriteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  340.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  341.             new NotFilter(NotFilter::CONNECTION_AND, [
  342.                 new EqualsFilter('date'$desiredDate),
  343.             ]),
  344.         ]));
  345.         $dataToDelete $this->freeLockerRepository->search($deleteCriteria$context);
  346.         if($dataToDelete->count() > 0) {
  347.             // gehe die Einträge durch. Jeder Eintrag enthält die Daten für ein Fach
  348.             foreach ($dataToDelete as $entry) {
  349.                 $pinsToDelete = [$entry->getMerchantPin(), $entry->getCustomerPin()];
  350.                 // lösche die Pins für das Fach
  351.                 $this->packingStationService->deletePins($entry->getBoxId(),$pinsToDelete);
  352.                 // lösche den Eintrag in der Tabelle dreiwm_free_locker
  353.                 $this->freeLockerRepository->delete([['id' => $entry->getId()]], $context);
  354.             }
  355.         }
  356.         // reserviere ein Fach
  357.         $reservedLockerData $this->packingStationService->reservePaketinLockerOnDate(new DateTime($desiredDate),
  358.             $event->getCart()->getToken());
  359.         // wenn kein Fach reserviert werden konnte → Fehlermeldung
  360.         if ($reservedLockerData == null) {
  361.             // Fehlermeldung CustomerTooLateForPackstationError
  362.             $event->getCart()->addErrors(new CustomerTooLateForPackstationError());
  363.             return;
  364.         }
  365.         // speichere das Cart-Token in 3wmfreelocker
  366.         $this->freeLockerRepository->upsert([
  367.             [
  368.                 'cartToken' => $event->getCart()->getToken(),
  369.                 'boxId' => $reservedLockerData['boxId'],
  370.                 'boxName' => $reservedLockerData['boxName'],
  371.                 'merchantPin' => $reservedLockerData['merchantPin'],
  372.                 'customerPin' => $reservedLockerData['customerPin'],
  373.                 'date' => $desiredDate,
  374.                 'freeLocker' => 1,
  375.             ]
  376.         ], $context);
  377.     }
  378. }