custom/plugins/ZweiPunktVariantenAusgrauen/src/Subscriber/ChooseAvailableVariant.php line 145

Open in your IDE?
  1. <?php
  2. namespace ZweiPunktVariantenAusgrauen\Subscriber;
  3. use Exception;
  4. use Shopware\Core\Content\Product\ProductEntity;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  11. use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
  12. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  13. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  14. use Shopware\Core\System\SystemConfig\SystemConfigService;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. use Symfony\Component\HttpFoundation\RequestStack;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use ZweiPunktVariantenAusgrauen\ZweiPunktVariantenAusgrauen;
  19. /**
  20.  * Class ChooseAvailableVariant
  21.  *
  22.  * Used to set the link to an available variant.
  23.  *
  24.  * @package ZweiPunktVariantenAusgrauen\Subscriber
  25.  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  26.  */
  27. class ChooseAvailableVariant implements EventSubscriberInterface
  28. {
  29.     /**
  30.      * @var SalesChannelRepositoryInterface
  31.      */
  32.     private $productRepository;
  33.     /**
  34.      * @var RequestStack
  35.      */
  36.     protected $requestStack;
  37.     /**
  38.      * @var array<string, mixed>
  39.      */
  40.     private $config;
  41.     /**
  42.      * Holds the crontroller and action of the current request
  43.      *
  44.      * @var string
  45.      */
  46.     private string $controllerAction '';
  47.     private SalesChannelEntityLoadedEvent $event;
  48.     /**
  49.      * ChooseAvailableVariant constructor.
  50.      *
  51.      * @param SalesChannelRepositoryInterface $productRepository
  52.      * @param RequestStack $requestStack
  53.      * @param SystemConfigService $systemConfigService
  54.      */
  55.     public function __construct(
  56.         SalesChannelRepositoryInterface $productRepository,
  57.         RequestStack $requestStack,
  58.         SystemConfigService $systemConfigService
  59.     ) {
  60.         $this->productRepository $productRepository;
  61.         $this->requestStack $requestStack;
  62.         // Get plugin configuration
  63.         $this->config $systemConfigService
  64.             ->get(ZweiPunktVariantenAusgrauen::PLUGIN_NAME '.config');
  65.     }
  66.     /**
  67.      * @return string[]
  68.      */
  69.     public static function getSubscribedEvents(): array
  70.     {
  71.         return [
  72.             'sales_channel.product.loaded' => 'getUrlFromAvailableVariant'
  73.         ];
  74.     }
  75.     /**
  76.      * Determines the id for the url generation of an available variant for a variant product
  77.      *
  78.      * @param SalesChannelEntityLoadedEvent $event
  79.      * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  80.      */
  81.     public function getUrlFromAvailableVariant(
  82.         SalesChannelEntityLoadedEvent $event
  83.     ): void {
  84.         $this->event $event;
  85.         try {
  86.             $this->checkRequest();
  87.             $products $this->getProducts();
  88.         } catch (Exception $exception) {
  89.             return;
  90.         }
  91.         // Goes through all products to determine the parent ids
  92.         $productsForNewUrl = [];
  93.         foreach ($products as $product) {
  94.             try {
  95.                 $this->checkProduct($product);
  96.             } catch (Exception $exception) {
  97.                 continue;
  98.             }
  99.             // If a parent id is set on the product, then it must be entered into the array.
  100.             if (is_string($product->get('parentId'))) {
  101.                 $productsForNewUrl[] = $product->get('parentId');
  102.             }
  103.             // If no parent id is set on the product,
  104.             // then it must be checked if the product has a child count and
  105.             // accordingly its own id must be entered into the array.
  106.             // Both cases can never be true at the same time,
  107.             // because variants do not have a child count and main items do not have a parent id.
  108.             if ($product->get('childCount') > 0) {
  109.                 $productsForNewUrl[] = $product->get('id');
  110.             }
  111.         }
  112.         // For the set parent ids, the variants are determined
  113.         $siblings $this->getSiblings(
  114.             $productsForNewUrl,
  115.             $event->getSalesChannelContext()
  116.         )->getEntities();
  117.         // For each product, the variants are gone through to determine the ids.
  118.         foreach ($products as $product) {
  119.             foreach ($siblings as $sibling) {
  120.                 // Here again, a difference must be made between the main product
  121.                 // and the variant, since a main item can also be selected in a slider and
  122.                 // have no variant information to display.
  123.                 if ($product->get('parentId') == $sibling->getParentId()) {
  124.                     $product->assign(['availableVariant' => $sibling]);
  125.                 }
  126.                 // If the id of the product matches the parentid of the variant,
  127.                 // then it is a main item.
  128.                 if ($product->get('id') == $sibling->getParentId()) {
  129.                     $product->assign(['availableVariant' => $sibling]);
  130.                 }
  131.             }
  132.         }
  133.     }
  134.     /**
  135.      * @param string[] $parentIds
  136.      * @param SalesChannelContext $context
  137.      * @return EntitySearchResult
  138.      */
  139.     private function getSiblings(
  140.         array $parentIds,
  141.         SalesChannelContext $context
  142.     ): EntitySearchResult {
  143.         $criteria = new Criteria();
  144.         $criteria->addFilter(new EqualsAnyFilter('parentId'$parentIds));
  145.         $criteria->addFilter(
  146.             new RangeFilter('availableStock', [
  147.                 RangeFilter::GT => 0
  148.             ])
  149.         );
  150.         $criteria
  151.             ->addAssociation('translated')
  152.             ->addAssociation('manufacturer.media')
  153.             ->addAssociation('options.group')
  154.             ->addAssociation('properties.group')
  155.             ->addAssociation('mainCategories.category')
  156.             ->addAssociation('deliveryTime.deliveryTime')
  157.             ->addAssociation('media');
  158.         return $this
  159.             ->productRepository
  160.             ->search($criteria$context);
  161.     }
  162.     private function checkRequest(): void
  163.     {
  164.         // Determines the current controller and its action
  165.         $request $this->requestStack->getCurrentRequest();
  166.         // no http protocol / browser request via console calls
  167.         if (!$request instanceof Request) {
  168.             throw new Exception('No request available');
  169.         }
  170.         $controllerPath explode('\\'$request->attributes->get('_controller'));
  171.         $this->controllerAction end($controllerPath);
  172.         // Sets the array for the controllers for which a change is to be made
  173.         // Allowed controllers and the action are:
  174.         // Home page, Listing, CMS Pages with product sliders or boxes, Detail page for cross selling.
  175.         $allowedController = [
  176.             'NavigationController::home',
  177.             'ProductController::index',
  178.             'NavigationController::index',
  179.             'CmsController::category'
  180.         ];
  181.         // If the current controller is not in the array, the function can be exited
  182.         if (false == in_array($this->controllerAction$allowedController)) {
  183.             throw new Exception('Not a valid request');
  184.         }
  185.     }
  186.     /**
  187.      * @return ProductEntity[]
  188.      * @throws Exception
  189.      */
  190.     private function getProducts(): array
  191.     {
  192.         // The products are determined
  193.         $products $this->event->getEntities();
  194.         // If no product is present, the function can be exited
  195.         if (empty($products)) {
  196.             throw new Exception('No products');
  197.         }
  198.         // If it is the controller and the action that is responsible for the detail page,
  199.         // then the pass can be skipped with only one product,
  200.         // since this is the called item.
  201.         // Another pass would then be the cross selling,
  202.         // which contains more articles.
  203.         if (
  204.             == count($products) &&
  205.             'ProductController::index' == $this->controllerAction
  206.         ) {
  207.             throw new Exception('Detail page');
  208.         }
  209.         return $products;
  210.     }
  211.     private function checkProduct(ProductEntity $product): void
  212.     {
  213.         // If the product is not a closeout product,
  214.         // but in the configuration only closeout products are marked,
  215.         // then it can continue with the next product.
  216.         if (
  217.             !$product->getIsCloseout() &&
  218.             $this->config["onlyCloseoutProducts"]
  219.         ) {
  220.             throw new Exception('Only closeout products');
  221.         }
  222.         // If the product has an available stock greater than zero
  223.         // and a child count of 0, then it is a variant that already has a stock
  224.         // and therefore does not need to be considered further.
  225.         // If a main item has been selected for a product box and has an available inventory greater than zero,
  226.         // then its variants with an inventory must still be checked
  227.         // so that a corresponding variant can be linked to.
  228.         // Therefore, only the variants are tested for available stock.
  229.         if ($product->get('availableStock') > && == $product->get('childCount')) {
  230.             throw new Exception('has stock');
  231.         }
  232.     }
  233. }