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

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.         $this->frontendProductAssociation($event->getCriteria());
  100.         // --- 1. Wochentag aus dem aktuellen Datum ermitteln ---
  101.         // hole das aktuelle Datum aus dem Cookie
  102.         $currentDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  103.         // hole die ausgewählte Versandart aus dem Cookie
  104.         $customerSelectedDeliveryId $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDeliveryId');
  105.         // Wenn Post ausgewählt ist, dann setze das gewählte Datum auf heute
  106.         if ($customerSelectedDeliveryId == DreiwmBrandstetterPlugin::POST_ID) {
  107. //            return;
  108.             $currentDate = (new DateTime())->format('Y-m-d');
  109.         }
  110.         // wenn kein aktuelles Datum gesetzt ist und die Versandart nicht Post ist, dann breche ab
  111.         if ($currentDate == null && $customerSelectedDeliveryId !== DreiwmBrandstetterPlugin::POST_ID) {
  112.             return;
  113.         }
  114.         // Datum in DateTime umwandeln
  115.         $currentDate = (new DateTime($currentDate))->format('Y-m-d');
  116.         // Filtere Produkte mit Lagerbestand
  117.         $event->getCriteria()->addFilter(new EqualsFilter('product.available'1));
  118.         // Erstelle ein DateTime-Objekt aus dem aktuellen Datum
  119.         $dt = new DateTime($currentDate);
  120.         // Ermittle den englischen Wochentag, z. B. "Mon", "Tue", etc.
  121.         $englishDay $dt->format('D');
  122.         // Mappen des englischen Wochentags auf das deutsche Kürzel
  123.         $dayMap = [
  124.             'Mon' => 'Mo',
  125.             'Tue' => 'Di',
  126.             'Wed' => 'Mi',
  127.             'Thu' => 'Do',
  128.             'Fri' => 'Fr',
  129.             'Sat' => 'Sa',
  130.             'Sun' => 'So'
  131.         ];
  132.         $desiredDay $dayMap[$englishDay] ?? null;
  133.         // --- 2. Gemeinsamer Datum-Filter ("common date filter") ---
  134.         // Dieser Filter deckt die üblichen Fälle ab, wie z.B.:
  135.         // - Produkte, die sowohl 'product_available_from' als auch 'product_available_until' gesetzt haben und in den Zeitraum fallen,
  136.         // - Produkte, bei denen nur eines der Felder gesetzt ist,
  137.         // - Produkte ohne beide Felder (immer verfügbar).
  138.         $commonDateFilter = new OrFilter([
  139.             // Fall 1: Beide Felder vorhanden und aktuelles Datum liegt dazwischen
  140.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  141.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  142.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  143.             ]),
  144.             // Fall 2: Nur 'product_available_from' vorhanden
  145.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  146.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  147.                 new EqualsFilter('customFields.product_available_until'null),
  148.             ]),
  149.             // Fall 3: Nur 'product_available_until' vorhanden
  150.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  151.                 new EqualsFilter('customFields.product_available_from'null),
  152.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  153.             ]),
  154.             // Fall 4: Keine Datumsfelder gesetzt
  155.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  156.                 new EqualsFilter('customFields.product_available_from'null),
  157.                 new EqualsFilter('customFields.product_available_until'null),
  158.             ]),
  159.         ]);
  160.         // --- 3. Filter für Produkte ohne baking_days (Gruppe A) ---
  161.         // Diese Produkte dürfen kein baking_days-Feld gesetzt haben und müssen nur den Datumskriterien genügen.
  162.         $filterWithoutBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  163.             new EqualsFilter('customFields.baking_days'null),
  164.             $commonDateFilter,
  165.         ]);
  166.         // 4. Gruppe B (mit baking_days)
  167.         $filterWithBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  168.             // baking_days gesetzt
  169.             new NotFilter(NotFilter::CONNECTION_AND, [
  170.                 new EqualsFilter('customFields.baking_days'null),
  171.             ]),
  172.             // enthält gewähltes Kürzel? (z. B. "Wed")
  173.             new ContainsFilter('customFields.baking_days'$desiredDay),
  174.             // Datumskriterien
  175.             $commonDateFilter,
  176.         ]);
  177.         // 5. Final immer beide Gruppen zusammenführen:
  178.         $finalFilter = new OrFilter([
  179.             $filterWithoutBakingDays,
  180.             $filterWithBakingDays,
  181.         ]);
  182.         $event->getCriteria()->addFilter($finalFilter);
  183.     }
  184.     private function loadProductCustomField(ProductListingCriteriaEvent $eventstring $productNumber): array
  185.     {
  186.         // ganz simple EINE Abfrage nur für Debug:
  187.         $criteria = new Criteria();
  188.         $criteria->addFilter(new EqualsFilter('product.productNumber'$productNumber));
  189.         $criteria->addAssociation('customFields');
  190.         $product $this->productRepository->search($criteria$event->getContext())->first();
  191.         return $product $product->getCustomFields() : [];
  192.     }
  193.     /**
  194.      * Füge die Association für die Frontend-Produktanzeige hinzu
  195.      * @param $criteria
  196.      */
  197.     private function frontendProductAssociation($criteria): void
  198.     {
  199.         $criteria->addAssociation('properties');
  200.         $criteria->addAssociation('properties.group');
  201.     }
  202.     /**
  203.      * Erstelle einen SalesChannelContext
  204.      * @param string $salesChannelId
  205.      * @param string $languageId
  206.      * @return SalesChannelContext $salesChannelContext
  207.      */
  208.     public function createSalesChannelContext(string $salesChannelIdstring $languageId): SalesChannelContext
  209.     {
  210.         return $this->salesChannelContextFactory->create(''$salesChannelId,
  211.             [SalesChannelContextService::LANGUAGE_ID => $languageId]);
  212.     }
  213.     /**
  214.      * Leite auf der Detail-Seite um, wenn eine Variante ausgewählt wurde.
  215.      * @param ResponseEvent $event
  216.      * @return void
  217.      */
  218.     public function setVariantIdToDisplay(ResponseEvent $event): void
  219.     {
  220.         // prüfe ob die Route frontend.detail.page ist
  221.         $currentRoute $event->getRequest()->attributes->get('_route');
  222.         // wenn nicht -> nicht weiterleiten
  223.         if ($currentRoute !== 'frontend.detail.page') {
  224.             return;
  225.         }
  226.         /**@var StorefrontResponse $storefrontResponse */
  227.         $storefrontResponse $event->getResponse();
  228.         if ($storefrontResponse->getStatusCode() !== 200) {
  229.             return;
  230.         }
  231.         // hole die parentId aus der Route
  232.         $parentProductId $storefrontResponse->getData()['page']->getProduct()->getParentId();
  233.         // hole die productId aus der Route
  234.         $currentProductId $storefrontResponse->getData()['page']->getProduct()->getId();
  235.         // wenn parentId == null -> dann ist es ein Produkt ohne Varianten -> nicht weiterleiten
  236.         if ($parentProductId == null) {
  237.             return;
  238.         }
  239.         // SalesChannelContext holen
  240.         $salesChannelContext $event->getRequest()->attributes->get('sw-sales-channel-context');
  241.         // schaue, welche Variante im Cookie gespeichert ist
  242.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  243.         // wenn keine Variante im Cookie gespeichert ist -> nicht weiterleiten
  244.         if ($variantIdToDisplay == null) {
  245.             return;
  246.         }
  247.         // setze die PropertyGroup und die Optionen für den ProductCombinationFinder um die richtige Variante zu finden
  248.         $options = [
  249.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID => $variantIdToDisplay,
  250.         ];
  251.         // hole das gefundene Produkt
  252.         $finderResponse $this->productCombinationFinder->find(
  253.             $parentProductId,
  254.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID,
  255.             $options ?? [],
  256.             $salesChannelContext
  257.         );
  258.         
  259.         $parentProductId $finderResponse->getVariantId();
  260.         // gefundenes Produkt ist das aktuelle Produkt -> leite nicht um
  261.         if ($parentProductId == $currentProductId) {
  262.             return;
  263.         }
  264.         // redirect to the new URL
  265.         $host $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
  266.             . $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
  267.         $url $this->seoUrlPlaceholderHandler->replace(
  268.             $this->seoUrlPlaceholderHandler->generate(
  269.                 'frontend.detail.page',
  270.                 ['productId' => $parentProductId]
  271.             ),
  272.             $host,
  273.             $salesChannelContext
  274.         );
  275.         $response = new RedirectResponse($url);
  276.         $event->setResponse($response);
  277.     }
  278.     /**
  279.      * Wähle die Variante aus, die angezeigt werden soll. Die ausgewählte Variante wird in einem Cookie gespeichert.
  280.      * Greift auf der Listing-Seite
  281.      * @param RequestEvent $event
  282.      */
  283.     public function setVariantIdToDisplayFilter(RequestEvent $event): void
  284.     {
  285.         // hole das Cookie für die ausgewählte Variante
  286.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  287.         // hole das Cookie für die ausgewählte Versandart
  288.         $customerAvailableShippingMethodPropertyId $this->requestStack->getCurrentRequest()->cookies->get('customerAvailableShippingMethodProperty');
  289.         // wird für das Listing benötigt
  290.         // nur ausführen, wenn die ausgewählte Variante und die ausgewählte Versandart gesetzt sind
  291.         if ($variantIdToDisplay and $customerAvailableShippingMethodPropertyId) {
  292.             // hole die Properties aus der URL
  293.             $lx_properties $event->getRequest()->query->get('properties');
  294.             // wenn Properties gesetzt sind, dann hänge die ausgewählte Variante und die ausgewählte Versandart an
  295.             if ($lx_properties !== null) {
  296.                 $ls_properties $lx_properties '|' $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  297.                 // wenn Properties nicht gesetzt sind, dann hänge nur die ausgewählte Variante und die ausgewählte Versandart an
  298.             } else {
  299.                 $ls_properties $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  300.             }
  301.             if ($ls_properties !== '') {
  302.                 $event->getRequest()->query->set('properties'$ls_properties);
  303.             }
  304.         }
  305.     }
  306.     /**
  307.      * Beim Hinzufügen eines Produkts zum Warenkorb wird geprüft, ob es sich um eine Paketstation handelt.
  308.      * Wenn ja, wird ein Fach reserviert und die Daten in der Datenbank gespeichert
  309.      * @param AfterLineItemAddedEvent $event
  310.      * @return void
  311.      * @throws \Exception
  312.      */
  313.     public function addPaketinLocker(AfterLineItemAddedEvent $event): void
  314.     {
  315.         $shippingMethodId $event->getCart()->getDeliveries()->first()->getShippingMethod()->getId();
  316.         // wenn nicht Paketstation → nichts machen
  317.         if ($shippingMethodId !== DreiwmBrandstetterPlugin::PAKETSTATION_ID) {
  318.             return;
  319.         }
  320.         // hole das Datum aus dem Cookie
  321.         $desiredDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  322.         // prüfe, ob schon ein Fach reserviert wurde
  323.         $context Context::createDefaultContext();
  324.         $criteria = new Criteria();
  325.         // filtere nach dem Cart-Token und dem Datum
  326.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  327.             new EqualsFilter('date'$desiredDate),
  328.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  329.         ]));
  330.         $reservedLocker $this->freeLockerRepository->searchIds($criteria$context)->firstId();
  331.         // wenn schon ein Fach reserviert wurde → nichts machen
  332.         if ($reservedLocker) {
  333.             return;
  334.         }
  335.         // alle Datensätze löschen, die den gleichen Cart-Token haben aber ein anderes Datum
  336.         $deleteCriteria = new Criteria();
  337.         $deleteCriteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  338.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  339.             new NotFilter(NotFilter::CONNECTION_AND, [
  340.                 new EqualsFilter('date'$desiredDate),
  341.             ]),
  342.         ]));
  343.         $dataToDelete $this->freeLockerRepository->search($deleteCriteria$context);
  344.         if($dataToDelete->count() > 0) {
  345.             // gehe die Einträge durch. Jeder Eintrag enthält die Daten für ein Fach
  346.             foreach ($dataToDelete as $entry) {
  347.                 $pinsToDelete = [$entry->getMerchantPin(), $entry->getCustomerPin()];
  348.                 // lösche die Pins für das Fach
  349.                 $this->packingStationService->deletePins($entry->getBoxId(),$pinsToDelete);
  350.                 // lösche den Eintrag in der Tabelle dreiwm_free_locker
  351.                 $this->freeLockerRepository->delete([['id' => $entry->getId()]], $context);
  352.             }
  353.         }
  354.         // reserviere ein Fach
  355.         $reservedLockerData $this->packingStationService->reservePaketinLockerOnDate(new DateTime($desiredDate),
  356.             $event->getCart()->getToken());
  357.         // wenn kein Fach reserviert werden konnte → Fehlermeldung
  358.         if ($reservedLockerData == null) {
  359.             // Fehlermeldung CustomerTooLateForPackstationError
  360.             $event->getCart()->addErrors(new CustomerTooLateForPackstationError());
  361.             return;
  362.         }
  363.         // speichere das Cart-Token in 3wmfreelocker
  364.         $this->freeLockerRepository->upsert([
  365.             [
  366.                 'cartToken' => $event->getCart()->getToken(),
  367.                 'boxId' => $reservedLockerData['boxId'],
  368.                 'boxName' => $reservedLockerData['boxName'],
  369.                 'merchantPin' => $reservedLockerData['merchantPin'],
  370.                 'customerPin' => $reservedLockerData['customerPin'],
  371.                 'date' => $desiredDate,
  372.                 'freeLocker' => 1,
  373.             ]
  374.         ], $context);
  375.     }
  376. }