custom/plugins/OkeonlineKejeRemoveProductsHotfix/src/Core/Content/Product/DataAbstractionLayer/StockUpdaterHotfix.php line 102

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. // this class overwrites the default  Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater   to hotfix one small item in the StockUpdater
  3. //  it is used to disable a big query while deleting a product. The original query is created while checking/setting null on orderLineItems.product_id
  4. // @see line 86 for the hotfix
  5. namespace Okeonline\KejeRemoveProductsHotfix\Core\Content\Product\DataAbstractionLayer;
  6. use Doctrine\DBAL\Connection;
  7. use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
  8. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  9. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemDefinition;
  10. use Shopware\Core\Checkout\Order\OrderEvents;
  11. use Shopware\Core\Checkout\Order\OrderStates;
  12. use Shopware\Core\Content\Product\DataAbstractionLayer\StockUpdater;
  13. use Shopware\Core\Content\Product\Events\ProductNoLongerAvailableEvent;
  14. use Shopware\Core\Defaults;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\RetryableQuery;
  17. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\ChangeSetAware;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  25. use Shopware\Core\Framework\Uuid\Uuid;
  26. use Shopware\Core\Profiling\Profiler;
  27. use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
  28. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  29. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  30. class StockUpdaterHotfix extends StockUpdater implements EventSubscriberInterface
  31. {
  32.     private Connection $connection;
  33.     private EventDispatcherInterface $dispatcher;
  34.     /**
  35.      * @internal
  36.      */
  37.     public function __construct(
  38.         Connection $connection,
  39.         EventDispatcherInterface $dispatcher
  40.     ) {
  41.         $this->connection $connection;
  42.         $this->dispatcher $dispatcher;
  43.     }
  44.     /**
  45.      * Returns a list of custom business events to listen where the product maybe changed
  46.      *
  47.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  48.      */
  49.     public static function getSubscribedEvents()
  50.     {
  51.         return [
  52.             CheckoutOrderPlacedEvent::class => 'orderPlaced',
  53.             StateMachineTransitionEvent::class => 'stateChanged',
  54.             PreWriteValidationEvent::class => 'triggerChangeSet',
  55.             OrderEvents::ORDER_LINE_ITEM_WRITTEN_EVENT => 'lineItemWritten',
  56.             OrderEvents::ORDER_LINE_ITEM_DELETED_EVENT => 'lineItemWritten',
  57.         ];
  58.     }
  59.     public function triggerChangeSet(PreWriteValidationEvent $event): void
  60.     {
  61.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  62.             return;
  63.         }
  64.         foreach ($event->getCommands() as $command) {
  65.             if (!$command instanceof ChangeSetAware) {
  66.                 continue;
  67.             }
  68.             /** @var ChangeSetAware|InsertCommand|UpdateCommand $command */
  69.             if ($command->getDefinition()->getEntityName() !== OrderLineItemDefinition::ENTITY_NAME) {
  70.                 continue;
  71.             }
  72.             if ($command instanceof InsertCommand) {
  73.                 continue;
  74.             }
  75.             if ($command instanceof DeleteCommand) {
  76.                 $command->requestChangeSet();
  77.                 continue;
  78.             }
  79.             /** @var WriteCommand&ChangeSetAware $command */
  80.             if ($command->hasField('referenced_id') || $command->hasField('product_id') || $command->hasField('quantity')) {
  81.                 // commented the following line as a hotfix
  82.                 // $command->requestChangeSet();
  83.                 dump('hotfix activated');
  84.             }
  85.         }
  86.     }
  87.     public function orderPlaced(CheckoutOrderPlacedEvent $event): void
  88.     {
  89.         $ids = [];
  90.         foreach ($event->getOrder()->getLineItems() as $lineItem) {
  91.             if ($lineItem->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
  92.                 continue;
  93.             }
  94.             if (!\array_key_exists($lineItem->getReferencedId(), $ids)) {
  95.                 $ids[$lineItem->getReferencedId()] = 0;
  96.             }
  97.             $ids[$lineItem->getReferencedId()] += $lineItem->getQuantity();
  98.         }
  99.         // order placed event is a high load event. Because of the high load, we simply reduce the quantity here instead of executing the high costs `update` function
  100.         $query = new RetryableQuery(
  101.             $this->connection,
  102.             $this->connection->prepare('UPDATE product SET available_stock = available_stock - :quantity WHERE id = :id')
  103.         );
  104.         Profiler::trace('order::update-stock', static function () use ($query$ids): void {
  105.             foreach ($ids as $id => $quantity) {
  106.                 $query->execute(['id' => Uuid::fromHexToBytes((string) $id), 'quantity' => $quantity]);
  107.             }
  108.         });
  109.         Profiler::trace('order::update-flag', function () use ($ids$event): void {
  110.             $this->updateAvailableFlag(\array_keys($ids), $event->getContext());
  111.         });
  112.     }
  113.     /**
  114.      * If the product of an order item changed, the stocks of the old product and the new product must be updated.
  115.      */
  116.     public function lineItemWritten(EntityWrittenEvent $event): void
  117.     {
  118.         $ids = [];
  119.         // we don't want to trigger to `update` method when we are inside the order process
  120.         if ($event->getContext()->hasState('checkout-order-route')) {
  121.             return;
  122.         }
  123.         foreach ($event->getWriteResults() as $result) {
  124.             if ($result->hasPayload('referencedId') && $result->getProperty('type') === LineItem::PRODUCT_LINE_ITEM_TYPE) {
  125.                 $ids[] = $result->getProperty('referencedId');
  126.             }
  127.             if ($result->getOperation() === EntityWriteResult::OPERATION_INSERT) {
  128.                 continue;
  129.             }
  130.             $changeSet $result->getChangeSet();
  131.             if (!$changeSet) {
  132.                 continue;
  133.             }
  134.             $type $changeSet->getBefore('type');
  135.             if ($type !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
  136.                 continue;
  137.             }
  138.             if (!$changeSet->hasChanged('referenced_id') && !$changeSet->hasChanged('quantity')) {
  139.                 continue;
  140.             }
  141.             $ids[] = $changeSet->getBefore('referenced_id');
  142.             $ids[] = $changeSet->getAfter('referenced_id');
  143.         }
  144.         $ids array_filter(array_unique($ids));
  145.         if (empty($ids)) {
  146.             return;
  147.         }
  148.         $this->update($ids$event->getContext());
  149.     }
  150.     public function stateChanged(StateMachineTransitionEvent $event): void
  151.     {
  152.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  153.             return;
  154.         }
  155.         if ($event->getEntityName() !== 'order') {
  156.             return;
  157.         }
  158.         if ($event->getToPlace()->getTechnicalName() === OrderStates::STATE_COMPLETED) {
  159.             $this->decreaseStock($event);
  160.             return;
  161.         }
  162.         if ($event->getFromPlace()->getTechnicalName() === OrderStates::STATE_COMPLETED) {
  163.             $this->increaseStock($event);
  164.             return;
  165.         }
  166.         if ($event->getToPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED || $event->getFromPlace()->getTechnicalName() === OrderStates::STATE_CANCELLED) {
  167.             $products $this->getProductsOfOrder($event->getEntityId());
  168.             $ids array_column($products'referenced_id');
  169.             $this->updateAvailableStockAndSales($ids$event->getContext());
  170.             $this->updateAvailableFlag($ids$event->getContext());
  171.             return;
  172.         }
  173.     }
  174.     public function update(array $idsContext $context): void
  175.     {
  176.         if ($context->getVersionId() !== Defaults::LIVE_VERSION) {
  177.             return;
  178.         }
  179.         $this->updateAvailableStockAndSales($ids$context);
  180.         $this->updateAvailableFlag($ids$context);
  181.     }
  182.     private function increaseStock(StateMachineTransitionEvent $event): void
  183.     {
  184.         $products $this->getProductsOfOrder($event->getEntityId());
  185.         $ids array_column($products'referenced_id');
  186.         $this->updateStock($products, +1);
  187.         $this->updateAvailableStockAndSales($ids$event->getContext());
  188.         $this->updateAvailableFlag($ids$event->getContext());
  189.     }
  190.     private function decreaseStock(StateMachineTransitionEvent $event): void
  191.     {
  192.         $products $this->getProductsOfOrder($event->getEntityId());
  193.         $ids array_column($products'referenced_id');
  194.         $this->updateStock($products, -1);
  195.         $this->updateAvailableStockAndSales($ids$event->getContext());
  196.         $this->updateAvailableFlag($ids$event->getContext());
  197.     }
  198.     private function updateAvailableStockAndSales(array $idsContext $context): void
  199.     {
  200.         $ids array_filter(array_keys(array_flip($ids)));
  201.         if (empty($ids)) {
  202.             return;
  203.         }
  204.         $sql '
  205. SELECT LOWER(HEX(order_line_item.product_id)) as product_id,
  206.     IFNULL(
  207.         SUM(IF(state_machine_state.technical_name = :completed_state, 0, order_line_item.quantity)),
  208.         0
  209.     ) as open_quantity,
  210.     IFNULL(
  211.         SUM(IF(state_machine_state.technical_name = :completed_state, order_line_item.quantity, 0)),
  212.         0
  213.     ) as sales_quantity
  214. FROM order_line_item
  215.     INNER JOIN `order`
  216.         ON `order`.id = order_line_item.order_id
  217.         AND `order`.version_id = order_line_item.order_version_id
  218.     INNER JOIN state_machine_state
  219.         ON state_machine_state.id = `order`.state_id
  220.         AND state_machine_state.technical_name <> :cancelled_state
  221. WHERE order_line_item.product_id IN (:ids)
  222.     AND order_line_item.type = :type
  223.     AND order_line_item.version_id = :version
  224.     AND order_line_item.product_id IS NOT NULL
  225. GROUP BY product_id;
  226.         ';
  227.         $rows $this->connection->fetchAllAssociative(
  228.             $sql,
  229.             [
  230.                 'type' => LineItem::PRODUCT_LINE_ITEM_TYPE,
  231.                 'version' => Uuid::fromHexToBytes($context->getVersionId()),
  232.                 'completed_state' => OrderStates::STATE_COMPLETED,
  233.                 'cancelled_state' => OrderStates::STATE_CANCELLED,
  234.                 'ids' => Uuid::fromHexToBytesList($ids),
  235.             ],
  236.             [
  237.                 'ids' => Connection::PARAM_STR_ARRAY,
  238.             ]
  239.         );
  240.         $fallback array_column($rows'product_id');
  241.         $fallback array_diff($ids$fallback);
  242.         $update = new RetryableQuery(
  243.             $this->connection,
  244.             $this->connection->prepare('UPDATE product SET available_stock = stock - :open_quantity, sales = :sales_quantity, updated_at = :now WHERE id = :id')
  245.         );
  246.         foreach ($fallback as $id) {
  247.             $update->execute([
  248.                 'id' => Uuid::fromHexToBytes((string) $id),
  249.                 'open_quantity' => 0,
  250.                 'sales_quantity' => 0,
  251.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  252.             ]);
  253.         }
  254.         foreach ($rows as $row) {
  255.             $update->execute([
  256.                 'id' => Uuid::fromHexToBytes($row['product_id']),
  257.                 'open_quantity' => $row['open_quantity'],
  258.                 'sales_quantity' => $row['sales_quantity'],
  259.                 'now' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  260.             ]);
  261.         }
  262.     }
  263.     private function updateAvailableFlag(array $idsContext $context): void
  264.     {
  265.         $ids array_filter(array_unique($ids));
  266.         if (empty($ids)) {
  267.             return;
  268.         }
  269.         $bytes Uuid::fromHexToBytesList($ids);
  270.         $sql '
  271.             UPDATE product
  272.             LEFT JOIN product parent
  273.                 ON parent.id = product.parent_id
  274.                 AND parent.version_id = product.version_id
  275.             SET product.available = IFNULL((
  276.                 IFNULL(product.is_closeout, parent.is_closeout) * product.available_stock
  277.                 >=
  278.                 IFNULL(product.is_closeout, parent.is_closeout) * IFNULL(product.min_purchase, parent.min_purchase)
  279.             ), 0)
  280.             WHERE product.id IN (:ids)
  281.             AND product.version_id = :version
  282.         ';
  283.         RetryableQuery::retryable($this->connection, function () use ($sql$context$bytes): void {
  284.             $this->connection->executeUpdate(
  285.                 $sql,
  286.                 ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  287.                 ['ids' => Connection::PARAM_STR_ARRAY]
  288.             );
  289.         });
  290.         $updated $this->connection->fetchFirstColumn(
  291.             'SELECT LOWER(HEX(id)) FROM product WHERE available = 0 AND id IN (:ids) AND product.version_id = :version',
  292.             ['ids' => $bytes'version' => Uuid::fromHexToBytes($context->getVersionId())],
  293.             ['ids' => Connection::PARAM_STR_ARRAY]
  294.         );
  295.         if (!empty($updated)) {
  296.             $this->dispatcher->dispatch(new ProductNoLongerAvailableEvent($updated$context));
  297.         }
  298.     }
  299.     private function updateStock(array $productsint $multiplier): void
  300.     {
  301.         $query = new RetryableQuery(
  302.             $this->connection,
  303.             $this->connection->prepare('UPDATE product SET stock = stock + :quantity WHERE id = :id AND version_id = :version')
  304.         );
  305.         foreach ($products as $product) {
  306.             $query->execute([
  307.                 'quantity' => (int) $product['quantity'] * $multiplier,
  308.                 'id' => Uuid::fromHexToBytes($product['referenced_id']),
  309.                 'version' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  310.             ]);
  311.         }
  312.     }
  313.     private function getProductsOfOrder(string $orderId): array
  314.     {
  315.         $query $this->connection->createQueryBuilder();
  316.         $query->select(['referenced_id''quantity']);
  317.         $query->from('order_line_item');
  318.         $query->andWhere('type = :type');
  319.         $query->andWhere('order_id = :id');
  320.         $query->andWhere('version_id = :version');
  321.         $query->setParameter('id'Uuid::fromHexToBytes($orderId));
  322.         $query->setParameter('version'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  323.         $query->setParameter('type'LineItem::PRODUCT_LINE_ITEM_TYPE);
  324.         return $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
  325.     }
  326. }