Skip to content

Commit 7c19f03

Browse files
author
Yaroslav Onischenko
committed
MAGETWO-64889: [Performance] Optimize "Is Salable" check on Category page for Configurable Product
2 parents 6b5de07 + b7a8b32 commit 7c19f03

File tree

14 files changed

+515
-54
lines changed

14 files changed

+515
-54
lines changed

app/code/Magento/Catalog/Model/Product.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,7 +1617,15 @@ public function setIsDuplicable($value)
16171617
*/
16181618
public function isSalable()
16191619
{
1620-
if ($this->hasData('salable') && !$this->_catalogProduct->getSkipSaleableCheck()) {
1620+
if ($this->_catalogProduct->getSkipSaleableCheck()) {
1621+
return true;
1622+
}
1623+
if (($this->getOrigData('status') != $this->getData('status'))
1624+
|| $this->isStockStatusChanged()) {
1625+
$this->unsetData('salable');
1626+
}
1627+
1628+
if ($this->hasData('salable')) {
16211629
return $this->getData('salable');
16221630
}
16231631
$this->_eventManager->dispatch('catalog_product_is_salable_before', ['product' => $this]);
@@ -1630,7 +1638,7 @@ public function isSalable()
16301638
['product' => $this, 'salable' => $object]
16311639
);
16321640
$this->setData('salable', $object->getIsSalable());
1633-
return $object->getIsSalable();
1641+
return $this->getData('salable');
16341642
}
16351643

16361644
/**
@@ -1640,7 +1648,7 @@ public function isSalable()
16401648
*/
16411649
public function isAvailable()
16421650
{
1643-
return $this->getTypeInstance()->isSalable($this) || $this->_catalogProduct->getSkipSaleableCheck();
1651+
return $this->_catalogProduct->getSkipSaleableCheck() || $this->getTypeInstance()->isSalable($this);
16441652
}
16451653

16461654
/**

app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ class Full
156156
*/
157157
private $iteratorFactory;
158158

