custom/plugins/ZweiPunktVariantsTableOverview/src/Subscriber/AllVariantDetailPage.php line 60

Open in your IDE?
  1. <?php
  2. namespace ZweiPunktVariantsTableOverview\Subscriber;
  3. use Shopware\Core\Content\Product\ProductEntity;
  4. use Shopware\Core\Content\Property\PropertyGroupCollection;
  5. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  9. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  10. use Shopware\Core\System\Unit\UnitEntity;
  11. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Shopware\Core\Framework\Feature;
  14. use Symfony\Component\DependencyInjection\ContainerInterface;
  15. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  16. /**
  17.  * Class AllVariantDetailPage
  18.  *
  19.  * Used to collect the information of the variants
  20.  */
  21. class AllVariantDetailPage implements EventSubscriberInterface
  22. {
  23.     private SalesChannelRepository $productRepository;
  24.     private ProductEntity $currentSibling;
  25.     public ContainerInterface $container;
  26.     /**
  27.      * AllVariantDetailPage constructor.
  28.      *
  29.      * @param SalesChannelRepository $productRepository
  30.      * @param ContainerInterface $container
  31.      */
  32.     public function __construct(
  33.         SalesChannelRepository $productRepository,
  34.         ContainerInterface $container
  35.     ) {
  36.         $this->productRepository $productRepository;
  37.         $this->container $container;
  38.     }
  39.     /**
  40.      * @return string[]
  41.      */
  42.     public static function getSubscribedEvents(): array
  43.     {
  44.         return [
  45.             ProductPageLoadedEvent::class => 'addAllVariantToDetailPage'
  46.         ];
  47.     }
  48.     /**
  49.      * Used to prepare the information of the variants for the overview table.
  50.      *
  51.      * @param ProductPageLoadedEvent $event
  52.      */
  53.     public function addAllVariantToDetailPage(
  54.         ProductPageLoadedEvent $event
  55.     ): void {
  56.         // Determines the current product
  57.         $product $event->getPage()->getProduct();
  58.         // Check for a parent product and skip if it's not a variant
  59.         $parentId $product->getParentId();
  60.         if (empty($parentId)) {
  61.             return;
  62.         }
  63.         // Get all siblings (products with the same parent)
  64.         $siblings $this->getSiblings(
  65.             $parentId,
  66.             $event->getSalesChannelContext()
  67.         );
  68.         $variants $this->prepareVariantsData(
  69.             $siblings,
  70.             $event->getPage()->getConfiguratorSettings()
  71.         );
  72.         // Check if we're running on Shopware 6.5 or newer
  73.         if (!Feature::isActive('v6.5.0.0')) {
  74.             // Get the CSRF token manager from the container
  75.             $csrfTokenManager $this
  76.                 ->container
  77.                 ->get('security.csrf.token_manager');
  78.             // Generate the CSRF token
  79.             $tableViewCsrfToken $csrfTokenManager
  80.                 ->getToken('frontend.checkout.line-item.add')
  81.                 ->getValue();
  82.         } else {
  83.             $tableViewCsrfToken null;
  84.         }
  85.         // The array is passed to the page and
  86.         // can be found at page.extensions.allVariants.
  87.         $event
  88.             ->getPage()
  89.             ->assign([
  90.                 'allVariants' => $variants,
  91.                 'variantsTable_csrf_token' => $tableViewCsrfToken
  92.             ]);
  93.     }
  94.     /**
  95.      * Determines the variants and
  96.      * all associated information based on the passed parentId.
  97.      *
  98.      * @param string $parent
  99.      * @param SalesChannelContext $context
  100.      * @return EntitySearchResult
  101.      */
  102.     private function getSiblings(
  103.         string $parent,
  104.         SalesChannelContext $context
  105.     ): EntitySearchResult {
  106.         $criteria = new Criteria();
  107.         $criteria->addFilter(new EqualsFilter('parentId'$parent))
  108.             ->addAssociation('manufacturer.media')
  109.             ->addAssociation('customFields')
  110.             ->addAssociation('options.group')
  111.             ->addAssociation('properties.group')
  112.             ->addAssociation('mainCategories.category')
  113.             ->addAssociation('deliveryTime.deliveryTime')
  114.             ->getAssociation('media')
  115.             ->addAssociation('unit');
  116.         return $this->productRepository->search($criteria$context);
  117.     }
  118.     /**
  119.      * Prepare the variant data for the overview table.
  120.      *
  121.      * @param EntitySearchResult $siblings
  122.      * @param PropertyGroupCollection $configSettings
  123.      * @return array<string, mixed>
  124.      */
  125.     private function prepareVariantsData(
  126.         EntitySearchResult $siblings,
  127.         PropertyGroupCollection $configSettings
  128.     ): array {
  129.         $variants = [];
  130.         if ($siblings !== null) {
  131.             foreach ($siblings->getElements() as $sibling) {
  132.                 if (!$sibling->getActive()) {
  133.                     continue;
  134.                 }
  135.                 $this->currentSibling $sibling;
  136.                 $stock $sibling->getAvailableStock() >= 1;
  137.                 $prices $this->calculateReferencePrice($sibling);
  138.                 $variantOptions array_column(
  139.                     $sibling
  140.                         ->getVariation(),
  141.                     'option'
  142.                 );
  143.                 $image $this->getProductImage();
  144.                 // The delivery time is determined.
  145.                 $deliveryTime $sibling
  146.                     ->getDeliveryTime() ? $sibling
  147.                     ->getDeliveryTime()
  148.                     ->getName() : null;
  149.                 // Determines the position of the variant in the order
  150.                 // in which the variants should later appear
  151.                 $positions $this->getPosition($configSettings);
  152.                 $position '';
  153.                 foreach ($sibling->getOptions() as $option) {
  154.                     foreach ($positions as $index => $positionId) {
  155.                         if ($option->getId() == $index) {
  156.                             $position .= $positionId;
  157.                         }
  158.                     }
  159.                 }
  160.                 $unit $sibling->getUnit();
  161.                 $unitDetails $this->getUnitDetails($unit);
  162.                 $unitName $unitDetails['unitName'];
  163.                 $unitShortCode $unitDetails['unitShortCode'];
  164.                 $unitId $unitDetails['unitId'];
  165.                 // All information is written to the array
  166.                 $variants[$position] = [
  167.                     'media' => $image['image'],
  168.                     'altTag' => $image['alt'],
  169.                     'productNumber' => $sibling->getProductNumber(),
  170.                     'manufacturerNumber' => $sibling->getManufacturerNumber(),
  171.                     'variation' => null,
  172.                     'price' => $prices['price'],
  173.                     'referenceUnitPrice' => $prices['referenceUnitPrice'],
  174.                     'referenceUnitName' => $prices['referenceUnitName'],
  175.                     'referenceUnit' => $prices['referenceUnit'],
  176.                     'inStock' => $stock,
  177.                     'options' => $variantOptions,
  178.                     'deliveryTime' => $deliveryTime,
  179.                     'id' => $sibling->getId(),
  180.                     'available' => $sibling->getAvailable(),
  181.                     'minPurchase' => $sibling->getMinPurchase(),
  182.                     'purchaseUnit' => $sibling->getPurchaseUnit(),
  183.                     'purchaseUnitName' => $unitName,
  184.                     'purchaseSteps' => $sibling->getPurchaseSteps(),
  185.                     'availableStock' => $sibling->getAvailableStock(),
  186.                     'translated' => $sibling->getTranslated(),
  187.                     'restockTime' => $sibling->getRestockTime(),
  188.                     'deliveryTimeTranslation' => $deliveryTime,
  189.                     'product' => $sibling,
  190.                     'isCloseout' => $sibling->getIsCloseout(),
  191.                     'shippingFree' => $sibling->getShippingFree(),
  192.                     'active' => $sibling->getActive(),
  193.                     'releaseDate' => $sibling->getReleaseDate(),
  194.                     'advancedPrices' => $this->prepareAdvancedPrices($siblings)[$sibling->getId()],
  195.                     'unitId' => $unitId,
  196.                     'unitShortCode' => $unitShortCode,
  197.                     'unitName' => $unitName
  198.                 ];
  199.             }
  200.         }
  201.         // Sorts the variants by position to match
  202.         // the order of the Configurator options
  203.         ksort($variants);
  204.         return $variants;
  205.     }
  206.     /**
  207.      * @param UnitEntity $unit
  208.      * @return array{unitId: string, unitShortCode: string, unitName: string, }
  209.      */
  210.     private function getUnitDetails($unit): array
  211.     {
  212.         $unitId '';
  213.         $unitShortCode  '';
  214.         $unitName '';
  215.         if ($unit instanceof UnitEntity) {
  216.             $unitId $unit->getId();
  217.             $unitShortCode $unit->getTranslated()['shortCode'];
  218.             $unitName $unit->getTranslated()['name'];
  219.         }
  220.         return ['unitId' => $unitId'unitShortCode' => $unitShortCode'unitName' => $unitName, ];
  221.     }
  222.     /**
  223.      * Get the product price and reference price.
  224.      *
  225.      * Warnung wurde nach Rücksprache von dengie und kevbei unterdrückt.
  226.      *
  227.      * @return array<string, mixed>
  228.      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  229.      */
  230.     private function calculateReferencePrice(
  231.         SalesChannelProductEntity $sibling
  232.     ): array {
  233.         $referencePrice 0;
  234.         $referenceUnitName '';
  235.         $referenceUnit 1;
  236.         $calcPrice $sibling->getCalculatedPrice();
  237.         $minQty $sibling->getMinPurchase();
  238.         $calcPrices $sibling->getCalculatedPrices();
  239.         if ($minQty 1) {
  240.             $selectedOption null;
  241.             $nextHigherQty null;
  242.             foreach ($calcPrices as $priceOption) {
  243.                 $quantity $priceOption->getQuantity();
  244.                 if (
  245.                     $quantity >= $minQty && (
  246.                         $nextHigherQty === null ||
  247.                         $quantity $nextHigherQty
  248.                     )
  249.                 ) {
  250.                     $nextHigherQty $quantity;
  251.                     $selectedOption $priceOption;
  252.                 }
  253.             }
  254.             if ($selectedOption === null) {
  255.                 // Kein höherer Preis gefunden, wähle den höchsten kleineren Preis
  256.                 $maxLowerQty 0;
  257.                 foreach ($calcPrices as $priceOption) {
  258.                     if (
  259.                         $priceOption
  260.                             ->getQuantity() > $maxLowerQty &&
  261.                         $priceOption->getQuantity() < $minQty
  262.                     ) {
  263.                         $maxLowerQty $priceOption->getQuantity();
  264.                         $selectedOption $priceOption;
  265.                     }
  266.                 }
  267.             }
  268.             $price $selectedOption $selectedOption
  269.                     ->getUnitPrice() * $minQty $calcPrice
  270.                     ->getTotalPrice() * $minQty;
  271.         } else {
  272.             // Überprüfen, ob getTotalPrice() einen gültigen Wert hat.
  273.             $price = (empty($sibling
  274.                 ->getCalculatedPrices()
  275.                 ->first())) ? $sibling
  276.                 ->getCalculatedPrice()
  277.                 ->getTotalPrice() : $sibling
  278.                 ->getCalculatedPrices()
  279.                 ->first()
  280.                 ->getUnitPrice();
  281.             // Überprüfen, ob getReferencePrice() einen gültigen Wert hat.
  282.             if ($calcPrice->getReferencePrice() !== null) {
  283.                 $referencePrice $calcPrice->getReferencePrice()->getPrice();
  284.                 $referenceUnitName $calcPrice
  285.                     ->getReferencePrice()
  286.                     ->getUnitName();
  287.             }
  288.         }
  289.         return [
  290.             'price' => $price,
  291.             'referenceUnitPrice' => $referencePrice,
  292.             'referenceUnitName' => $referenceUnitName,
  293.             'referenceUnit' => $referenceUnit
  294.         ];
  295.     }
  296.     /**
  297.      * Get the product image and alt tag.
  298.      *
  299.      * @return array<string, string>
  300.      */
  301.     private function getProductImage(): array
  302.     {
  303.         $image null;
  304.         $altTag null;
  305.         foreach ($this->currentSibling->getMedia()->getElements() as $media) {
  306.             if ($this->currentSibling->getCoverId() == $media->getId()) {
  307.                 $image $media->getMedia()->getUrl();
  308.             }
  309.             if (!empty($media->getMedia()->getAlt())) {
  310.                 $altTag $media->getMedia()->getAlt();
  311.             } else {
  312.                 $altTag $this->currentSibling->getProductNumber();
  313.             }
  314.         }
  315.         return [
  316.             'image' => $image,
  317.             'alt' => $altTag
  318.         ];
  319.     }
  320.     /**
  321.      * Sets the order of the Configurator options.
  322.      *
  323.      * @param PropertyGroupCollection $configSettings
  324.      * @return array<string ,int>
  325.      */
  326.     private function getPosition(PropertyGroupCollection $configSettings): array
  327.     {
  328.         $positions = [];
  329.         $i 1;
  330.         foreach ($configSettings->getElements() as $configSetting) {
  331.             foreach ($configSetting->getOptions()->getElements() as $option) {
  332.                 $positions[$option->getId()] = $i;
  333.                 $i++;
  334.             }
  335.         }
  336.         return $positions;
  337.     }
  338.     /**
  339.      * returns the staggered prices including due toeter columns
  340.      *
  341.      * @return array<string, mixed>
  342.      */
  343.     private function prepareAdvancedPrices(
  344.         EntitySearchResult $siblings
  345.     ): array {
  346.         $prices = [];
  347.         // find the largest number of scales
  348.         $pricesCount 0;
  349.         foreach ($siblings as $sibling) {
  350.             if (count($sibling->getCalculatedPrices()->getElements()) > $pricesCount) {
  351.                 $pricesCount count($sibling->getCalculatedPrices()->getElements());
  352.             }
  353.         }
  354.         // top up for the column edition
  355.         foreach ($siblings as $variantId => $sibling) {
  356.             $prices[$variantId] = $sibling->getCalculatedPrices()->getElements();
  357.             if (count($prices[$variantId]) < $pricesCount) {
  358.                 for ($i = ($pricesCount count($prices[$variantId])); $i 0$i--) {
  359.                     $prices[$variantId][] = false;
  360.                 }
  361.             }
  362.         }
  363.         return $prices;
  364.     }
  365. }