<?php
namespace Neon\Configurator\Services;
use Neon\Configurator\Core\Content\Calculation\CalculationEntity;
use Neon\Configurator\Core\Content\Configuration\ConfigurationEntity;
use Neon\Configurator\Core\Content\Field\FieldEntity;
use Neon\Configurator\Core\Content\Fieldgroup\FieldgroupEntity;
use Neon\Configurator\Core\Content\Fieldvalidation\FieldvalidationEntity;
use Neon\Configurator\Core\Content\Fieldvalue\FieldvalueCollection;
use Neon\Configurator\Core\Content\Fieldvalue\FieldvalueEntity;
use Neon\Configurator\Core\Content\Info\InfoEntity;
use Neon\Configurator\Neon6Configurator;
use Neon\Configurator\Services\Calculators\CalculationContextFactory;
use Neon\Configurator\Services\Calculators\CalculatorService;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
use Shopware\Core\Content\Category\Service\CategoryBreadcrumbBuilder;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Content\Property\PropertyGroupEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class ConfigurationLoader
{
private EntityRepository $configurationRepository;
private EntityRepository $commoncalculationRepository;
private EntityRepository $fieldRepository;
private TwigRenderer $twigRenderer;
private CategoryBreadcrumbBuilder $breadcrumbBuilder;
private ProductMediaService $productMediaService;
private $productRepository;
private $productStreamBuilder;
private CalculatorService $calculationService;
private CalculationContextFactory $calculationContextFactory;
public function __construct($configurationRepository,
$fieldRepository,
$commoncalculationRepository,
$twigRenderer,
$breadcrumbBuilder,
$productMediaService,
$productRepository,
$productStreamBuilder,
$calculationService,
Calculators\CalculationContextFactory $calculationContextFactory)
{
$this->configurationRepository = $configurationRepository;
$this->fieldRepository = $fieldRepository;
$this->commoncalculationRepository = $commoncalculationRepository;
$this->twigRenderer = $twigRenderer;
$this->breadcrumbBuilder = $breadcrumbBuilder;
$this->productMediaService = $productMediaService;
$this->productRepository = $productRepository;
$this->productStreamBuilder = $productStreamBuilder;
$this->calculationService = $calculationService;
$this->calculationContextFactory = $calculationContextFactory;
}
/**
* @param SalesChannelProductEntity $product
* @param SalesChannelContext $context
* @return ConfigurationEntity|null
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\SyntaxError
*/
public function loadFullConfigurationByProduct(SalesChannelProductEntity $product, SalesChannelContext $context, $skipMedia = false)
{
return $this->load(function ($configId, $product, $context, $skipMedia) {
return $this->loadFullConfiguration($configId, $product, $context, $skipMedia);
}, $product, $context, $skipMedia);
}
public function loadFullConfigurationByProductId(string $productId, $context)
{
$product = $this->productRepository->search(new Criteria([$productId]), $context)->first();
return $this->loadFullConfigurationByProduct($product, $context);
}
/**
* @param SalesChannelProductEntity $product
* @param BeforeLineItemAddedEvent $event
* @return ConfigurationEntity
*/
public function loadWithFields(SalesChannelProductEntity $product, SalesChannelContext $context)
{
$this->setExtensions($product);
return $this->load(function ($configId, $product, $context) {
$criteria = new Criteria([$configId]);
$criteria->addAssociations(['commoncalculations.calculation']);
$criteria->addAssociations(['fieldgroups']);
$criteria->addAssociations(['fieldgroups.fields']);
$criteria->addAssociations(['fieldgroups.fields.values']);
$criteria->addAssociations(['fieldgroups.fields.validations']);
$configuration = $this->configurationRepository->search($criteria, $context->getContext())->first();
if($configuration !== null) {
$this->manuallyAddOneToOneRelationships($configuration, $context);
}
return $configuration;
}, $product, $context);
}
public function loadWithoutAssociations(SalesChannelProductEntity $product, SalesChannelContext $context)
{
return $this->load(function ($configId, $product, $context) {
$criteria = new Criteria([$configId]);
return $this->configurationRepository->search($criteria, $context->getContext())->first();
}, $product, $context);
}
/**
* @param $configId
* @param SalesChannelProductEntity $product
* @param $context
* @return mixed
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\SyntaxError
*/
public function loadFullConfiguration($configId, $product, SalesChannelContext $context, $skipMedia = false)
{
$this->setExtensions($product);
$configuration = null;
if ($configId !== null) {
$criteria = new Criteria([$configId]);
$criteria->addAssociations([
'commoncalculations'
, 'commoncalculations.calculation'
, 'fieldgroups.fields'
, 'fieldgroups.fields.calculations'
, 'fieldgroups.fields.calculations.calculation'
, 'fieldgroups.fields.children'
, 'fieldgroups.fields.children.validations'
, 'fieldgroups.fields.children.values'
, 'fieldgroups.fields.children.values.media'
, 'fieldgroups.fields.children.visibilities'
, 'fieldgroups.fields.validations'
, 'fieldgroups.fields.values'
, 'fieldgroups.fields.values.product'
, 'fieldgroups.fields.values.media'
, 'fieldgroups.fields.values.fieldvalueInfos'
, 'fieldgroups.fields.visibilities'
, 'fieldgroups.fields.productStream'
, 'fieldgroups.fields.fieldInfos'
, 'medias'
, 'medias.media'
, 'medias.visibilityCalculation'
]);
$criteria->getAssociation('commoncalculations')
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->getAssociation('fieldgroups')
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->getAssociation('fieldgroups.fields.children')
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING
));
$criteria->getAssociation('fieldgroups.fields')
->addFilter(new EqualsFilter('parentId', null))
->addFilter(new EqualsFilter('isActive', 1))
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->getAssociation('fieldgroups.fields.values')
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->getAssociation('medias')
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
/** @var ConfigurationEntity $configuration */
$configuration = $this->configurationRepository->search($criteria, $context->getContext())->get($configId);
if ($configuration != null) {
$this->manuallyAddOneToOneRelationships($configuration, $context);
$configuration->setCheapestPrice($this->calculationService->calculate($configuration->getCheapestPriceCalculation(), $this->calculationContextFactory->createDefault($product, $configuration, $context)));
if (!$skipMedia) {
$initialVisibleMedias = $this->productMediaService->getVisibleMediasByDefaultFieldvalues($product, $configuration, $context);
$initialVisibleMedias = array_map(function ($media) {
return $media->getMedia();
}, $initialVisibleMedias);
$configuration->setInitialVisibleMedias($initialVisibleMedias);
}
foreach ($configuration->getFields() as $field) {
if ($field->getSelectValueMode() == 'productStream') {
$field->setValues($this->getValuesFromProductStream($field, $context));
}
if ($field->getSelectValueMode() == 'csv') {
$field->setValues($this->getValuesFromCsv($field, $context));
}
}
$this->renderTwigTemplates($configuration, $product, $context);
}
}
return $configuration;
}
private function load($loader, SalesChannelProductEntity $product, SalesChannelContext $context, $skipMedia = false)
{
$config = $this->getAlreadyLoadedConfig($product, $context);
if ($config == null) {
$configId = $this->getConfigId($product, $context);
if ($configId != null) {
$config = $loader($configId, $product, $context, $skipMedia);
}
}
return $config;
}
protected function renderTwigTemplates(ConfigurationEntity $configuration, SalesChannelProductEntity $product, $context)
{
foreach (ConfigurationEntity::TRANSLATABLE_FIELDS as $fieldName) {
$configuration->$fieldName = $this->twigRenderer->render($configuration->$fieldName, $product);
}
foreach ($configuration->getFieldgroups() as $fieldgroup) {
foreach (FieldgroupEntity::TRANSLATABLE_FIELDS as $fieldName) {
$fieldgroup->$fieldName = $this->twigRenderer->render($fieldgroup->$fieldName, $product);
}
foreach ($fieldgroup->getFields() as $field) {
if (array_key_exists('commoncalculation', $field->getOptions())) {
$commoncalculationId = $field->getOptions()['commoncalculation'];
$c = new Criteria([$commoncalculationId]);
$c->addAssociation('calculation');
$options = $field->getOptions();
$options['commoncalculation'] = $this->commoncalculationRepository->search($c, $context->getContext())->get($commoncalculationId);
$field->setOptions($options);
}
foreach ($field->getValidations() as $validation) {
foreach (FieldvalidationEntity::TRANSLATABLE_FIELDS as $fieldName) {
$renderedResult = $this->twigRenderer->render($validation->$fieldName, $product);
$validation->$fieldName = $renderedResult;
}
}
foreach ($field->getFieldInfos() as $fieldInfo) {
foreach (InfoEntity::TRANSLATABLE_FIELDS as $fieldName) {
$renderedResult = $this->twigRenderer->render($fieldInfo->getInfo()->$fieldName, $product);
$fieldInfo->getInfo()->$fieldName = $renderedResult;
}
}
foreach (FieldEntity::TRANSLATABLE_FIELDS as $fieldName) {
$field->$fieldName = $this->twigRenderer->render($field->$fieldName, $product);
}
/** @var FieldvalueEntity $value */
foreach ($field->getValues() as $value) {
$linkedProduct = $value->getHasProduct()
? $this->productRepository->search(new Criteria([$value->getProduct()->getId()]), $context)->first()
: null;
if ($linkedProduct != null) {
$additional = [
'linkedProduct' => [
'name' => $linkedProduct->getTranslation('name'),
'price' => $linkedProduct->getCalculatedPrice()->getUnitPrice()
]];
if ($linkedProduct->getIsCloseout() && $linkedProduct->getStock() <= 0) {
if($value->getVisibilityCalculation() == null) {
$value->setVisibilityCalculation(new CalculationEntity());
}
$value->getVisibilityCalculation()->setType('simple');
$value->getVisibilityCalculation()->setStringFormula('false');
}
}
foreach (FieldvalueEntity::TRANSLATABLE_FIELDS as $fieldName) {
$value->$fieldName = $this->twigRenderer->render($value->$fieldName, $product, $additional ?? []);
}
$options = $value->options;
if ($options != null && array_key_exists('priceFormat', $options)) {
$options['priceFormat'] = $this->twigRenderer->render($options['priceFormat'], $product, ['price' => $value->getPrice()]);
$value->options = $options;
}
}
}
}
}
/**
* @param SalesChannelProductEntity $product
* @return mixed|null
*/
private function getConfigId(SalesChannelProductEntity $product, $context)
{
$configId = null;
if (array_key_exists('customFields', $product->getTranslated())
&& array_key_exists(Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG, $product->getTranslated()['customFields'])){
$configId = $product->getTranslated()['customFields'][Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG];
}else {
if ($product->getSeoCategory() == null) {
try {
$productSeoCategory = $this->breadcrumbBuilder->getProductSeoCategory($product, $context);
$product->setSeoCategory($productSeoCategory);
} catch (\Exception $e) {
}
}
if ($product->getSeoCategory() !== null && $product->getSeoCategory()->getCustomFields() != null && array_key_exists(Neon6Configurator::CUSTOMFIELD_CATEGORIES_CONFIG, $product->getSeoCategory()->getCustomFields())) {
$configId = $product->getSeoCategory()->getCustomFields()[Neon6Configurator::CUSTOMFIELD_CATEGORIES_CONFIG];
}
}
return $configId;
}
private function getAlreadyLoadedConfig(SalesChannelProductEntity $product, $context)
{
if (array_key_exists('customFields', $product->getTranslated())
&& array_key_exists(Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG, $product->getTranslated()['customFields'])){
$config = $product->getTranslated()['customFields'][Neon6Configurator::CUSTOMFIELD_PRODUCTS_CONFIG];
if ($config instanceof ConfigurationEntity) {
return $config;
}
}
return null;
}
private function setExtensions(SalesChannelProductEntity $product)
{
$product->addExtension('neon_configurator', new ArrayStruct(['properties' => $this->getProperties($product)]));
}
/**
* @param SalesChannelProductEntity $product
* @return array
*/
private function getProperties(SalesChannelProductEntity $product): array
{
$properties = [];
if ($product->getSortedProperties() != null) {
/** @var PropertyGroupEntity $property */
foreach ($product->getSortedProperties() as $property) {
$properties[$property->getName()] = $property->getOptions()->first()->getName();
}
}
return $properties;
}
/**
* @param ConfigurationEntity $configuration
* @param SalesChannelContext $context
* @return void
*/
private function manuallyAddOneToOneRelationships(ConfigurationEntity $configuration, SalesChannelContext $context): void
{
// if (Feature::isActive('FEATURE_NEXT_22262')) {
// return;
// }
if ($configuration->getQuantityFieldId()) {
$configuration->setQuantityField($this->fieldRepository->search(new Criteria([$configuration->getQuantityFieldId()]), $context->getContext())->get($configuration->getQuantityFieldId()));
}
if ($configuration->getBlockpriceQuantityFieldId()) {
$configuration->setBlockpriceQuantityField($this->fieldRepository->search(new Criteria([$configuration->getBlockpriceQuantityFieldId()]), $context->getContext())->get($configuration->getBlockpriceQuantityField()));
}
}
/**
* @param FieldEntity $field
* @param SalesChannelContext $context
* @return FieldvalueCollection
*/
private function getValuesFromProductStream(FieldEntity $field, SalesChannelContext $context): FieldvalueCollection
{
$filters = $this->productStreamBuilder->buildFilters(
$field->getProductStreamId(),
$context->getContext()
);
$criteria = new Criteria();
$criteria->addFilter(...$filters);
$criteria->addAssociation('options.group');
// Ensure storefront presentation settings of product variants
$criteria->addGroupField(new FieldGrouping('displayGroup'));
$criteria->addFilter(
new NotFilter(
NotFilter::CONNECTION_AND,
[new EqualsFilter('displayGroup', null)]
)
);
$productsOfStream = $this->productRepository->search($criteria, $context);
$values = new FieldvalueCollection();
foreach ($productsOfStream as $productOfStream) {
$value = new FieldvalueEntity();
$value->setId($productOfStream->getId());
$value->setIsDefault(false);
$value->setHasProduct(true);
$value->setProductId($productOfStream->getId());
$value->setProduct($productOfStream);
$value->setUniqueIdentifier($productOfStream->getId());
$value->setLabel($productOfStream->getTranslation('name'));
$value->setValue($productOfStream->getId());
$value->setPrice('{{linkedProduct.price}}');
if ($productOfStream->getCover() !== null) {
$value->setMedia($productOfStream->getCover()->getMedia());
}
$value->setOptions([
'productCartMode' => $field->getProductCartMode(),
'priceDisplay' => $field->getOptions()['priceDisplay'] ?? 'absolute',
'imageSourceType' => 'media',
'priceFormat' => $field->getOptions()['priceFormat'],
]);
$values->add($value);
}
return $values;
}
private function getValuesFromCsv(FieldEntity $field, SalesChannelContext $context)
{
$values = new FieldvalueCollection();
$rows = explode(PHP_EOL,$field->getCsvValues());
$headerColumns = $field->getCsvColumns();
foreach($rows as $row) {
$columns = explode(";", $row);
$value = new FieldvalueEntity();
$value->setId(sha1($row));
$value->setUniqueIdentifier(sha1($row));
$value->setIsDefault(false);
$value->setHasProduct(false);
if(array_search('value', $headerColumns) !== false) {
dump(array_search('value', $headerColumns));
$value->setValue($columns[array_search('value', $headerColumns)]);
}
if(array_search('label', $headerColumns) !== false) {
$value->setLabel($columns[array_search('label', $headerColumns)]);
}
if(array_search('price', $headerColumns) !== false) {
$value->setPrice($columns[array_search('price', $headerColumns)]);
}
if(array_search('isDefault', $headerColumns) !== false) {
$value->setIsDefault($columns[array_search('isDefault', $headerColumns)]);
}
$options = [
'imageSourceType' => 'media'
];
if(array_search('priceDisplay', $headerColumns) !== false) {
$options['priceDisplay'] = $columns[array_search('priceDisplay', $headerColumns)];
}
$value->setOptions($options);
$values->add($value);
}
return $values;
}
}