custom/plugins/Neon6Configurator/src/Services/ConfigurationLoader.php line 125

Open in your IDE?
  1. <?php
  2. namespace Neon\Configurator\Services;
  3. use Neon\Configurator\Core\Content\Calculation\CalculationEntity;
  4. use Neon\Configurator\Core\Content\Configuration\ConfigurationEntity;
  5. use Neon\Configurator\Core\Content\Field\FieldEntity;
  6. use Neon\Configurator\Core\Content\Fieldgroup\FieldgroupEntity;
  7. use Neon\Configurator\Core\Content\Fieldvalidation\FieldvalidationEntity;
  8. use Neon\Configurator\Core\Content\Fieldvalue\FieldvalueCollection;
  9. use Neon\Configurator\Core\Content\Fieldvalue\FieldvalueEntity;
  10. use Neon\Configurator\Core\Content\Info\InfoEntity;
  11. use Neon\Configurator\Neon6Configurator;
  12. use Neon\Configurator\Services\Calculators\CalculationContextFactory;
  13. use Neon\Configurator\Services\Calculators\CalculatorService;
  14. use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
  15. use Shopware\Core\Content\Category\Service\CategoryBreadcrumbBuilder;
  16. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  17. use Shopware\Core\Content\Property\PropertyGroupEntity;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  24. use Shopware\Core\Framework\Struct\ArrayStruct;
  25. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  26. class ConfigurationLoader
  27. {
  28.     private EntityRepository $configurationRepository;
  29.     private EntityRepository $commoncalculationRepository;
  30.     private EntityRepository $fieldRepository;
  31.     private TwigRenderer $twigRenderer;
  32.     private CategoryBreadcrumbBuilder $breadcrumbBuilder;
  33.     private ProductMediaService $productMediaService;
  34.     private $productRepository;
  35.     private $productStreamBuilder;
  36.     private CalculatorService $calculationService;
  37.     private CalculationContextFactory $calculationContextFactory;
  38.     public function __construct($configurationRepository,
  39.                                 $fieldRepository,
  40.                                 $commoncalculationRepository,
  41.                                 $twigRenderer,
  42.                                 $breadcrumbBuilder,
  43.                                 $productMediaService,
  44.                                 $productRepository,
  45.                                 $productStreamBuilder,
  46.                                 $calculationService,
  47.                                 Calculators\CalculationContextFactory $calculationContextFactory)
  48.     {
  49.         $this->configurationRepository $configurationRepository;
  50.         $this->fieldRepository $fieldRepository;
  51.         $this->commoncalculationRepository $commoncalculationRepository;
  52.         $this->twigRenderer $twigRenderer;
  53.         $this->breadcrumbBuilder $breadcrumbBuilder;
  54.         $this->productMediaService $productMediaService;
  55.         $this->productRepository $productRepository;
  56.         $this->productStreamBuilder $productStreamBuilder;
  57.         $this->calculationService $calculationService;
  58.         $this->calculationContextFactory $calculationContextFactory;
  59.     }
  60.     /**
  61.      * @param SalesChannelProductEntity $product
  62.      * @param SalesChannelContext $context
  63.      * @return ConfigurationEntity|null
  64.      * @throws \Twig\Error\LoaderError
  65.      * @throws \Twig\Error\SyntaxError
  66.      */
  67.     public function loadFullConfigurationByProduct(SalesChannelProductEntity $productSalesChannelContext $context$skipMedia false)
  68.     {
  69.         return $this->load(function ($configId$product$context$skipMedia) {
  70.             return $this->loadFullConfiguration($configId$product$context$skipMedia);
  71.         }, $product$context$skipMedia);
  72.     }
  73.     public function loadFullConfigurationByProductId(string $productId$context)
  74.     {
  75.         $product $this->productRepository->search(new Criteria([$productId]), $context)->first();
  76.         return $this->loadFullConfigurationByProduct($product$context);
  77.     }
  78.     /**
  79.      * @param SalesChannelProductEntity $product
  80.      * @param BeforeLineItemAddedEvent $event
  81.      * @return ConfigurationEntity
  82.      */
  83.     public function loadWithFields(SalesChannelProductEntity $productSalesChannelContext $context)
  84.     {
  85.         $this->setExtensions($product);
  86.         return $this->load(function ($configId$product$context) {
  87.             $criteria = new Criteria([$configId]);
  88.             $criteria->addAssociations(['commoncalculations.calculation']);
  89.             $criteria->addAssociations(['fieldgroups']);
  90.             $criteria->addAssociations(['fieldgroups.fields']);
  91.             $criteria->addAssociations(['fieldgroups.fields.values']);
  92.             $criteria->addAssociations(['fieldgroups.fields.validations']);
  93.             $configuration $this->configurationRepository->search($criteria$context->getContext())->first();
  94.             if($configuration !== null) {
  95.                 $this->manuallyAddOneToOneRelationships($configuration$context);
  96.             }
  97.             return $configuration;
  98.         }, $product$context);
  99.     }
  100.     public function loadWithoutAssociations(SalesChannelProductEntity $productSalesChannelContext $context)
  101.     {
  102.         return $this->load(function ($configId$product$context) {
  103.             $criteria = new Criteria([$configId]);
  104.             return $this->configurationRepository->search($criteria$context->getContext())->first();
  105.         }, $product$context);
  106.     }
  107.     /**
  108.      * @param $configId
  109.      * @param SalesChannelProductEntity $product
  110.      * @param $context
  111.      * @return mixed
  112.      * @throws \Twig\Error\LoaderError
  113.      * @throws \Twig\Error\SyntaxError
  114.      */
  115.     public function loadFullConfiguration($configId$productSalesChannelContext $context$skipMedia false)
  116.     {
  117.         $this->setExtensions($product);
  118.         $configuration null;
  119.         if ($configId !== null) {
  120.             $criteria = new Criteria([$configId]);
  121.             $criteria->addAssociations([
  122.                 'commoncalculations'
  123.                 'commoncalculations.calculation'
  124.                 'fieldgroups.fields'
  125.                 'fieldgroups.fields.calculations'
  126.                 'fieldgroups.fields.calculations.calculation'
  127.                 'fieldgroups.fields.children'
  128.                 'fieldgroups.fields.children.validations'
  129.                 'fieldgroups.fields.children.values'
  130.                 'fieldgroups.fields.children.values.media'
  131.                 'fieldgroups.fields.children.visibilities'
  132.                 'fieldgroups.fields.validations'
  133.                 'fieldgroups.fields.values'
  134.                 'fieldgroups.fields.values.product'
  135.                 'fieldgroups.fields.values.media'
  136.                 'fieldgroups.fields.values.fieldvalueInfos'
  137.                 'fieldgroups.fields.visibilities'
  138.                 'fieldgroups.fields.productStream'
  139.                 'fieldgroups.fields.fieldInfos'
  140.                 'medias'
  141.                 'medias.media'
  142.                 'medias.visibilityCalculation'
  143.             ]);
  144.             $criteria->getAssociation('commoncalculations')
  145.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  146.             $criteria->getAssociation('fieldgroups')
  147.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  148.             $criteria->getAssociation('fieldgroups.fields.children')
  149.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING
  150.                 ));
  151.             $criteria->getAssociation('fieldgroups.fields')
  152.                 ->addFilter(new EqualsFilter('parentId'null))
  153.                 ->addFilter(new EqualsFilter('isActive'1))
  154.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  155.             $criteria->getAssociation('fieldgroups.fields.values')
  156.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  157.             $criteria->getAssociation('medias')
  158.                 ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  159.             /** @var ConfigurationEntity $configuration */
  160.             $configuration $this->configurationRepository->search($criteria$context->getContext())->get($configId);
  161.             if ($configuration != null) {
  162.                 $this->manuallyAddOneToOneRelationships($configuration$context);
  163.                 $configuration->setCheapestPrice($this->calculationService->calculate($configuration->getCheapestPriceCalculation(), $this->calculationContextFactory->createDefault($product$configuration$context)));
  164.                 if (!$skipMedia) {
  165.                     $initialVisibleMedias $this->productMediaService->getVisibleMediasByDefaultFieldvalues($product$configuration$context);
  166.                     $initialVisibleMedias array_map(function ($media) {
  167.                         return $media->getMedia();
  168.                     }, $initialVisibleMedias);
  169.                     $configuration->setInitialVisibleMedias($initialVisibleMedias);
  170.                 }
  171.                 foreach ($configuration->getFields() as $field) {
  172.                     if ($field->getSelectValueMode() == 'productStream') {
  173.                         $field->setValues($this->getValuesFromProductStream($field$context));
  174.                     }
  175.                     if ($field->getSelectValueMode() == 'csv') {
  176.                         $field->setValues($this->getValuesFromCsv($field$context));
  177.                     }
  178.                 }
  179.                 $this->renderTwigTemplates($configuration$product$context);
  180.             }
  181.         }
  182.         return $configuration;
  183.     }
  184.     private function load($loaderSalesChannelProductEntity $productSalesChannelContext $context$skipMedia false)
  185.     {
  186.         $config $this->getAlreadyLoadedConfig($product$context);
  187.         if ($config == null) {
  188.             $configId $this->getConfigId($product$context);
  189.             if ($configId != null) {
  190.                 $config $loader($configId$product$context$skipMedia);
  191.             }
  192.         }
  193.         return $config;
  194.     }
  195.     protected function renderTwigTemplates(ConfigurationEntity $configurationSalesChannelProductEntity $product$context)
  196.     {
  197.         foreach (ConfigurationEntity::TRANSLATABLE_FIELDS as $fieldName) {
  198.             $configuration->$fieldName $this->twigRenderer->render($configuration->$fieldName$product);
  199.         }
  200.         foreach ($configuration->getFieldgroups() as $fieldgroup) {
  201.             foreach (FieldgroupEntity::TRANSLATABLE_FIELDS as $fieldName) {
  202.                 $fieldgroup->$fieldName $this->twigRenderer->render($fieldgroup->$fieldName$product);
  203.             }
  204.             foreach ($fieldgroup->getFields() as $field) {
  205.                 if (array_key_exists('commoncalculation'$field->getOptions())) {
  206.                     $commoncalculationId $field->getOptions()['commoncalculation'];
  207.                     $c = new Criteria([$commoncalculationId]);
  208.                     $c->addAssociation('calculation');
  209.                     $options $field->getOptions();
  210.                     $options['commoncalculation'] = $this->commoncalculationRepository->search($c$context->getContext())->get($commoncalculationId);
  211.                     $field->setOptions($options);
  212.                 }
  213.                 foreach ($field->getValidations() as $validation) {
  214.                     foreach (FieldvalidationEntity::TRANSLATABLE_FIELDS as $fieldName) {
  215.                         $renderedResult $this->twigRenderer->render($validation->$fieldName$product);
  216.                         $validation->$fieldName $renderedResult;
  217.                     }
  218.                 }
  219.                 foreach ($field->getFieldInfos() as $fieldInfo) {
  220.                     foreach (InfoEntity::TRANSLATABLE_FIELDS as $fieldName) {
  221.                         $renderedResult $this->twigRenderer->render($fieldInfo->getInfo()->$fieldName$product);
  222.                         $fieldInfo->getInfo()->$fieldName $renderedResult;
  223.                     }
  224.                 }
  225.                 foreach (FieldEntity::TRANSLATABLE_FIELDS as $fieldName) {
  226.                     $field->$fieldName $this->twigRenderer->render($field->$fieldName$product);
  227.                 }
  228.                 /** @var FieldvalueEntity $value */
  229.                 foreach ($field->getValues() as $value) {
  230.                     $linkedProduct $value->getHasProduct()
  231.                         ? $this->productRepository->search(new Criteria([$value->getProduct()->getId()]), $context)->first()
  232.                         : null;
  233.                     if ($linkedProduct != null) {
  234.                         $additional = [
  235.                             'linkedProduct' => [
  236.                                 'name' => $linkedProduct->getTranslation('name'),
  237.                                 'price' => $linkedProduct->getCalculatedPrice()->getUnitPrice()
  238.                             ]];
  239.                         if ($linkedProduct->getIsCloseout() && $linkedProduct->getStock() <= 0) {
  240.                             if($value->getVisibilityCalculation() == null) {
  241.                                 $value->setVisibilityCalculation(new CalculationEntity());
  242.                             }
  243.                             $value->getVisibilityCalculation()->setType('simple');
  244.                             $value->getVisibilityCalculation()->setStringFormula('false');
  245.                         }
  246.                     }
  247.                     foreach (FieldvalueEntity::TRANSLATABLE_FIELDS as $fieldName) {
  248.                         $value->$fieldName $this->twigRenderer->render($value->$fieldName$product$additional ?? []);
  249.                     }
  250.                     $options $value->options;
  251.                     if ($options != null && array_key_exists('priceFormat'$options)) {
  252.                         $options['priceFormat'] = $this->twigRenderer->render($options['priceFormat'], $product, ['price' => $value->getPrice()]);
  253.                         $value->options $options;
  254.                     }
  255.                 }
  256.             }
  257.         }
  258.     }
  259.     /**
  260.      * @param SalesChannelProductEntity $product
  261.      * @return mixed|null
  262.      */
  263.     private function getConfigId(SalesChannelProductEntity $product$context)
  264.     {
  265.         $configId null;
  266.         if (array_key_exists('customFields'$product->getTranslated())
  267.             && array_key_exists(Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG$product->getTranslated()['customFields'])){
  268.             $configId $product->getTranslated()['customFields'][Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG];
  269.         }else {
  270.             if ($product->getSeoCategory() == null) {
  271.                 try {
  272.                     $productSeoCategory $this->breadcrumbBuilder->getProductSeoCategory($product$context);
  273.                     $product->setSeoCategory($productSeoCategory);
  274.                 } catch (\Exception $e) {
  275.                 }
  276.             }
  277.             if ($product->getSeoCategory() !== null && $product->getSeoCategory()->getCustomFields() != null && array_key_exists(Neon6Configurator::CUSTOMFIELD_CATEGORIES_CONFIG$product->getSeoCategory()->getCustomFields())) {
  278.                 $configId $product->getSeoCategory()->getCustomFields()[Neon6Configurator::CUSTOMFIELD_CATEGORIES_CONFIG];
  279.             }
  280.         }
  281.         return $configId;
  282.     }
  283.     private function getAlreadyLoadedConfig(SalesChannelProductEntity $product$context)
  284.     {
  285.         if (array_key_exists('customFields'$product->getTranslated())
  286.             && array_key_exists(Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG$product->getTranslated()['customFields'])){
  287.             $config $product->getTranslated()['customFields'][Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG];
  288.             if ($config instanceof ConfigurationEntity) {
  289.                 return $config;
  290.             }
  291.         }
  292.         return null;
  293.     }
  294.     private function setExtensions(SalesChannelProductEntity $product)
  295.     {
  296.         $product->addExtension('neon_configurator', new ArrayStruct(['properties' => $this->getProperties($product)]));
  297.     }
  298.     /**
  299.      * @param SalesChannelProductEntity $product
  300.      * @return array
  301.      */
  302.     private function getProperties(SalesChannelProductEntity $product): array
  303.     {
  304.         $properties = [];
  305.         if ($product->getSortedProperties() != null) {
  306.             /** @var PropertyGroupEntity $property */
  307.             foreach ($product->getSortedProperties() as $property) {
  308.                 $properties[$property->getName()] = $property->getOptions()->first()->getName();
  309.             }
  310.         }
  311.         return $properties;
  312.     }
  313.     /**
  314.      * @param ConfigurationEntity $configuration
  315.      * @param SalesChannelContext $context
  316.      * @return void
  317.      */
  318.     private function manuallyAddOneToOneRelationships(ConfigurationEntity $configurationSalesChannelContext $context): void
  319.     {
  320. //        if (Feature::isActive('FEATURE_NEXT_22262')) {
  321. //            return;
  322. //        }
  323.         if ($configuration->getQuantityFieldId()) {
  324.             $configuration->setQuantityField($this->fieldRepository->search(new Criteria([$configuration->getQuantityFieldId()]), $context->getContext())->get($configuration->getQuantityFieldId()));
  325.         }
  326.         if ($configuration->getBlockpriceQuantityFieldId()) {
  327.             $configuration->setBlockpriceQuantityField($this->fieldRepository->search(new Criteria([$configuration->getBlockpriceQuantityFieldId()]), $context->getContext())->get($configuration->getBlockpriceQuantityField()));
  328.         }
  329.     }
  330.     /**
  331.      * @param FieldEntity $field
  332.      * @param SalesChannelContext $context
  333.      * @return FieldvalueCollection
  334.      */
  335.     private function getValuesFromProductStream(FieldEntity $fieldSalesChannelContext $context): FieldvalueCollection
  336.     {
  337.         $filters $this->productStreamBuilder->buildFilters(
  338.             $field->getProductStreamId(),
  339.             $context->getContext()
  340.         );
  341.         $criteria = new Criteria();
  342.         $criteria->addFilter(...$filters);
  343.         $criteria->addAssociation('options.group');
  344.         // Ensure storefront presentation settings of product variants
  345.         $criteria->addGroupField(new FieldGrouping('displayGroup'));
  346.         $criteria->addFilter(
  347.             new NotFilter(
  348.                 NotFilter::CONNECTION_AND,
  349.                 [new EqualsFilter('displayGroup'null)]
  350.             )
  351.         );
  352.         $productsOfStream $this->productRepository->search($criteria$context);
  353.         $values = new FieldvalueCollection();
  354.         foreach ($productsOfStream as $productOfStream) {
  355.             $value = new FieldvalueEntity();
  356.             $value->setId($productOfStream->getId());
  357.             $value->setIsDefault(false);
  358.             $value->setHasProduct(true);
  359.             $value->setProductId($productOfStream->getId());
  360.             $value->setProduct($productOfStream);
  361.             $value->setUniqueIdentifier($productOfStream->getId());
  362.             $value->setLabel($productOfStream->getTranslation('name'));
  363.             $value->setValue($productOfStream->getId());
  364.             $value->setPrice('{{linkedProduct.price}}');
  365.             if ($productOfStream->getCover() !== null) {
  366.                 $value->setMedia($productOfStream->getCover()->getMedia());
  367.             }
  368.             $value->setOptions([
  369.                 'productCartMode' => $field->getProductCartMode(),
  370.                 'priceDisplay' => $field->getOptions()['priceDisplay'] ?? 'absolute',
  371.                 'imageSourceType' => 'media',
  372.                 'priceFormat' => $field->getOptions()['priceFormat'],
  373.             ]);
  374.             $values->add($value);
  375.         }
  376.         return $values;
  377.     }
  378.     private function getValuesFromCsv(FieldEntity $fieldSalesChannelContext $context)
  379.     {
  380.         $values = new FieldvalueCollection();
  381.         $rows explode(PHP_EOL,$field->getCsvValues());
  382.         $headerColumns $field->getCsvColumns();
  383.         foreach($rows as $row) {
  384.             $columns explode(";"$row);
  385.             $value = new FieldvalueEntity();
  386.             $value->setId(sha1($row));
  387.             $value->setUniqueIdentifier(sha1($row));
  388.             $value->setIsDefault(false);
  389.             $value->setHasProduct(false);
  390.             if(array_search('value'$headerColumns) !== false) {
  391.                 dump(array_search('value'$headerColumns));
  392.                 $value->setValue($columns[array_search('value'$headerColumns)]);
  393.             }
  394.             if(array_search('label'$headerColumns) !== false) {
  395.                 $value->setLabel($columns[array_search('label'$headerColumns)]);
  396.             }
  397.             if(array_search('price'$headerColumns) !== false) {
  398.                 $value->setPrice($columns[array_search('price'$headerColumns)]);
  399.             }
  400.             if(array_search('isDefault'$headerColumns) !== false) {
  401.                 $value->setIsDefault($columns[array_search('isDefault'$headerColumns)]);
  402.             }
  403.             $options = [
  404.                 'imageSourceType' => 'media'
  405.             ];
  406.             if(array_search('priceDisplay'$headerColumns) !== false) {
  407.                 $options['priceDisplay'] = $columns[array_search('priceDisplay'$headerColumns)];
  408.             }
  409.             $value->setOptions($options);
  410.             $values->add($value);
  411.         }
  412.         return $values;
  413.     }
  414. }