diff --git a/vendor/magento/module-catalog/Block/Product/ListProduct.php b/vendor/magento/module-catalog/Block/Product/ListProduct.php index 5b8cb6a1919..ab50e83e967 100644 --- a/vendor/magento/module-catalog/Block/Product/ListProduct.php +++ b/vendor/magento/module-catalog/Block/Product/ListProduct.php @@ -43,15 +43,11 @@ class ListProduct extends AbstractProduct implements IdentityInterface protected $_defaultToolbarBlock = Toolbar::class; /** - * Product Collection - * * @var AbstractCollection */ protected $_productCollection; /** - * Catalog layer - * * @var Layer */ protected $_catalogLayer; @@ -427,8 +423,10 @@ class ListProduct extends AbstractProduct implements IdentityInterface */ protected function getPriceRender() { - return $this->getLayout()->getBlock('product.price.render.default') - ->setData('is_product_list', true); + $block = $this->getLayout()->getBlock('product.price.render.default'); + $block->setData('is_product_list', true); + + return $block->setData('product_list', $this->_productCollection); } /** diff --git a/vendor/magento/module-configurable-product/Pricing/Price/SpecialPriceBulkResolver.php b/vendor/magento/module-configurable-product/Pricing/Price/SpecialPriceBulkResolver.php new file mode 100644 index 00000000000..611aa15109f --- /dev/null +++ b/vendor/magento/module-configurable-product/Pricing/Price/SpecialPriceBulkResolver.php @@ -0,0 +1,212 @@ +resource = $resource; + $this->metadataPool = $metadataPool; + $this->cache = $cache; + $this->cacheLifeTime = $cacheLifeTime; + } + + /** + * Determines if blocks have special prices + * + * @param int $storeId + * @param AbstractCollection|null $productCollection + * @return array + * @throws \Exception + */ + public function generateSpecialPriceMap(int $storeId, ?AbstractCollection $productCollection): array + { + if (!$productCollection) { + return []; + } + $cacheKey = $this->getCacheKey($storeId, $productCollection); + $cachedData = $this->getCachedData($cacheKey); + if ($cachedData === null) { + $configurableProducts = $this->filterConfigurableProducts($productCollection); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $connection = $this->resource->getConnection(); + $select = $connection->select() + ->from( + ['link' => $this->resource->getTableName('catalog_product_super_link')] + ) + ->joinInner( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'link.parent_id = e.' . $metadata->getLinkField() + ) + ->joinInner( + ['product_website' => $this->resource->getTableName('catalog_product_website')], + 'product_website.product_id = link.product_id' + ) + ->joinLeft( + ['price' => $this->resource->getTableName('catalog_product_index_price')], + 'price.entity_id = link.product_id AND price.website_id = ' . $storeId . + ' AND price.customer_group_id = 0' + ) + ->where('e.entity_id IN (' . implode(',', $configurableProducts) . ')') + ->columns( + [ + 'link.product_id', + '(price.final_price < price.price) AS hasSpecialPrice', + 'e.' . $metadata->getLinkField() . ' AS identifier', + 'e.entity_id' + ] + ); + $data = $connection->fetchAssoc($select); + $map = []; + foreach ($data as $specialPriceInfo) { + if (!isset($map[$specialPriceInfo['entity_id']])) { + $map[$specialPriceInfo['entity_id']] = (bool) $specialPriceInfo['hasSpecialPrice']; + } else { + if ($specialPriceInfo['hasSpecialPrice'] > $map[$specialPriceInfo['entity_id']]) { + $map[$specialPriceInfo['entity_id']] = true; + } + } + + } + $this->saveCachedData($cacheKey, $map, array_column($data, 'identifier')); + + return $map; + } + + return $cachedData; + } + + /** + * Returns only configurable product ids + * + * @param AbstractCollection $productCollection + * @return array + */ + private function filterConfigurableProducts(AbstractCollection $productCollection): array + { + $configurableProductIds = []; + /** @var Product $product */ + foreach ($productCollection->getItems() as $product) { + if ($product->getTypeId() == Configurable::TYPE_CODE) { + $configurableProductIds[] = $product->getEntityId(); + } + } + + return $configurableProductIds; + } + + /** + * Generate cache key + * + * @param int $storeId + * @param AbstractCollection $productCollection + * @return string + */ + private function getCacheKey(int $storeId, AbstractCollection $productCollection): string + { + $keyParts = $productCollection->getAllIds(); + $keyParts[] = 'store_id_' . $storeId; + + return hash('sha256', implode('_', $keyParts)); + } + + /** + * Retrieve potential cached data + * + * @param string $cacheKey + * @return array|null + */ + private function getCachedData(string $cacheKey): ?array + { + $data = $this->cache->load($cacheKey); + if (!$data) { + return null; + } + + return json_decode($data, true); + } + + /** + * Store data in cache + * + * @param string $cacheKey + * @param array $data + * @param array $productTags + * @return bool + */ + private function saveCachedData(string $cacheKey, array $data, array $productTags): bool + { + $tags = [ + Category::CACHE_TAG, + Product::CACHE_TAG, + 'price' + ]; + $productTags = array_unique($productTags); + foreach ($productTags as $tag) { + $tags[] = Configurable::TYPE_CODE . '_' . $tag; + } + + return $this->cache->save(json_encode($data), $cacheKey, $tags, $this->cacheLifeTime); + } +} diff --git a/vendor/magento/module-configurable-product/Pricing/Render/FinalPriceBox.php b/vendor/magento/module-configurable-product/Pricing/Render/FinalPriceBox.php index b48a987c235..3582f1d8176 100644 --- a/vendor/magento/module-configurable-product/Pricing/Render/FinalPriceBox.php +++ b/vendor/magento/module-configurable-product/Pricing/Render/FinalPriceBox.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface; use Magento\Catalog\Pricing\Price\FinalPrice; use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface; use Magento\Catalog\Pricing\Price\RegularPrice; +use Magento\ConfigurableProduct\Pricing\Price\SpecialPriceBulkResolver; use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\Render\RendererPool; @@ -23,7 +24,17 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox /** * @var ConfigurableOptionsProviderInterface */ - private $configurableOptionsProvider; + private ConfigurableOptionsProviderInterface $configurableOptionsProvider; + + /** + * @var SpecialPriceBulkResolver + */ + private SpecialPriceBulkResolver $specialPriceBulkResolver; + + /** + * @var array + */ + private array $specialPriceMap = []; /** * @param Context $context @@ -33,17 +44,19 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox * @param SalableResolverInterface $salableResolver * @param MinimalPriceCalculatorInterface $minimalPriceCalculator * @param ConfigurableOptionsProviderInterface $configurableOptionsProvider + * @param SpecialPriceBulkResolver $specialPriceBulkResolver * @param array $data */ public function __construct( - Context $context, - SaleableInterface $saleableItem, - PriceInterface $price, - RendererPool $rendererPool, - SalableResolverInterface $salableResolver, - MinimalPriceCalculatorInterface $minimalPriceCalculator, + Context $context, + SaleableInterface $saleableItem, + PriceInterface $price, + RendererPool $rendererPool, + SalableResolverInterface $salableResolver, + MinimalPriceCalculatorInterface $minimalPriceCalculator, ConfigurableOptionsProviderInterface $configurableOptionsProvider, - array $data = [] + SpecialPriceBulkResolver $specialPriceBulkResolver, + array $data = [] ) { parent::__construct( $context, @@ -56,23 +69,40 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox ); $this->configurableOptionsProvider = $configurableOptionsProvider; + $this->specialPriceBulkResolver = $specialPriceBulkResolver; } /** * Define if the special price should be shown * * @return bool + * @throws \Exception */ public function hasSpecialPrice() { - $product = $this->getSaleableItem(); - foreach ($this->configurableOptionsProvider->getProducts($product) as $subProduct) { - $regularPrice = $subProduct->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getValue(); - $finalPrice = $subProduct->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); - if ($finalPrice < $regularPrice) { - return true; + if ($this->isProductList()) { + if ($this->getData('product_list') === null) { + return false; + } + + if (empty($this->specialPriceMap)) { + $this->specialPriceMap = $this->specialPriceBulkResolver->generateSpecialPriceMap( + $this->saleableItem->getStoreId(), + $this->getData('product_list') + ); + } + + return $this->specialPriceMap[$this->saleableItem->getId()]; + } else { + $product = $this->getSaleableItem(); + foreach ($this->configurableOptionsProvider->getProducts($product) as $subProduct) { + $regularPrice = $subProduct->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getValue(); + $finalPrice = $subProduct->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue(); + if ($finalPrice < $regularPrice) { + return true; + } } + return false; } - return false; } } diff --git a/vendor/magento/module-configurable-product/etc/di.xml b/vendor/magento/module-configurable-product/etc/di.xml index 16e3aaafc9c..7ce4d50df81 100644 --- a/vendor/magento/module-configurable-product/etc/di.xml +++ b/vendor/magento/module-configurable-product/etc/di.xml @@ -283,4 +283,9 @@ + + + Magento\Framework\App\Cache\Type\Collection + + diff --git a/vendor/magento/module-configurable-product/view/base/templates/product/price/final_price.phtml b/vendor/magento/module-configurable-product/view/base/templates/product/price/final_price.phtml index a6dc6c81998..c1238139d01 100644 --- a/vendor/magento/module-configurable-product/view/base/templates/product/price/final_price.phtml +++ b/vendor/magento/module-configurable-product/view/base/templates/product/price/final_price.phtml @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -/** @var \Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox$block */ +/** @var \Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox $block */ /** @var \Magento\Framework\Pricing\Price\PriceInterface $priceModel */ $priceModel = $block->getPriceType('regular_price'); /** @var \Magento\Framework\Pricing\Price\PriceInterface $finalPriceModel */ @@ -23,24 +23,24 @@ $schema = ($block->getZone() == 'item_view') ? true : false; ?> -isProductList() && $block->hasSpecialPrice()) : ?> +hasSpecialPrice()): ?> renderAmount($priceModel->getAmount(), [ 'display_label' => __('Regular Price'), 'price_id' => $block->getPriceId('old-price-' . $idSuffix), 'price_type' => 'oldPrice', 'include_container' => true, - 'skip_adjustments' => true + 'skip_adjustments' => false ]); ?> -showMinimalPrice()) : ?> - getUseLinkForAsLowAs()) :?> +showMinimalPrice()): ?> + getUseLinkForAsLowAs()): ?> renderAmountMinimal() ?> - + renderAmountMinimal() ?>