vendor/shopware/core/Content/Product/SalesChannel/Detail/ProductConfiguratorLoader.php line 42

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\SalesChannel\Detail;
  3. use Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingEntity;
  4. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  5. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
  6. use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionEntity;
  7. use Shopware\Core\Content\Property\PropertyGroupCollection;
  8. use Shopware\Core\Content\Property\PropertyGroupDefinition;
  9. use Shopware\Core\Content\Property\PropertyGroupEntity;
  10. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  14. use Shopware\Core\Framework\Log\Package;
  15. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  16. #[Package('inventory')]
  17. class ProductConfiguratorLoader
  18. {
  19.     /**
  20.      * @internal
  21.      */
  22.     public function __construct(
  23.         private readonly EntityRepository $configuratorRepository,
  24.         private readonly AbstractAvailableCombinationLoader $combinationLoader
  25.     ) {
  26.     }
  27.     /**
  28.      * @throws InconsistentCriteriaIdsException
  29.      */
  30.     public function load(
  31.         SalesChannelProductEntity $product,
  32.         SalesChannelContext $context
  33.     ): PropertyGroupCollection {
  34.         if (!$product->getParentId()) {
  35.             return new PropertyGroupCollection();
  36.         }
  37.         $groups $this->loadSettings($product$context);
  38.         $groups $this->sortSettings($groups$product);
  39.         $combinations $this->combinationLoader->load(
  40.             $product->getParentId(),
  41.             $context->getContext(),
  42.             $context->getSalesChannelId()
  43.         );
  44.         $current $this->buildCurrentOptions($product$groups);
  45.         foreach ($groups as $group) {
  46.             $options $group->getOptions();
  47.             if ($options === null) {
  48.                 continue;
  49.             }
  50.             foreach ($options as $option) {
  51.                 $combinable $this->isCombinable($option$current$combinations);
  52.                 if ($combinable === null) {
  53.                     $options->remove($option->getId());
  54.                     continue;
  55.                 }
  56.                 $option->setGroup(null);
  57.                 $option->setCombinable($combinable);
  58.             }
  59.         }
  60.         return $groups;
  61.     }
  62.     /**
  63.      * @throws InconsistentCriteriaIdsException
  64.      *
  65.      * @return array<string, PropertyGroupEntity>|null
  66.      */
  67.     private function loadSettings(SalesChannelProductEntity $productSalesChannelContext $context): ?array
  68.     {
  69.         $criteria = (new Criteria())->addFilter(
  70.             new EqualsFilter('productId'$product->getParentId() ?? $product->getId())
  71.         );
  72.         $criteria->addAssociation('option.group')
  73.             ->addAssociation('option.media')
  74.             ->addAssociation('media');
  75.         $settings $this->configuratorRepository
  76.             ->search($criteria$context->getContext())
  77.             ->getEntities();
  78.         if ($settings->count() <= 0) {
  79.             return null;
  80.         }
  81.         $groups = [];
  82.         /** @var ProductConfiguratorSettingEntity $setting */
  83.         foreach ($settings as $setting) {
  84.             $option $setting->getOption();
  85.             if ($option === null) {
  86.                 continue;
  87.             }
  88.             $group $option->getGroup();
  89.             if ($group === null) {
  90.                 continue;
  91.             }
  92.             $groupId $group->getId();
  93.             if (isset($groups[$groupId])) {
  94.                 $group $groups[$groupId];
  95.             }
  96.             $groups[$groupId] = $group;
  97.             $options $group->getOptions();
  98.             if ($options === null) {
  99.                 $options = new PropertyGroupOptionCollection();
  100.                 $group->setOptions($options);
  101.             }
  102.             $options->add($option);
  103.             $options->add($option);
  104.             $option->setConfiguratorSetting($setting);
  105.         }
  106.         return $groups;
  107.     }
  108.     /**
  109.      * @param array<string, PropertyGroupEntity>|null $groups
  110.      */
  111.     private function sortSettings(?array $groupsSalesChannelProductEntity $product): PropertyGroupCollection
  112.     {
  113.         if (!$groups) {
  114.             return new PropertyGroupCollection();
  115.         }
  116.         $sorted = [];
  117.         foreach ($groups as $group) {
  118.             if (!$group) {
  119.                 continue;
  120.             }
  121.             if (!$group->getOptions()) {
  122.                 $group->setOptions(new PropertyGroupOptionCollection());
  123.             }
  124.             $sorted[$group->getId()] = $group;
  125.         }
  126.         /** @var PropertyGroupEntity $group */
  127.         foreach ($sorted as $group) {
  128.             $options $group->getOptions();
  129.             if ($options === null) {
  130.                 continue;
  131.             }
  132.             $options->sort(
  133.                 static function (PropertyGroupOptionEntity $aPropertyGroupOptionEntity $b) use ($group) {
  134.                     $configuratorSettingA $a->getConfiguratorSetting();
  135.                     $configuratorSettingB $b->getConfiguratorSetting();
  136.                     if ($configuratorSettingA !== null && $configuratorSettingB !== null
  137.                         && $configuratorSettingA->getPosition() !== $configuratorSettingB->getPosition()) {
  138.                         return $configuratorSettingA->getPosition() <=> $configuratorSettingB->getPosition();
  139.                     }
  140.                     if ($group->getSortingType() === PropertyGroupDefinition::SORTING_TYPE_ALPHANUMERIC) {
  141.                         return strnatcmp((string) $a->getTranslation('name'), (string) $b->getTranslation('name'));
  142.                     }
  143.                     return ($a->getTranslation('position') ?? $a->getPosition() ?? 0) <=> ($b->getTranslation('position') ?? $b->getPosition() ?? 0);
  144.                 }
  145.             );
  146.         }
  147.         $collection = new PropertyGroupCollection($sorted);
  148.         // check if product has an individual sorting configuration for property groups\
  149.         $config $product->getVariantListingConfig();
  150.         if ($config) {
  151.             $config $config->getConfiguratorGroupConfig();
  152.         }
  153.         if (!$config) {
  154.             $collection->sortByPositions();
  155.             return $collection;
  156.         }
  157.         $sortedGroupIds array_column($config'id');
  158.         // ensure all ids are in the array (but only once)
  159.         $sortedGroupIds array_unique(array_merge($sortedGroupIds$collection->getIds()));
  160.         $collection->sortByIdArray($sortedGroupIds);
  161.         return $collection;
  162.     }
  163.     /**
  164.      * @param array<string> $current
  165.      */
  166.     private function isCombinable(
  167.         PropertyGroupOptionEntity $option,
  168.         array $current,
  169.         AvailableCombinationResult $combinations
  170.     ): ?bool {
  171.         unset($current[$option->getGroupId()]);
  172.         $current[] = $option->getId();
  173.         // available with all other current selected options
  174.         if ($combinations->hasCombination($current) && $combinations->isAvailable($current)) {
  175.             return true;
  176.         }
  177.         // available but not with the other current selected options
  178.         if ($combinations->hasOptionId($option->getId())) {
  179.             return false;
  180.         }
  181.         return null;
  182.     }
  183.     /**
  184.      * @return array<int|string, string>
  185.      */
  186.     private function buildCurrentOptions(SalesChannelProductEntity $productPropertyGroupCollection $groups): array
  187.     {
  188.         $keyMap $groups->getOptionIdMap();
  189.         $optionIds $product->getOptionIds() ?? [];
  190.         $current = [];
  191.         if ($product->getOptionIds() === null) {
  192.             return $current;
  193.         }
  194.         foreach ($optionIds as $optionId) {
  195.             $groupId $keyMap[$optionId] ?? null;
  196.             if ($groupId === null) {
  197.                 continue;
  198.             }
  199.             $current[$groupId] = $optionId;
  200.         }
  201.         return $current;
  202.     }
  203. }