custom/plugins/SwkwebProductSet/src/Core/Content/ProductSet/SalesChannel/ProductSetRoute.php line 57

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Swkweb\ProductSet\Core\Content\ProductSet\SalesChannel;
  3. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  4. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  8. use Shopware\Core\Framework\Routing\Annotation\Entity;
  9. use Shopware\Core\Framework\Uuid\Uuid;
  10. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  11. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  12. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetOption\ProductSetOptionEntity;
  13. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetOption\SalesChannel\SalesChannelProductSetOptionEntity;
  14. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetPriceCalculation\ProductSetPriceCalculationDefinition;
  15. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetPriceCalculation\ProductSetPriceCalculationEntity;
  16. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetSlot\ProductSetSlotEntity;
  17. use Swkweb\ProductSet\Core\Content\ProductSet\Aggregate\ProductSetSlot\SalesChannel\SalesChannelProductSetSlotEntity;
  18. use Swkweb\ProductSet\Core\Content\ProductSet\Cart\ProductSetIterationHelper;
  19. use Swkweb\ProductSet\Core\Content\ProductSet\Exception\InvalidProductSetOptionVariantIdException;
  20. use Swkweb\ProductSet\Core\Content\ProductSet\Exception\ProductSetNotFoundException;
  21. use Swkweb\ProductSet\Core\Content\ProductSet\SalesChannel\Price\ProductSetOptionPriceCalculator;
  22. use Symfony\Component\HttpFoundation\Request;
  23. use Symfony\Component\Routing\Annotation\Route;
  24. /**
  25.  * @Route(defaults={"_routeScope"={"store-api"}})
  26.  */
  27. class ProductSetRoute implements ProductSetRouteInterface
  28. {
  29.     private SalesChannelRepositoryInterface $productRepository;
  30.     private SalesChannelRepositoryInterface $productSetRepository;
  31.     private EntityRepositoryInterface $productSetPriceCalculationRepository;
  32.     private ProductSetOptionPriceCalculator $optionPriceCalculator;
  33.     private ProductSetIterationHelper $iterationHelper;
  34.     public function __construct(
  35.         SalesChannelRepositoryInterface $productRepository,
  36.         SalesChannelRepositoryInterface $productSetRepository,
  37.         EntityRepositoryInterface $productSetPriceCalculationRepository,
  38.         ProductSetOptionPriceCalculator $optionPriceCalculator,
  39.         ProductSetIterationHelper $iterationHelper
  40.     ) {
  41.         $this->productRepository $productRepository;
  42.         $this->productSetRepository $productSetRepository;
  43.         $this->productSetPriceCalculationRepository $productSetPriceCalculationRepository;
  44.         $this->optionPriceCalculator $optionPriceCalculator;
  45.         $this->iterationHelper $iterationHelper;
  46.     }
  47.     /**
  48.      * @Entity("swkweb_product_set")
  49.      *
  50.      * @Route("/store-api/swkweb-product-set/{productId}", name="store-api.swkweb-product-set.detail", methods={"POST"})
  51.      */
  52.     public function load(string $productId, ?string $pathRequest $requestSalesChannelContext $contextCriteria $criteria): ProductSetRouteResponse
  53.     {
  54.         $set $this->loadSet($productId$path$context$criteria);
  55.         $this->loadPriceCalculations($set$context);
  56.         $this->loadOptionProducts($productId$set$request$context);
  57.         $this->calculateOptionPrices($set$context);
  58.         return new ProductSetRouteResponse($set$path);
  59.     }
  60.     private function loadSet(string $productId, ?string $pathSalesChannelContext $contextCriteria $criteria): SalesChannelProductSetEntity
  61.     {
  62.         $setPath $this->parsePath($path);
  63.         $criteria->setTitle('swkweb-product-set::product-set-route::load::product-set');
  64.         $slotAssignmentCriteria $criteria->getAssociation('slotAssignments');
  65.         $slotCriteria $slotAssignmentCriteria->getAssociation('slot');
  66.         $optionCriteria $slotCriteria->getAssociation('options');
  67.         $criteria->addFilter(new EqualsFilter('setProducts.productId'$productId));
  68.         if (isset($setPath['slotId'])) {
  69.             $slotAssignmentCriteria->addFilter(new EqualsFilter('id'$setPath['slotId']));
  70.         }
  71.         if (isset($setPath['optionId'])) {
  72.             $optionCriteria->addFilter(new EqualsFilter('id'$setPath['optionId']));
  73.         }
  74.         $slotAssignmentCriteria->addSorting(new FieldSorting('position'));
  75.         $optionCriteria->addSorting(new FieldSorting('position'));
  76.         $productSet $this->productSetRepository->search($criteria$context)->first();
  77.         if (!$productSet instanceof SalesChannelProductSetEntity) {
  78.             throw new ProductSetNotFoundException($productId$path);
  79.         }
  80.         return $productSet;
  81.     }
  82.     private function loadOptionProducts(string $productIdSalesChannelProductSetEntity $setRequest $requestSalesChannelContext $context): void
  83.     {
  84.         $optionProductIds = [];
  85.         foreach ($this->iterationHelper->iterateOptions($set) as $optionPath => $option) {
  86.             if (!$option->getOptionProductId()) {
  87.                 continue;
  88.             }
  89.             $optionProductIds[] = $this->getOptionVariantProductId($optionPath$request) ?? $option->getOptionProductId();
  90.         }
  91.         if (count($optionProductIds) === 0) {
  92.             return;
  93.         }
  94.         $criteria = new Criteria($optionProductIds);
  95.         $criteria->setTitle('swkweb-product-set::product-set-route::load::option-products');
  96.         $criteria->addAssociation('options.group');
  97.         $criteria->addAssociation('configuratorSettings.option.group');
  98.         $result $this->productRepository->search($criteria$context);
  99.         foreach ($this->iterationHelper->iterateSlots($set) as $slotPath => $slot) {
  100.             assert($slot instanceof SalesChannelProductSetSlotEntity);
  101.             foreach ($this->iterationHelper->iterateSlotOptions($slot$slotPath) as $optionPath => $option) {
  102.                 assert($option instanceof SalesChannelProductSetOptionEntity);
  103.                 $optionProductId $this->getOptionVariantProductId($optionPath$request) ?? $option->getOptionProductId();
  104.                 $optionProduct $result->get($optionProductId);
  105.                 if ($optionProductId !== $option->getOptionProductId()
  106.                     && (
  107.                         !$optionProduct instanceof SalesChannelProductEntity
  108.                         || $optionProduct->getId() !== $option->getOptionProductId() && $optionProduct->getParentId() !== $option->getOptionProductId()
  109.                     )
  110.                 ) {
  111.                     throw new InvalidProductSetOptionVariantIdException($productId$optionPath);
  112.                 }
  113.                 if (!$optionProduct instanceof SalesChannelProductEntity) {
  114.                     assert($slot->getOptions() !== null);
  115.                     $slot->getOptions()->remove($option->getId());
  116.                     continue;
  117.                 }
  118.                 $option->setSlot($slot);
  119.                 $option->setOptionProduct($optionProduct);
  120.                 // First calculate the max purchase since the min purchase depends on this value
  121.                 $this->buildOptionCalculatedMaxQuantity($slot$option);
  122.                 $this->buildOptionCalculatedMinQuantity($option);
  123.             }
  124.         }
  125.     }
  126.     private function loadPriceCalculations(SalesChannelProductSetEntity $setSalesChannelContext $context): void
  127.     {
  128.         /**
  129.          * @var list<array{
  130.          *      entity: ProductSetSlotEntity|ProductSetOptionEntity,
  131.          *      priceCalculationId: string,
  132.          * }> $mapping
  133.          */
  134.         $mapping = [];
  135.         foreach ($this->iterationHelper->iterateSlots($set) as $slot) {
  136.             if (empty($slot->getOptions())) {
  137.                 continue;
  138.             }
  139.             $slotPriceCalculationId $slot->getPriceCalculationId() ?? ProductSetPriceCalculationDefinition::DEFAULT;
  140.             $mapping[] = [
  141.                 'entity' => $slot,
  142.                 'priceCalculationId' => $slotPriceCalculationId,
  143.             ];
  144.             foreach ($slot->getOptions() as $option) {
  145.                 $mapping[] = [
  146.                     'entity' => $option,
  147.                     'priceCalculationId' => $option->getPriceCalculationId() ?? $slotPriceCalculationId,
  148.                 ];
  149.             }
  150.         }
  151.         if (count($mapping) === 0) {
  152.             return;
  153.         }
  154.         $criteria = new Criteria(array_unique(array_column($mapping'priceCalculationId')));
  155.         $result $this->productSetPriceCalculationRepository->search($criteria$context->getContext());
  156.         foreach ($mapping as ['entity' => $entity'priceCalculationId' => $priceCalculationId]) {
  157.             $priceCalculation $result->get($priceCalculationId);
  158.             assert($priceCalculation instanceof ProductSetPriceCalculationEntity);
  159.             $entity->setPriceCalculation($priceCalculation);
  160.         }
  161.     }
  162.     private function calculateOptionPrices(SalesChannelProductSetEntity $setSalesChannelContext $context): void
  163.     {
  164.         foreach ($this->iterationHelper->iterateOptions($set) as $option) {
  165.             assert($option instanceof SalesChannelProductSetOptionEntity);
  166.             $this->optionPriceCalculator->calculate($option$context);
  167.         }
  168.     }
  169.     /**
  170.      * @return array{slotId: ?string, optionId: ?string, childPath: ?string}
  171.      */
  172.     private function parsePath(?string $path): array
  173.     {
  174.         $parts explode('/'$path ?? ''3);
  175.         // Remove empty element caused by leading slash
  176.         if ($parts[0] === '') {
  177.             array_shift($parts);
  178.         }
  179.         $slotId $parts[0] ?? '';
  180.         $slotId Uuid::isValid($slotId) ? $slotId null;
  181.         $optionId $parts[1] ?? '';
  182.         $optionId $slotId && Uuid::isValid($optionId) ? $optionId null;
  183.         return [
  184.             'slotId' => $slotId,
  185.             'optionId' => $optionId,
  186.             'childPath' => $parts[2] ?? null,
  187.         ];
  188.     }
  189.     private function getOptionVariantProductId(string $pathRequest $request): ?string
  190.     {
  191.         $optionVariantProductIds $request->get(self::OPTION_VARIANT_PRODUCT_IDS);
  192.         if (!is_array($optionVariantProductIds)) {
  193.             return null;
  194.         }
  195.         return $optionVariantProductIds[$path] ?? null;
  196.     }
  197.     private function buildOptionCalculatedMaxQuantity(
  198.         SalesChannelProductSetSlotEntity $slot,
  199.         SalesChannelProductSetOptionEntity $option
  200.     ): void {
  201.         if (empty($option->getOptionProduct())) {
  202.             $option->setCalculatedMaxQuantity($option->getMaximumQuantity());
  203.             return;
  204.         }
  205.         $maxQuantities = [
  206.             $option->getMaximumQuantity(),
  207.             $option->getOptionProduct()->getCalculatedMaxPurchase(),
  208.         ];
  209.         if ($slot->isLimitAggregatedOptionQuantities()) {
  210.             $maxQuantities[] = $slot->getMaximumSelectedOptions();
  211.         }
  212.         $option->setCalculatedMaxQuantity(min(...$maxQuantities));
  213.     }
  214.     private function buildOptionCalculatedMinQuantity(SalesChannelProductSetOptionEntity $option): void
  215.     {
  216.         // Limit of the aggregated option quantities or the product max purchase is already
  217.         // considered in the calculatedMaxQuantity of the option
  218.         $option->setCalculatedMinQuantity(min(
  219.             $option->getMinimumQuantity(),
  220.             $option->getCalculatedMaxQuantity()
  221.         ));
  222.     }
  223. }