src/Core/Content/Product/Cart/ProductLineItemCommandValidator.php line 42

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\Cart;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  5. use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemDefinition;
  6. use Shopware\Core\Content\Product\Exception\ProductLineItemDifferentIdException;
  7. use Shopware\Core\Content\Product\Exception\ProductLineItemInconsistentException;
  8. use Shopware\Core\Defaults;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\SetNullOnDeleteCommand;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
  13. use Shopware\Core\Framework\Log\Package;
  14. use Shopware\Core\Framework\Uuid\Uuid;
  15. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  16. /**
  17.  * @internal
  18.  */
  19. #[Package('inventory')]
  20. class ProductLineItemCommandValidator implements EventSubscriberInterface
  21. {
  22.     /**
  23.      * @internal
  24.      */
  25.     public function __construct(private readonly Connection $connection)
  26.     {
  27.     }
  28.     /**
  29.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  30.      */
  31.     public static function getSubscribedEvents(): array
  32.     {
  33.         return [
  34.             PreWriteValidationEvent::class => 'preValidate',
  35.         ];
  36.     }
  37.     public function preValidate(PreWriteValidationEvent $event): void
  38.     {
  39.         if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
  40.             return;
  41.         }
  42.         $products $this->findProducts($event->getCommands());
  43.         foreach ($event->getCommands() as $command) {
  44.             if ($command->getDefinition()->getClass() !== OrderLineItemDefinition::class) {
  45.                 continue;
  46.             }
  47.             if ($command instanceof SetNullOnDeleteCommand) {
  48.                 continue;
  49.             }
  50.             $payload $command->getPayload();
  51.             $lineItemId Uuid::fromBytesToHex($command->getPrimaryKey()['id']);
  52.             $productIdChanged \array_key_exists('product_id'$payload);
  53.             $referenceIdChanged \array_key_exists('referenced_id'$payload);
  54.             $lineItemPayload = isset($payload['payload']) ? json_decode((string) $payload['payload'], true512\JSON_THROW_ON_ERROR) : [];
  55.             $orderNumberChanged \array_key_exists('productNumber'$lineItemPayload);
  56.             if (!$this->isProduct($products$payload$lineItemId)) {
  57.                 continue;
  58.             }
  59.             $somethingChanged $productIdChanged || $referenceIdChanged || $orderNumberChanged;
  60.             $allChanged $productIdChanged && $referenceIdChanged && $orderNumberChanged;
  61.             // has a field changed?
  62.             if (!$somethingChanged) {
  63.                 continue;
  64.             }
  65.             $productId = isset($payload['product_id']) ? Uuid::fromBytesToHex($payload['product_id']) : null;
  66.             $referenceId $payload['referenced_id'] ?? null;
  67.             if ($productId !== $referenceId) {
  68.                 $event->getExceptions()->add(
  69.                     new ProductLineItemDifferentIdException($lineItemId)
  70.                 );
  71.             }
  72.             // all fields updated? everything is consistent
  73.             if ($allChanged) {
  74.                 continue;
  75.             }
  76.             $event->getExceptions()->add(
  77.                 new ProductLineItemInconsistentException($lineItemId)
  78.             );
  79.         }
  80.     }
  81.     /**
  82.      * @param list<WriteCommand> $commands
  83.      *
  84.      * @return array<string, int>
  85.      */
  86.     private function findProducts(array $commands): array
  87.     {
  88.         $ids array_map(function (WriteCommand $command) {
  89.             if ($command->getDefinition()->getClass() !== OrderLineItemDefinition::class) {
  90.                 return null;
  91.             }
  92.             if ($command instanceof UpdateCommand) {
  93.                 return $command->getPrimaryKey()['id'];
  94.             }
  95.             return null;
  96.         }, $commands);
  97.         $ids array_values(array_filter($ids));
  98.         if (empty($ids)) {
  99.             return [];
  100.         }
  101.         /** @var array<string, int> $products */
  102.         $products \array_flip($this->connection->fetchFirstColumn(
  103.             'SELECT DISTINCT LOWER(HEX(id)) FROM order_line_item WHERE id IN (:ids) AND type = \'product\'',
  104.             ['ids' => $ids],
  105.             ['ids' => Connection::PARAM_STR_ARRAY]
  106.         ));
  107.         return $products;
  108.     }
  109.     /**
  110.      * @param array<string, mixed> $products
  111.      * @param array<string, mixed> $payload
  112.      */
  113.     private function isProduct(array $products, array $payloadstring $lineItemId): bool
  114.     {
  115.         if (isset($payload['type'])) {
  116.             return $payload['type'] === LineItem::PRODUCT_LINE_ITEM_TYPE;
  117.         }
  118.         return isset($products[$lineItemId]);
  119.     }
  120. }