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

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