custom/plugins/WbfkBundles/src/Subscriber/ProductSubscriber.php line 79

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Wbfk\Bundles\Subscriber;
  4. use Doctrine\DBAL\Connection;
  5. use Doctrine\DBAL\Exception;
  6. use Shopware\Core\Content\Product\ProductEvents;
  7. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  8. use Shopware\Core\Defaults;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  13. use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
  14. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  15. use Shopware\Core\System\SystemConfig\SystemConfigService;
  16. use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
  17. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. use Wbfk\Bundles\Core\Content\Product\DataAbstractionLayer\BundleFilter;
  20. use Wbfk\Bundles\Core\Content\Product\DataAbstractionLayer\BundleStockUpdater;
  21. use Wbfk\Bundles\Entity\Bundle\BundleProductCollection;
  22. use Wbfk\Bundles\Service\BundleProductDeliveryAndAvailabilityCalculator;
  23. use WbfkExtensions\Core\Content\WbfkProductExtension\WbfkProductExtensionEntity;
  24. use ApplifactionPriceMachine\Service\ProductPriceCalculationService;
  25. class ProductSubscriber implements EventSubscriberInterface
  26. {
  27. public function __construct(
  28. protected readonly BundleProductDeliveryAndAvailabilityCalculator $bundleProductDeliveryAndAvailabilityCalculator,
  29. protected readonly EntityRepository $bundleProductRepository,
  30. protected readonly SystemConfigService $systemConfigService,
  31. protected readonly Connection $connection,
  32. protected readonly BundleFilter $bundleFilter,
  33. protected readonly BundleStockUpdater $bundleStockUpdater,
  34. protected readonly ?ProductPriceCalculationService $productPriceCalculationService
  35. ) {
  36. }
  37. public static function getSubscribedEvents(): array
  38. {
  39. return [
  40. 'sales_channel.product.loaded' => 'onProductLoaded',
  41. ProductPageCriteriaEvent::class => 'onProductCriteriaLoaded',
  42. ProductPageLoadedEvent::class => 'onProductPageLoaded',
  43. ProductEvents::PRODUCT_WRITTEN_EVENT => [['queueBundleParentPriceCalculation'], ['updateBundleCloseoutStatus']],
  44. ];
  45. }
  46. public function getBundles(string $productId, SalesChannelContext $scContext): BundleProductCollection
  47. {
  48. $context = $scContext->getContext();
  49. $criteria = new Criteria();
  50. $criteria->setTitle("Getting bundle products child products data");
  51. $criteria->addAssociation('childProduct');
  52. $criteria->addFilter(new EqualsFilter('productId', $productId));
  53. /** @var BundleProductCollection $bundles */
  54. $bundles = $this->bundleProductRepository->search($criteria, $context)->getEntities();
  55. return $bundles;
  56. }
  57. public function onProductLoaded(SalesChannelEntityLoadedEvent $event)
  58. {
  59. $products = $event->getEntities();
  60. $scContext = $event->getSalesChannelContext();
  61. $sysMaxQuantity = $this->systemConfigService->getInt(
  62. 'core.cart.maxQuantity',
  63. $scContext->getSalesChannel()->getId()
  64. );
  65. /** @var SalesChannelProductEntity $scProduct */
  66. foreach ($products as $scProduct) {
  67. $bundles = $this->getBundles($scProduct->getId(), $scContext);
  68. if ($bundles->count() === 0) {
  69. return null;
  70. }
  71. /** @noinspection PhpDeprecationInspection */
  72. $scProduct->addExtension('wbfk_bundle_product', $bundles);
  73. // Calculate the max available Quantity.
  74. $steps = $scProduct->get('purchaseSteps') ?? 1;
  75. $min = $scProduct->get('minPurchase') ?? 1;
  76. $max = $scProduct->get('maxPurchase') ?? $sysMaxQuantity;
  77. // If the max Quantity is set to "0", we ignore it.
  78. // #1124: ANG12299 kann nicht angenommen werden
  79. $max = $max <= 0 ? $sysMaxQuantity : $max;
  80. // the amount of times the purchase step is fitting in between min and max added to the minimum
  81. $max = \floor(($max - $min) / $steps) * $steps + $min;
  82. $maxPurchase = [$max];
  83. // Consider Child-Articles
  84. $parentCloseOut = $scProduct->get('isCloseout');
  85. foreach ($bundles as $bundleProduct) {
  86. $childProduct = $bundleProduct->getChildProduct();
  87. /*
  88. * If the bundle or the child is in clearance sale (closeout) we restrict the bundle by the stock of the child article.
  89. * But we ignore the configured quantities on the child, since quantities can and should be configured on the bundle product.
  90. */
  91. if ($childProduct->get('isCloseout') || $parentCloseOut) {
  92. $stock = (int)$childProduct->get('availableStock');
  93. /** @var WbfkProductExtensionEntity $wpe */
  94. if ($wpe = $childProduct->getExtension('WbfkProductExtension')) {
  95. $stock = $wpe->getSuppliersTotalStock();
  96. }
  97. // Also consider if this child is multiple times in the bundle
  98. $maxPurchase[] = floor($stock / $bundleProduct->getQuantity());
  99. }
  100. }
  101. $scProduct->setCalculatedMaxPurchase((int)min($maxPurchase));
  102. }
  103. }
  104. public function onProductCriteriaLoaded(ProductPageCriteriaEvent $event): void
  105. {
  106. $event->getCriteria()->addAssociation('wbfk_bundle_product');
  107. }
  108. public function onProductPageLoaded(ProductPageLoadedEvent $event): void
  109. {
  110. $product = $event->getPage()->getProduct();
  111. if (!$product->getExtension('wbfk_bundle_product')->count()) {
  112. return;
  113. }
  114. $this->bundleProductDeliveryAndAvailabilityCalculator->calculatedDeliveryAndAvailability($product, $event->getSalesChannelContext());
  115. }
  116. public function queueBundleParentPriceCalculation(EntityWrittenEvent $event): void
  117. {
  118. // Only queue the price calculation of bundle parents, if the Price Machine Plugin is installed.
  119. if (!$this->productPriceCalculationService) {
  120. return;
  121. }
  122. $bundleParentIdSql = '
  123. SELECT
  124. LOWER(HEX(wbp.product_id)) AS parent_product_id
  125. FROM wbfk_bundle_product wbp
  126. WHERE LOWER(HEX(wbp.child_product_id)) = :childProductId
  127. AND LOWER(HEX(wbp.product_version_id)) = :versionId
  128. AND LOWER(HEX(wbp.child_product_version_id)) = :versionId
  129. GROUP BY wbp.product_id';
  130. foreach ($event->getWriteResults() as $entityWriteResult) {
  131. $bundleChildProductId = $entityWriteResult->getPrimaryKey();
  132. $bundleParentIdResults = $this->connection->fetchAllAssociative($bundleParentIdSql, [
  133. 'childProductId' => $bundleChildProductId,
  134. 'versionId' => Defaults::LIVE_VERSION,
  135. ]);
  136. foreach ($bundleParentIdResults as $bundleParentIdResult) {
  137. $this->productPriceCalculationService->queueProductPriceCalculation($bundleParentIdResult['parent_product_id']);
  138. }
  139. }
  140. }
  141. /**
  142. * @throws Exception
  143. */
  144. public function updateBundleCloseoutStatus(EntityWrittenEvent $event): void
  145. {
  146. $closeoutUpdatedProductIds = array_reduce($event->getPayloads(), function (array $closeOutUpdatedProductIds, array $payload) {
  147. if (isset($payload['isCloseout'])) {
  148. $closeOutUpdatedProductIds[] = $payload['id'];
  149. }
  150. return $closeOutUpdatedProductIds;
  151. }, []);
  152. $bundleChildrenIds = $this->bundleFilter->filterBundleChildrenIds($closeoutUpdatedProductIds, $event->getContext());
  153. $bundleIds = $this->bundleFilter->findParentIds($bundleChildrenIds, $event->getContext());
  154. $this->bundleStockUpdater->updateBundleStockAndAvailability($bundleIds, $event->getContext());
  155. }
  156. }