159+
/**
160+
* @var \Magento\Framework\EntityManager\MetadataPool
161+
*/
162+
private $metadataPool;
163+
159164
/**
160165
* @param ResourceConnection $resource
161166
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
@@ -175,6 +180,7 @@ class Full
175180
* @param \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory
176181
* @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig
177182
* @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory
183+
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
178184
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
179185
*/
180186
public function __construct(
@@ -195,7 +201,8 @@ public function __construct(
195201
\Magento\CatalogSearch\Model\ResourceModel\Fulltext $fulltextResource,
196202
\Magento\Framework\Search\Request\DimensionFactory $dimensionFactory,
197203
\Magento\Framework\Indexer\ConfigInterface $indexerConfig,
198-
\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory
204+
\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory,
205+
\Magento\Framework\EntityManager\MetadataPool $metadataPool = null
199206
) {
200207
$this->resource = $resource;
201208
$this->connection = $resource->getConnection();
@@ -216,6 +223,8 @@ public function __construct(
216223
$this->fulltextResource = $fulltextResource;
217224
$this->dimensionFactory = $dimensionFactory;
218225
$this->iteratorFactory = $indexIteratorFactory;
226+
$this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance()
227+
->get(\Magento\Framework\EntityManager\MetadataPool::class);
219228
}
220229

221230
/**
@@ -252,14 +261,22 @@ protected function getTable($table)
252261
*/
253262
protected function getProductIdsFromParents(array $entityIds)
254263
{
255-
return $this->connection
264+
/** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */
265+
$metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
266+
$fieldForParent = $metadata->getLinkField();
267+
268+
$select = $this->connection
256269
->select()
257-
->from($this->getTable('catalog_product_relation'), 'parent_id')
270+
->from(['relation' => $this->getTable('catalog_product_relation')], [])
258271
->distinct(true)
259272
->where('child_id IN (?)', $entityIds)
260273
->where('parent_id NOT IN (?)', $entityIds)
261-
->query()
262-
->fetchAll(\Zend_Db::FETCH_COLUMN);
274+
->join(
275+
['cpe' => $this->getTable('catalog_product_entity')],
276+
'relation.parent_id = cpe.' . $fieldForParent,
277+
['cpe.entity_id']
278+
);
279+
return $this->connection->fetchCol($select);
263280
}
264281

265282
/**
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\ConfigurableProduct\Model\Product\Type\Collection;
7+
8+
use Magento\Catalog\Api\Data\ProductInterface;
9+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
10+
use Magento\Catalog\Model\ResourceModel\Product\Collection;
11+
use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory;
12+
13+
/**
14+
* This class is responsible for adding additional filters for products collection
15+
* to check if the product from this collection available to buy.
16+
*/
17+
class SalableProcessor
18+
{
19+
/**
20+
* @var StatusFactory
21+
*/
22+
private $stockStatusFactory;
23+
24+
/**
25+
* @param StatusFactory $stockStatusFactory
26+
*/
27+
public function __construct(StatusFactory $stockStatusFactory)
28+
{
29+
$this->stockStatusFactory = $stockStatusFactory;
30+
}
31+
32+
/**
33+
* Adds filters to the collection to help determine if product is available for sale.
34+
*
35+
* This method adds several additional checks for a children products availability.
36+
* Children products should be enabled and available in stock to be sold.
37+
* It also adds the specific flag to the collection to prevent the case
38+
* when filter already added and therefore may break the collection.
39+
*
40+
* @param Collection $collection
41+
* @return Collection
42+
*/
43+
public function process(Collection $collection)
44+
{
45+
$collection->addAttributeToFilter(
46+
ProductInterface::STATUS,
47+
Status::STATUS_ENABLED
48+
);
49+
50+
$stockFlag = 'has_stock_status_filter';
51+
if (!$collection->hasFlag($stockFlag)) {
52+
$stockStatusResource = $this->stockStatusFactory->create();
53+
$stockStatusResource->addStockDataToCollection($collection, true);
54+
$collection->setFlag($stockFlag, true);
55+
}
56+
57+
return $collection;
58+
}
59+
}

app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Magento\Catalog\Api\Data\ProductInterface;
1010
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
1111
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor;
1213
use Magento\Catalog\Model\Config;
1314
use Magento\Framework\App\ObjectManager;
1415
use Magento\Framework\EntityManager\MetadataPool;
@@ -214,7 +215,8 @@ public function __construct(
214215
\Magento\Framework\Cache\FrontendInterface $cache = null,
215216
\Magento\Customer\Model\Session $customerSession = null,
216217
\Magento\Framework\Serialize\Serializer\Json $serializer = null,
217-
ProductInterfaceFactory $productFactory = null
218+
ProductInterfaceFactory $productFactory = null,
219+
SalableProcessor $salableProcessor = null
218220
) {
219221
$this->typeConfigurableFactory = $typeConfigurableFactory;
220222
$this->_eavAttributeFactory = $eavAttributeFactory;
@@ -228,6 +230,7 @@ public function __construct(
228230
$this->customerSession = $customerSession;
229231
$this->productFactory = $productFactory ?: ObjectManager::getInstance()
230232
->get(ProductInterfaceFactory::class);
233+
$this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class);
231234
parent::__construct(
232235
$catalogProductOption,
233236
$eavConfig,
@@ -401,7 +404,7 @@ public function getUsedProductAttributes($product)
401404
$usedProductAttributes = [];
402405
$usedAttributes = [];
403406
foreach ($this->getConfigurableAttributes($product) as $attribute) {
404-
if (!is_null($attribute->getProductAttribute())) {
407+
if (null !== $attribute->getProductAttribute()) {
405408
$id = $attribute->getProductAttribute()->getId();
406409
$usedProductAttributes[$id] = $attribute->getProductAttribute();
407410
$usedAttributes[$id] = $attribute;
@@ -588,7 +591,7 @@ function ($item) {
588591
$product->setData($this->_usedProducts, $usedProducts);
589592
}
590593
\Magento\Framework\Profiler::stop('CONFIGURABLE:' . __METHOD__);
591-
$usedProducts = $product->getData($this->_usedProducts);
594+
$usedProducts = $product->getData($this->_usedProducts);
592595
return $usedProducts;
593596
}
594597

@@ -621,7 +624,7 @@ public function getUsedProductCollection($product)
621624
)->setProductFilter(
622625
$product
623626
);
624-
if (!is_null($this->getStoreFilter($product))) {
627+
if (null !== $this->getStoreFilter($product)) {
625628
$collection->addStoreFilter($this->getStoreFilter($product));
626629
}
627630

@@ -676,7 +679,7 @@ public function save($product)
676679
{
677680
parent::save($product);
678681
$metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
679-
$cacheId = __CLASS__ . $product->getData($metadata->getLinkField()) . '_' . $product->getStoreId();
682+
$cacheId = __CLASS__ . $product->getData($metadata->getLinkField()) . '_' . $product->getStoreId();
680683
$this->cache->remove($cacheId);
681684

682685
$extensionAttributes = $product->getExtensionAttributes();
@@ -775,17 +778,10 @@ public function isSalable($product)
775778
$salable = parent::isSalable($product);
776779

777780
if ($salable !== false) {
778-
$salable = false;
779-
if (!is_null($product)) {
780-
$this->setStoreFilter($product->getStoreId(), $product);
781-
}
782-
/** @var \Magento\Catalog\Model\Product $child */
783-
foreach ($this->getUsedProducts($product) as $child) {
784-
if ($child->isSalable()) {
785-
$salable = true;
786-
break;
787-
}
788-
}
781+
$collection = $this->getUsedProductCollection($product);
782+
$collection->addStoreFilter($this->getStoreFilter($product));
783+
$collection = $this->salableProcessor->process($collection);
784+
$salable = 0 !== $collection->getSize();
789785
}
790786

791787
return $salable;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Collection;
7+
8+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
9+
use Magento\Catalog\Api\Data\ProductInterface;
10+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
11+
12+
class SalableProcessorTest extends \PHPUnit_Framework_TestCase
13+
{
14+
const STOCK_FLAG = 'has_stock_status_filter';
15+
16+
/** @var ObjectManager */
17+
private $objectManager;
18+
19+
/** @var \Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor */
20+
protected $model;
21+
22+
/** @var \PHPUnit_Framework_MockObject_MockObject */
23+
protected $stockStatusFactory;
24+
25+
protected function setUp()
26+
{
27+
$this->objectManager = new ObjectManager($this);
28+
29+
$this->stockStatusFactory = $this->getMockBuilder(
30+
\Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory::class
31+
)
32+
->setMethods(['create'])
33+
->disableOriginalConstructor()
34+
->getMock();
35+
36+
$this->model = $this->objectManager->getObject(
37+
\Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor::class,
38+
[
39+
'stockStatusFactory' => $this->stockStatusFactory,
40+
]
41+
);
42+
}
43+
44+
public function testProcess()
45+
{
46+
$productCollection = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Collection::class)
47+
->setMethods(['addAttributeToFilter'])
48+
->disableOriginalConstructor()
49+
->getMock();
50+
51+
$productCollection->expects($this->once())
52+
->method('addAttributeToFilter')
53+
->with(ProductInterface::STATUS, Status::STATUS_ENABLED)
54+
->will($this->returnSelf());
55+
56+
$stockStatusResource = $this->getMockBuilder(\Magento\CatalogInventory\Model\ResourceModel\Stock\Status::class)
57+
->setMethods(['addStockDataToCollection'])
58+
->disableOriginalConstructor()
59+
->getMock();
60+
$stockStatusResource->expects($this->once())
61+
->method('addStockDataToCollection')
62+
->with($productCollection, true)
63+
->will($this->returnSelf());
64+
65+
$this->stockStatusFactory
66+
->expects($this->once())
67+
->method('create')
68+
->will($this->returnValue($stockStatusResource));
69+
70+
$this->model->process($productCollection);
71+
72+
$this->assertTrue($productCollection->hasFlag(self::STOCK_FLAG));
73+
}
74+
}

0 commit comments

Comments
 (0)