custom/plugins/ZweiPunktVariantenAusgrauen/src/Subscriber/AddStockToDetailPage.php line 104

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace ZweiPunktVariantenAusgrauen\Subscriber;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Content\Product\ProductEntity;
  6. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  7. use Shopware\Core\System\SystemConfig\SystemConfigService;
  8. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  9. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  10. use ZweiPunktVariantenAusgrauen\ZweiPunktVariantenAusgrauen;
  11. /**
  12.  * Class AddStockToDetailPage
  13.  *
  14.  * Is used to have the stock numbers of the variants submitted to the frontend
  15.  * when loading the detail page.
  16.  *
  17.  * @package ZweiPunktVariantenAusgrauen\Subscriber
  18.  */
  19. class AddStockToDetailPage implements EventSubscriberInterface
  20. {
  21.     /**
  22.      * @var mixed
  23.      */
  24.     private $config;
  25.     private Connection $connection;
  26.     /**
  27.      * AddStockToDetailPage constructor.
  28.      */
  29.     public function __construct(
  30.         SystemConfigService $systemConfigService,
  31.         Connection $connection
  32.     ) {
  33.         // Get plugin configuration
  34.         $this->config $systemConfigService
  35.             ->get(ZweiPunktVariantenAusgrauen::PLUGIN_NAME '.config');
  36.         $this->connection $connection;
  37.     }
  38.     /**
  39.      * @return string[]
  40.      */
  41.     public static function getSubscribedEvents(): array
  42.     {
  43.         return [
  44.             ProductPageLoadedEvent::class => 'onProductPageLoaded'
  45.         ];
  46.     }
  47.     /**
  48.      * This subscriber determens unavailable variants (siblings) and passes
  49.      * unavailable option ids to the theme where the standard shopware logic is
  50.      * used gray out the unavailable options.
  51.      *
  52.      * How it is done:
  53.      * If we have only 1 option group:
  54.      * Check the stock of all other siblings and gray them out
  55.      *
  56.      * If we have 2 option groups or more (i.e. color, size, material):
  57.      * We want to gray out all siblings that are only one change (color, size
  58.      * or material) away from the current product. We have to iterate over
  59.      * every group and check if there are siblings with another option in the
  60.      * current group but every other group/option is matching the current
  61.      * product. Then check if this sibling has stock and if not, save the
  62.      * option id in an array that we pass to the theme.
  63.      *
  64.      * @param ProductPageLoadedEvent $event
  65.      */
  66.     public function onProductPageLoaded(ProductPageLoadedEvent $event): void
  67.     {
  68.         $product $event
  69.             ->getPage()
  70.             ->getProduct();
  71.         // Check for a parent product and skip if it's not a variant
  72.         $parentId $product->getParentId();
  73.         if (empty($parentId)) {
  74.             return;
  75.         }
  76.         // Ermittelt die Options IDs der Produkte die (je nach Konfiguration)
  77.         // keinen (verfügbaren) Bestand haben.
  78.         $optionIds $this->getOptionIds($parentId);
  79.         if (empty($optionIds)) {
  80.             return;
  81.         }
  82.         $unavailable $this->getGreyOutOptions(
  83.             $product,
  84.             $optionIds
  85.         );
  86.         // Handling selection fields
  87.         $hideSelection 'hide' == $this->config["behaviorSelectionAttributes"];
  88.         $event
  89.             ->getPage()
  90.             ->assign([
  91.                 'unavailable' => $unavailable,
  92.                 'hideselection' => $hideSelection
  93.             ])
  94.         ;
  95.     }
  96.     /**
  97.      * Liefert die Option IDs der Produkte die aktuell keinen Bestand haben.
  98.      *
  99.      * @param string $parent
  100.      * @return array<int, array<string, mixed>>
  101.      */
  102.     private function getOptionIds(string $parent): array
  103.     {
  104.         $stockType = ('availableStock' == $this->config['stockType']) ? 'available_stock' 'stock';
  105.         return $this->connection->fetchAll(
  106.             '
  107.             SELECT IF(
  108.                        product.is_closeout IS NULL,
  109.                        (
  110.                            SELECT is_closeout
  111.                              FROM product
  112.                             WHERE id = UNHEX(:parent)
  113.                        ),
  114.                        product.is_closeout
  115.                     ) AS `is_closeout`,
  116.                    product.option_ids
  117.               FROM product
  118.              WHERE product.' $stockType '< 1
  119.                AND product.parent_id = UNHEX(:parent);
  120.             ',
  121.             ['parent' => $parent]
  122.         );
  123.     }
  124.     /**
  125.      * @param SalesChannelProductEntity $product
  126.      * @param array<int, array<string, mixed>> $siblings
  127.      * @return array<int, string>
  128.      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  129.      * @throws \JsonException
  130.      */
  131.     private function getGreyOutOptions(
  132.         SalesChannelProductEntity $product,
  133.         array $siblings
  134.     ): array {
  135.         // Keeps all the options that are unavailable
  136.         $unavailable = [];
  137.         // Options for the main product
  138.         $optionIds $product
  139.             ->getOptionIds()
  140.         ;
  141.         // Iterate over all options (not groups)
  142.         // start with the first option / group
  143.         foreach ($optionIds as $index => $optionId) {
  144.             // create a clone of the array that holds all option ids
  145.             $allOptions $optionIds;
  146.             // we remove the first option/group because we want to check the
  147.             // stock on siblings with a different option in this group,
  148.             // but every other option (in other groups) has to be the same
  149.             unset($allOptions[$index]);
  150.             // iterate over all siblings with another option for the group
  151.             /** @var ProductEntity $sibling */
  152.             foreach ($siblings as $sibling) {
  153.                 // Check if only clouseout products (Abverkaufsartikel) shoud be
  154.                 // marked and skip the product if necessary.
  155.                 if (
  156.                     (null == $sibling['is_closeout'] || == $sibling['is_closeout'])
  157.                     && true == $this->config["onlyCloseoutProducts"]
  158.                 ) {
  159.                     continue;
  160.                 }
  161.                 // do this on all siblings which have a different option
  162.                 // in the current group. This will give us a sibling with only
  163.                 // ONE option different to the main product.
  164.                 if (!in_array($optionIdjson_decode($sibling['option_ids'], true512JSON_THROW_ON_ERROR))) {
  165.                     // 1. get all sibling option ids
  166.                     // 2. remove the matching options
  167.                     // 3. the one difference will remain
  168.                     $siblingOptions json_decode($sibling['option_ids'], true512JSON_THROW_ON_ERROR);
  169.                     // If there is more then one option
  170.                     // check if all other options match or
  171.                     // procceed with the next sibling
  172.                     if (count($siblingOptions) > 1) {
  173.                         foreach ($allOptions as $otherOption) {
  174.                             if (false == in_array($otherOption$siblingOptions)) {
  175.                                 continue 2;
  176.                             }
  177.                             // remove the matched option from the array
  178.                             if (false !== ($i array_search($otherOption$siblingOptions))) {
  179.                                 unset($siblingOptions[$i]);
  180.                             }
  181.                         }
  182.                     }
  183.                     // the current option
  184.                     $unavailable[] = array_pop($siblingOptions);
  185.                 }
  186.             }
  187.         }
  188.         return $unavailable;
  189.     }
  190. }