<?php
namespace Maxia\MaxiaListingVariants6\Service;
use Maxia\MaxiaListingVariants6\Core\Content\Product\Cms\ProductListingCmsElementResolver;
use Maxia\MaxiaListingVariants6\Events\ListingVariantsBeforeLoadEvent;
use Monolog\Logger;
use Doctrine\DBAL\Connection;
use Maxia\MaxiaListingVariants6\Config\BaseConfig;
use Maxia\MaxiaListingVariants6\Config\ProductConfig;
use Maxia\MaxiaListingVariants6\Config\PropertyGroupConfig;
use Maxia\MaxiaListingVariants6\Events\ListingVariantsLoadedEvent;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionEntity;
use Shopware\Core\Content\Property\PropertyGroupCollection;
use Shopware\Core\Content\Property\PropertyGroupEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Product\Configurator\ProductPageConfiguratorLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
class ListingVariantsLoader
{
/** @var ContainerInterface */
protected $container;
/** @var Logger */
protected $logger;
/** @var RequestStack */
protected $requestStack;
/** @var EventDispatcherInterface */
protected $eventDispatcher;
/** @var Connection */
protected $dbConnection;
/** @var ProductPageConfiguratorLoader */
protected $configuratorLoader;
/** @var ProductCombinationFinder */
protected $combinationFinder;
/** @var SalesChannelRepositoryInterface */
protected $productRepository;
/** @var EntityRepository */
protected $mediaRepository;
/** @var EntityRepository */
protected $productMediaRepository;
/** @var VariantDisplayConfigLoader */
protected $variantDisplayConfigLoader;
/** @var ConfigService */
protected $configService;
public function __construct(
ContainerInterface $container,
Logger $logger,
RequestStack $requestStack,
EventDispatcherInterface $eventDispatcher,
Connection $dbConnection,
ProductPageConfiguratorLoader $configuratorLoader,
ProductCombinationFinder $combinationFinder,
SalesChannelRepositoryInterface $productRepository,
/*EntityRepository*/ $mediaRepository,
/*EntityRepository*/ $productMediaRepository,
VariantDisplayConfigLoader $variantDisplayConfigLoader,
ConfigService $configService
) {
$this->container = $container;
$this->logger = $logger;
$this->requestStack = $requestStack;
$this->eventDispatcher = $eventDispatcher;
$this->dbConnection = $dbConnection;
$this->configuratorLoader = $configuratorLoader;
$this->combinationFinder = $combinationFinder;
$this->productRepository = $productRepository;
$this->productMediaRepository = $productMediaRepository;
$this->mediaRepository = $mediaRepository;
$this->variantDisplayConfigLoader = $variantDisplayConfigLoader;
$this->configService = $configService;
}
/**
* @param SalesChannelProductEntity[] $products
*/
public function load(array $products, SalesChannelContext $context, $expandOptions = null)
{
$request = $this->getMainRequest();
$pluginConfig = $this->configService->getBaseConfig($context);
if ($expandOptions === null) {
$expandOptions = $request ? $request->query->get('expandOptions', false) : false;
}
// add variants config extension to all products
foreach ($products as $product) {
$isVariantProduct = !empty($product->getParentId()) || $product->getChildCount();
if (!$product->hasExtension('maxiaListingVariants')) {
$config = $this->configService->getProductConfig($product, null, $context);
$product->addExtension('maxiaListingVariants', $config);
}
if (!$isVariantProduct) {
// disable for main products
$config = $product->getExtension('maxiaListingVariants');
$config->setDisplayMode('none');
$config->setQuickBuyActive(
$pluginConfig->isQuickBuyActive() && $pluginConfig->isActivateForMainProducts()
);
}
}
// filter products where no additional data needs to be loaded
$products = array_filter($products, function($product) {
return $product->hasExtension('maxiaListingVariants')
&& $product->getExtension('maxiaListingVariants')->getDisplayMode() !== 'none';
});
if (!$products) {
return;
}
// get display configs / preselection info
$displayConfigs = $this->variantDisplayConfigLoader->load($products, $context);
if ($request && $request->query->get('prependOptions')) {
$prependedOptions = json_decode($request->query->get('prependOptions'), true);
}
// load configurator for each product
foreach ($products as $i => $product) {
/** @var ProductConfig $config */
$config = $product->getExtension('maxiaListingVariants');
$config->setIsExpanded($expandOptions);
if ($config->isExpanded()) {
$this->setExpandedConfig($pluginConfig, $config);
}
if (isset($prependedOptions) && $prependedOptions) {
$config->setPrependedOptions($prependedOptions);
}
$this->eventDispatcher->dispatch(new ListingVariantsBeforeLoadEvent($product, $config, $context));
// load options
if (!$product->getOptions()) {
$product->setOptions(new PropertyGroupOptionCollection());
}
if (!$product->getOptionIds()) {
$product->setOptionIds([]);
}
if (!$product->getParentId()) {
$firstVariant = $this->getFirstVariantProduct($product, $displayConfigs, $context);
if (!$firstVariant) {
continue;
}
$settings = $this->configuratorLoader->load($firstVariant, $context);
} else {
$settings = $this->configuratorLoader->load($product, $context);
}
if ($settings && $settings->count()) {
$config->setTotalGroupCount($settings->count());
$config->setOptions($settings);
$config->setOptions($this->filterGroups($product, $context));
} else {
unset($products[$i]);
}
}
// handle preselection
if ($this->shouldHandlePreselection()) {
$this->handlePreselection($products, $displayConfigs, $context);
}
// load additional data, limit displayed options
foreach ($products as $product) {
/** @var ProductConfig $config */
$config = $product->getExtension('maxiaListingVariants');
$settings = $config->getOptions();
if (!$settings || !$settings->count()) {
continue;
}
$settings = $this->filterOptions($product, $context);
$config->setOptions($settings);
$mappings = $this->loadProductMappings($product, $context);
$config->setOptionProductMappings($mappings);
$mappings = $this->loadProductEntities($config->getOptionProductMappings(), $product, $context);
$mappings = $this->loadProductCovers($mappings, $product, $context);
$config->setOptionProductMappings($mappings);
// get preselection and save to config
$selection = [];
if ($product->getOptions()) {
foreach ($product->getOptions()->getElements() as $optionEntity) {
$selection[$optionEntity->getGroupId()] = $optionEntity->getId();
}
$config->setSelection($selection);
}
$this->eventDispatcher->dispatch(new ListingVariantsLoadedEvent($product, $config, $context));
}
}
/**
* Returns the first variant product for the parent product.
*/
protected function getFirstVariantProduct(SalesChannelProductEntity $parentProduct,
array $displayConfigs,
SalesChannelContext $context): ?SalesChannelProductEntity
{
$criteria = new Criteria();
if (isset($displayConfigs[$parentProduct->getId()])) {
$displayConfig = $displayConfigs[$parentProduct->getId()];
if ($displayConfig['mainVariantId']) {
$productId = $displayConfig['mainVariantId'];
} else {
$productId = $displayConfig['firstAvailableVariantId'] ?: $displayConfig['firstVariantId'];
}
}
if (isset($productId)) {
$criteria->addFilter(new EqualsFilter('id', $productId));
} else {
$criteria->addFilter(new EqualsFilter('parentId', $parentProduct->getId()));
}
$criteria->setLimit(1);
$criteria->addAssociation('options')
->addAssociation('options.group');
$criteria->addSorting(new FieldSorting('available', FieldSorting::DESCENDING));
$criteria->addSorting(new FieldSorting('availableStock', FieldSorting::DESCENDING));
$criteria->addSorting(new FieldSorting('autoIncrement', FieldSorting::ASCENDING));
$criteria->setLimit(1);
$results = $this->productRepository->search($criteria, $context);
return $results->first();
}
/**
* Check if preselection should be handled for the current request.
*/
protected function shouldHandlePreselection(): bool
{
$request = $this->getMainRequest();
$hasPropertyFilter = $request->query->get('properties') !== null;
$handlePreselectionAttr = $request->attributes->get('maxia-handle-variant-preselection', true);
/** @var SalesChannelContext $context */
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
return $handlePreselectionAttr && !$hasPropertyFilter
&& !$this->configService->getBaseConfig($context)->isDisablePreselection();
}
/**
* Overwrite config when popup mode is active
*/
protected function setExpandedConfig(BaseConfig $baseConfig, ProductConfig $config): void
{
$config->setDisplayMode('all');
$config->setQuickBuyActive(true);
$baseConfig->setShowDeliveryInfo(true);
}
/**
* Loads the property group configs and removes groups that should be hidden in the listing.
*/
protected function filterGroups(ProductEntity $product, SalesChannelContext $context): PropertyGroupCollection
{
/** @var ProductConfig $config */
$config = $product->getExtension('maxiaListingVariants');
$settings = $config->getOptions();
$groupIndex = -1;
$removed = 0;
// filter and extend options
foreach ($settings->getElements() as $index => $groupEntity) {
$groupIndex++;
// remove group if not allowed by config
if (!$this->configService->checkDisplayMode($config, $groupEntity, $groupIndex)) {
$settings->remove($index);
$removed++;
continue;
}
// add property group config extension
$groupConfig = $this->configService->getPropertyGroupConfig($groupEntity, $context);
$groupEntity->addExtension('maxiaListingVariants', $groupConfig);
if ($groupConfig->getDisplayType() && $groupConfig->getDisplayType() !== 'inherited') {
$groupEntity->setDisplayType($groupConfig->getDisplayType());
}
}
if ($removed) {
$config->setIsPartialConfiguration(true);
}
return $settings;
}
protected function filterOptions(ProductEntity $product, SalesChannelContext $context): PropertyGroupCollection
{
/** @var ProductConfig $config */
$config = $product->getExtension('maxiaListingVariants');
/** @var PropertyGroupCollection $settings */
$settings = $config->getOptions();
$prependedOptions = new EntityCollection();
$hideUnavailable = $this->configService->getBaseConfig($context)->isHideSoldOutCloseoutProducts()
&& $config->getTotalGroupCount() === 1;
$requiredOptionIds = array_unique(array_merge(
$product->getOptionIds() ?? [], $config->getPrependedOptions() ?? []
));
// filter and extend options
foreach ($settings->getElements() as $groupEntity) {
/** @var PropertyGroupEntity $groupEntity */
/** @var PropertyGroupConfig $groupConfig */
$groupConfig = $groupEntity->getExtension('maxiaListingVariants');
$newOptions = new PropertyGroupOptionCollection();
// remove out of stock options
if ($hideUnavailable) {
$newOptions = $groupEntity->getOptions()->filter(function($option) use ($requiredOptionIds) {
return $option->getCombinable() || in_array($option->getId(), $requiredOptionIds);
});
} else {
$newOptions->merge($groupEntity->getOptions());
}
$groupConfig->setTotalEntries($newOptions->count());
if (!$config->isExpanded()) {
// limit max options and remove unavailable options
$optionIndex = 0;
foreach ($newOptions as $option) {
$optionIndex++;
if ($groupConfig->getMaxEntries() && $optionIndex > $groupConfig->getMaxEntries()) {
$newOptions->remove($option->getId());
}
}
// prepend / append preselected option if it was cut off
foreach ($requiredOptionIds as $requiredOptionId) {
$options = $newOptions->filter(function($option) use ($requiredOptionId) {
return $requiredOptionId === $option->getId();
});
if ($options->count()) {
// option is already in list
continue;
}
$requiredOptions = $groupEntity->getOptions()->filter(function($option) use ($requiredOptionId) {
return $requiredOptionId === $option->getId();
});
if ($requiredOptions->count()) {
$prependedOptions->merge($requiredOptions);
$requiredOption = $requiredOptions->first();
if ($requiredOption && $newOptions->count() > 1 &&
$requiredOption->getPosition() < $newOptions->first()->getPosition())
{
// prepend option
$requiredOptions->merge($newOptions);
$newOptions = $requiredOptions;
if ($newOptions->count() === $groupConfig->getMaxEntries()) {
$newOptions->remove($newOptions->last()->getId());
}
} else if ($requiredOption) {
// append option
if ($newOptions->count() === $groupConfig->getMaxEntries()) {
$newOptions->remove($newOptions->last()->getId());
}
$newOptions->add($requiredOption);
}
}
}
}
$groupEntity->setOptions($newOptions);
}
$config->setPrependedOptions($prependedOptions->getKeys());
return $settings;
}
/**
* Search product IDs for each option
*/
protected function loadProductMappings(ProductEntity $product, SalesChannelContext $context): array
{
/** @var ProductConfig $config */
$config = $product->getExtension('maxiaListingVariants');
$hideUnavailable = $this->configService->getBaseConfig($context)->isHideSoldOutCloseoutProducts()
&& $config->getTotalGroupCount() === 1;
$settings = $config->getOptions();
$mappings = $config->getOptionProductMappings() ?: [];
foreach ($settings->getElements() as $group) {
/** @var PropertyGroupConfig $groupConfig */
$groupConfig = $group->getExtension('maxiaListingVariants');
$optionIndex = 0;
foreach ($group->getOptions() as $key => $option) {
if (isset($mappings[$option->getId()])) {
continue;
}
$mappings[$option->getId()] = $this->loadProductMapping($product, $group, $option, $context);
if ($hideUnavailable && !$option->getCombinable()
&& !in_array($option->getId(), $product->getOptionIds()))
{
$group->getOptions()->remove($key);
continue;
}
$optionIndex++;
if (!$config->isExpanded() && $optionIndex > $groupConfig->getMaxEntries()) {
break;
}
}
}
return $mappings;
}
/**
* Loads variant mapping for the given option.
*/
protected function loadProductMapping(
ProductEntity $product,
PropertyGroupEntity $group,
PropertyGroupOptionEntity $option,
SalesChannelContext $context
): array {
/** @var ProductConfig $productConfig */
$productConfig = $product->getExtension('maxiaListingVariants');
/** @var PropertyGroupConfig $groupConfig */
$groupConfig = $group->getExtension('maxiaListingVariants');
/** @var PropertyGroupOptionEntity $option */
$combinationOptionIds = $this->getCombinationOptionIds($product, $option);
$parentId = $product->getParentId() ?: $product->getId();
// use less restrictive search, only if quick buy is off and expand by property values is inactive
$preferExactOptions = $productConfig->isQuickBuyActive();
if ($product->getConfiguratorGroupConfig()) {
foreach ($product->getConfiguratorGroupConfig() as $item) {
if ($item['expressionForListings']) {
$preferExactOptions = true;
}
}
}
try {
// try to find available variants first
$foundCombination = $this->combinationFinder->find(
$parentId,
$group->getId(),
$combinationOptionIds,
false,
$context
);
$option->setCombinable(true);
$mapping = [
'productId' => $foundCombination->getVariantId(),
'isCombinable' => $option->getCombinable()
];
} catch (ProductNotFoundException $e) {
try {
if ($preferExactOptions) {
$foundCombination = $this->combinationFinder->find(
$parentId,
$group->getId(),
$combinationOptionIds,
true,
$context
);
$option->setCombinable(false);
} else {
try {
$foundCombination = $this->combinationFinder->find(
$parentId,
$group->getId(),
[$option->getId()],
false,
$context
);
$option->setCombinable(true);
} catch (ProductNotFoundException $e) {
$foundCombination = $this->combinationFinder->find(
$parentId,
$group->getId(),
[$option->getId()],
true,
$context
);
$option->setCombinable(false);
}
}
$mapping = [
'productId' => $foundCombination->getVariantId(),
'isCombinable' => $option->getCombinable()
];
} catch (ProductNotFoundException $e) {
$this->logger->debug('No products found for combination', [
'productId' => $product->getId(),
'combinationOptionIds' => $combinationOptionIds
]);
$mapping = [
'productId' => $product->getId(),
'isCombinable' => $product->getAvailable(),
'loadEntity' => false
];
}
}
$pluginConfig = $this->configService->getBaseConfig($context);
if ($pluginConfig->isLoadAllEntities() ||
($groupConfig->isShowPrices() && in_array($groupConfig->getDisplayType(), ['dropdown', 'list']) ||
($groupConfig->isShowPrices() && in_array($groupConfig->getDisplayType(), ['inherited', null]) && $group->getDisplayType() === 'select'))
) {
$mapping['loadEntity'] = true;
} else {
$mapping['loadEntity'] = false;
}
return $mapping;
}
/**
* Loads product entities for each option.
*/
protected function loadProductEntities(array $mappings,
ProductEntity $product,
SalesChannelContext $context): array
{
$variantIds = [];
foreach ($mappings as $optionId => $mapping) {
if ($mapping['loadEntity']) {
$variantIds[] = $mapping['productId'];
}
}
if ($variantIds) {
$criteria = new Criteria();
$criteria->setIds(array_unique($variantIds));
$criteria->addAssociation('prices');
/** @var EntityCollection|null $product */
$products = $this->productRepository->search($criteria, $context);
// assign entities to mappings array
foreach ($mappings as $optionId => $mapping) {
if ($products->get($mapping['productId'])) {
/** @var SalesChannelProductEntity $variant */
$variant = $products->get($mapping['productId']);
$mappings[$optionId]['entity'] = $variant;
}
}
}
return $mappings;
}
/**
* Returns the option IDs that are used for resolving the product for each option.
*/
protected function getCombinationOptionIds(ProductEntity $productEntity, PropertyGroupOptionEntity $option): array
{
/** @var PropertyGroupCollection $settings */
$settings = $productEntity->getExtension('maxiaListingVariants')->getOptions();
$optionIds = [];
if (!$productEntity->getOptionIds()) {
return [$option->getId()];
}
foreach ($productEntity->getOptionIds() as $optionId) {
$group = $settings->filter(function (PropertyGroupEntity $group) use ($optionId) {
$optionIds = $group->getOptions()->map(function(PropertyGroupOptionEntity $option) {
return $option->getId();
});
return in_array($optionId, $optionIds);
})->first();
if ($group && $group->getId() === $option->getGroupId()) {
continue;
} else {
$optionIds[] = $optionId;
}
}
$optionIds[] = $option->getId();
return $optionIds;
}
/**
* Overrides the default variant preselection in some cases.
*
* @param SalesChannelProductEntity[] $products
*/
protected function handlePreselection(array $products, array $displayConfigs, SalesChannelContext $context): void
{
$newProductIds = $this->resolvePreselectionIds($products, $displayConfigs, $context);
if (!$newProductIds) {
return;
}
// load product entity for the new preselection
$criteria = clone $this->getProductListingCriteria($context);
$criteria->addAssociation('cover')
->addAssociation('options.group')
->addAssociation('manufacturer.media')
->addAssociation('properties.group');
$criteria->addFilter(new EqualsAnyFilter('id', array_unique(array_values($newProductIds))));
$criteria->setLimit(null);
$criteria->setOffset(null);
$newProducts = $this->productRepository->search($criteria, $context);
// override products with the new ones
foreach ($products as $productId => $product) {
if (!isset($newProductIds[$productId])) {
continue;
}
$newProductId = $newProductIds[$productId];
if (!$newProducts->has($newProductId)) {
continue;
}
$newProduct = clone $newProducts->get($newProductId);
foreach ($product->getExtensions() as $name => $extension) {
if (!$newProduct->hasExtension($name)) {
$newProduct->addExtension($name, $extension);
}
}
foreach ($newProduct->getVars() as $key => $value) {
$product->assign([$key => $value]);
}
$settings = $this->configuratorLoader->load($product, $context);
if ($settings && $settings->count()) {
$config = $product->getExtension('maxiaListingVariants');
$config->setTotalGroupCount($settings->count());
$config->setOptions($settings);
$config->setOptions($this->filterGroups($product, $context));
}
}
}
/**
* Returns new product IDs as array (old product ID => new product ID)
*
* @param SalesChannelProductEntity[] $products
*/
protected function resolvePreselectionIds(array $products,
array $displayConfigs,
SalesChannelContext $context): array
{
$pluginConfig = $this->configService->getBaseConfig($context);
$newProductIds = [];
foreach ($products as $product) {
/** @var ProductConfig $productConfig */
$productConfig = $product->getExtension('maxiaListingVariants');
$settings = $productConfig->getOptions();
$mainVariantId = null;
if ($product->getChildCount() && $pluginConfig->isDisplayParentSupported()) {
// parent product without preselection
continue;
}
$productId = $product->getParentId() ?: $product->getId();
if (!isset($displayConfigs[$productId]) || !$settings || !$settings->count()) {
continue;
}
$displayConfig = $displayConfigs[$productId];
$mainVariantId = $displayConfig['mainVariantId'];
$configuratorGroupConfig = $displayConfig['configuratorGroupConfig'];
if ($configuratorGroupConfig) {
// check if expand by property is active (auffaechern), if yes, do not update this product
$expandActive = false;
foreach ($configuratorGroupConfig as $item) {
if (isset($item['expressionForListings']) && $item['expressionForListings']) {
$expandActive = true;
break;
}
}
if ($expandActive && !($product->getChildCount() && !$pluginConfig->isDisplayParentSupported())) {
continue;
}
}
if (!$mainVariantId && $pluginConfig->isPreselectFirstVariantByDefault()) {
$firstGroup = $settings->first();
$firstOption = $firstGroup && $firstGroup->getOptions()
? $firstGroup->getOptions()->first()
: null;
if ($firstOption) {
try {
$foundCombination = $this->combinationFinder->find(
$displayConfig['parentId'],
$firstGroup->getId(),
[$firstOption->getId()],
!$pluginConfig->isAvoidOutOfStockPreselection(),
$context
);
$mainVariantId = $foundCombination->getVariantId();
if ($pluginConfig->isAvoidOutOfStockPreselection()) {
$displayConfig['available'] = true;
}
} catch (ProductNotFoundException $e) {}
}
}
if ($pluginConfig->isAvoidOutOfStockPreselection() && !$displayConfig['available']
&& $displayConfig['firstAvailableVariantId'])
{
$mainVariantId = $displayConfig['firstAvailableVariantId'];
}
if (!$mainVariantId && $product->getChildCount() && !$pluginConfig->isDisplayParentSupported()) {
$mainVariantId = $displayConfig['firstVariantId'];
}
if ($mainVariantId && $mainVariantId !== $product->getId()) {
$newProductIds[$product->getId()] = $mainVariantId;
}
}
return $newProductIds;
}
/**
* Loads cover media for all options.
*/
protected function loadProductCovers(array $mappings,
ProductEntity $product,
SalesChannelContext $context): array
{
// build product IDs array
$variantIds = [];
foreach ($mappings as $mapping) {
if (!isset($mapping['media'])) {
$variantIds[] = $mapping['productId'];
}
}
if (empty($variantIds)) {
return $mappings;
}
$parentId = $product->getParentId() ?: $product->getId();
$variantIds[] = $parentId;
$productMedia = $this->getProductCovers($variantIds, $context);
if (!$productMedia) {
return $mappings;
}
// assign media to options
foreach ($mappings as $optionId => $mapping) {
if (isset($productMedia[$mapping['productId']])) {
$media = $productMedia[$mapping['productId']];
} else if (isset($productMedia[$parentId])) {
$media = $productMedia[$parentId];
} else {
continue;
}
if ($media) {
$mappings[$optionId]['media'] = $media['entity'];
}
}
// assign media to main product entity
if (!$product->getCover()) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $parentId));
$media = $this->productMediaRepository->search($criteria, $context->getContext());
if ($media->count()) {
$product->setCover($media->first());
}
}
return $mappings;
}
/**
* Load cover media for multiple products, grouped by product IDs.
*/
protected function getProductCovers(array $productIds, SalesChannelContext $context): array
{
// get media IDs for all products
$mediaIds = $this->dbConnection->fetchAllAssociative("
SELECT product.id, product_media.media_id FROM product
LEFT JOIN product_media ON (product_media.id = product.cover)
WHERE product.id IN (:ids) AND product_media.media_id IS NOT NULL
",
['ids' => Uuid::fromHexToBytesList(array_unique($productIds))],
['ids' => Connection::PARAM_STR_ARRAY]
);
if (empty($mediaIds)) {
return [];
}
$criteria = new Criteria();
$criteria->setIds(Uuid::fromBytesToHexList(array_column($mediaIds, 'media_id')));
$mediaEntities = $this->mediaRepository->search($criteria, $context->getContext());
$productMedia = [];
foreach ($mediaIds as $item) {
$productMedia[UUid::fromBytesToHex($item['id'])] =
$productMedia[UUid::fromBytesToHex($item['id'])] = [
'media_id' => UUid::fromBytesToHex($item['media_id']),
'entity' => $mediaEntities->get(UUid::fromBytesToHex($item['media_id']))
];
}
return $productMedia;
}
/**
* Returns cached product listing criteria for the current request.
*/
public function getProductListingCriteria(SalesChannelContext $context): ?Criteria
{
static $criteria;
if ($criteria === null) {
$criteria = new Criteria();
if (class_exists('\Shopware\Core\Content\Product\SalesChannel\Sorting\ProductSortingCollection')) {
$criteria->addExtension('sortings', ProductListingCmsElementResolver::createSortings());
}
$this->eventDispatcher->dispatch(
new ProductListingCriteriaEvent($this->getMainRequest(), $criteria, $context)
);
}
return $criteria;
}
/**
* Returns listing criteria for the current configurator selection.
*/
public function buildCriteria(Request $request, SalesChannelContext $salesChannelContext): Criteria
{
$productId = $request->query->get('productId');
$pluginConfig = $this->configService->getBaseConfig($salesChannelContext);
if ($pluginConfig->isDisplayParentSupported()) {
$displayConfig = $this->dbConnection->fetchAssociative(
'SELECT COALESCE(parent_id, id) as parent_id, display_parent FROM product WHERE id = :id',
['id' => Uuid::fromHexToBytes($productId)]
);
} else {
$displayConfig = $this->dbConnection->fetchAssociative(
'SELECT COALESCE(parent_id, id) as parent_id, 0 as display_parent FROM product WHERE id = :id',
['id' => Uuid::fromHexToBytes($productId)]
);
}
$parentId = Uuid::fromBytesToHex($displayConfig['parent_id']);
if ($request->query->get('options') && $request->query->get('switched')) {
// find new product id by selected options
$newOptions = json_decode($request->query->get('options'), true);
$switchedOption = $request->query->get('switched');
try {
$foundCombination = $this->combinationFinder->find(
$parentId, $switchedOption, $newOptions,
true, $salesChannelContext
);
$productId = $foundCombination->getVariantId();
} catch (ProductNotFoundException $e) {}
}
$criteria = (new Criteria())
->addAssociation('cover')
->addAssociation('options.group')
->addAssociation('manufacturer.media')
->addAssociation('properties.group')
->setLimit(1);
if ($parentId === $productId && $displayConfig['display_parent']) {
// show parent product (handled by ProductListingLoader::resolvePreviews)
$criteria->addFilter(new EqualsFilter('product.parentId', $productId));
} else {
// show specific variant
$criteria->addFilter(new EqualsFilter('product.id', $productId));
// add option filter to prevent ProductListingLoader::resolvePreviews
$criteria->addFilter(new NotFilter(
NotFilter::CONNECTION_AND,
[ new EqualsFilter('optionIds', '1')]
))
->addPostFilter(new NotFilter(
NotFilter::CONNECTION_AND,
[ new EqualsFilter('optionIds', '1')]
));
}
return $criteria;
}
protected function getMainRequest(): ?Request
{
return method_exists($this->requestStack, 'getMainRequest')
? $this->requestStack->getMainRequest()
: $this->requestStack->getMasterRequest();
}
}