diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php
index 5c3fd4730aaed..0bb468b77ee6e 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php
@@ -59,7 +59,7 @@ public function beforeSave($object)
if (isset($stockData['qty']) && $stockData['qty'] === '') {
$stockData['qty'] = null;
}
- if ($object->getStockData() !== null || $stockData !== null) {
+ if ($object->getStockData() !== null && $stockData !== null) {
$object->setStockData(array_replace((array)$object->getStockData(), (array)$stockData));
}
$object->unsetData($this->getAttribute()->getAttributeCode());
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php
new file mode 100644
index 0000000000000..3bf7407b095fa
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php
@@ -0,0 +1,25 @@
+baseSelectProcessors = $baseSelectProcessors;
+ }
+
+ /**
+ * @param Select $select
+ * @return Select
+ */
+ public function process(Select $select)
+ {
+ foreach ($this->baseSelectProcessors as $baseSelectProcessor) {
+ $select = $baseSelectProcessor->process($select);
+ }
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
index 32281eccd934b..4ec5aab06f5f6 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php
@@ -5,9 +5,9 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product\Indexer;
-use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
-use Magento\Store\Model\Store;
use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface;
class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuilderInterface
@@ -27,19 +27,28 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild
*/
private $customerSession;
+ /**
+ * @var BaseSelectProcessorInterface
+ */
+ private $baseSelectProcessor;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
* @param \Magento\Customer\Model\Session $customerSession
+ * @param BaseSelectProcessorInterface $baseSelectProcessor
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\App\ResourceConnection $resourceConnection,
- \Magento\Customer\Model\Session $customerSession
+ \Magento\Customer\Model\Session $customerSession,
+ BaseSelectProcessorInterface $baseSelectProcessor = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
$this->customerSession = $customerSession;
+ $this->baseSelectProcessor = (null !== $baseSelectProcessor)
+ ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
}
/**
@@ -47,16 +56,22 @@ public function __construct(
*/
public function build($productId)
{
- return [$this->resource->getConnection()->select()
+ $priceSelect = $this->resource->getConnection()->select()
->from(['t' => $this->resource->getTableName('catalog_product_index_price')], 'entity_id')
->joinInner(
- ['link' => $this->resource->getTableName('catalog_product_relation')],
- 'link.child_id = t.entity_id',
+ [
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS
+ => $this->resource->getTableName('catalog_product_relation')
+ ],
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id = t.entity_id',
[]
- )->where('link.parent_id = ? ', $productId)
+ )->where(BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.parent_id = ? ', $productId)
->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId())
->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId())
->order('t.min_price ' . Select::SQL_ASC)
- ->limit(1)];
+ ->limit(1);
+ $priceSelect = $this->baseSelectProcessor->process($priceSelect);
+
+ return [$priceSelect];
}
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
index e127fb471cb21..b3949e41069ea 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php
@@ -6,6 +6,7 @@
namespace Magento\Catalog\Model\ResourceModel\Product;
use Magento\Catalog\Model\Product;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
use Magento\Store\Model\Store;
@@ -31,22 +32,31 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde
*/
private $catalogHelper;
+ /**
+ * @var BaseSelectProcessorInterface
+ */
+ private $baseSelectProcessor;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
* @param \Magento\Eav\Model\Config $eavConfig
* @param \Magento\Catalog\Helper\Data $catalogHelper
+ * @param BaseSelectProcessorInterface $baseSelectProcessor
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\App\ResourceConnection $resourceConnection,
\Magento\Eav\Model\Config $eavConfig,
- \Magento\Catalog\Helper\Data $catalogHelper
+ \Magento\Catalog\Helper\Data $catalogHelper,
+ BaseSelectProcessorInterface $baseSelectProcessor = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
$this->eavConfig = $eavConfig;
$this->catalogHelper = $catalogHelper;
+ $this->baseSelectProcessor = (null !== $baseSelectProcessor)
+ ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
}
/**
@@ -58,14 +68,18 @@ public function build($productId)
$priceSelect = $this->resource->getConnection()->select()
->from(['t' => $priceAttribute->getBackendTable()], 'entity_id')
->joinInner(
- ['link' => $this->resource->getTableName('catalog_product_relation')],
- 'link.child_id = t.entity_id',
+ [
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS
+ => $this->resource->getTableName('catalog_product_relation')
+ ],
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id = t.entity_id',
[]
- )->where('link.parent_id = ? ', $productId)
+ )->where(BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.parent_id = ? ', $productId)
->where('t.attribute_id = ?', $priceAttribute->getAttributeId())
->where('t.value IS NOT NULL')
->order('t.value ' . Select::SQL_ASC)
->limit(1);
+ $priceSelect = $this->baseSelectProcessor->process($priceSelect);
$priceSelectDefault = clone $priceSelect;
$priceSelectDefault->where('t.store_id = ?', Store::DEFAULT_STORE_ID);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
index 1312cd868189a..44a041d2fbe37 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php
@@ -6,6 +6,7 @@
namespace Magento\Catalog\Model\ResourceModel\Product;
use Magento\Catalog\Model\Product;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
use Magento\Store\Model\Store;
@@ -41,6 +42,11 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui
*/
private $localeDate;
+ /**
+ * @var BaseSelectProcessorInterface
+ */
+ private $baseSelectProcessor;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
@@ -48,6 +54,7 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui
* @param \Magento\Catalog\Helper\Data $catalogHelper
* @param \Magento\Framework\Stdlib\DateTime $dateTime
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ * @param BaseSelectProcessorInterface $baseSelectProcessor
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
@@ -55,7 +62,8 @@ public function __construct(
\Magento\Eav\Model\Config $eavConfig,
\Magento\Catalog\Helper\Data $catalogHelper,
\Magento\Framework\Stdlib\DateTime $dateTime,
- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
+ BaseSelectProcessorInterface $baseSelectProcessor = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
@@ -63,6 +71,8 @@ public function __construct(
$this->catalogHelper = $catalogHelper;
$this->dateTime = $dateTime;
$this->localeDate = $localeDate;
+ $this->baseSelectProcessor = (null !== $baseSelectProcessor)
+ ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
}
/**
@@ -80,8 +90,11 @@ public function build($productId)
$specialPrice = $connection->select()
->from(['t' => $specialPriceAttribute->getBackendTable()], 'entity_id')
->joinInner(
- ['link' => $this->resource->getTableName('catalog_product_relation')],
- 'link.child_id = t.entity_id',
+ [
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS
+ => $this->resource->getTableName('catalog_product_relation')
+ ],
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id = t.entity_id',
[]
)->joinInner(
['special_from' => $specialPriceFromDate->getBackendTable()],
@@ -97,7 +110,7 @@ public function build($productId)
$specialPriceToDate->getAttributeId()
),
''
- )->where('link.parent_id = ? ', $productId)
+ )->where(BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.parent_id = ? ', $productId)
->where('t.attribute_id = ?', $specialPriceAttribute->getAttributeId())
->where('t.value IS NOT NULL')
->where(
@@ -108,6 +121,7 @@ public function build($productId)
$currentDate
)->order('t.value ' . Select::SQL_ASC)
->limit(1);
+ $specialPrice = $this->baseSelectProcessor->process($specialPrice);
$specialPriceDefault = clone $specialPrice;
$specialPriceDefault->where('t.store_id = ?', Store::DEFAULT_STORE_ID);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
index 6d93bcbd20898..a83e09de6847d 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php
@@ -5,7 +5,7 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product;
-use Magento\Catalog\Model\Product;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilderInterface
@@ -35,22 +35,31 @@ class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilde
*/
private $catalogHelper;
+ /**
+ * @var BaseSelectProcessorInterface
+ */
+ private $baseSelectProcessor;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
* @param \Magento\Customer\Model\Session $customerSession
* @param \Magento\Catalog\Helper\Data $catalogHelper
+ * @param BaseSelectProcessorInterface $baseSelectProcessor
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\App\ResourceConnection $resourceConnection,
\Magento\Customer\Model\Session $customerSession,
- \Magento\Catalog\Helper\Data $catalogHelper
+ \Magento\Catalog\Helper\Data $catalogHelper,
+ BaseSelectProcessorInterface $baseSelectProcessor = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
$this->customerSession = $customerSession;
$this->catalogHelper = $catalogHelper;
+ $this->baseSelectProcessor = (null !== $baseSelectProcessor)
+ ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
}
/**
@@ -59,16 +68,20 @@ public function __construct(
public function build($productId)
{
$priceSelect = $this->resource->getConnection()->select()
- ->from(['t' => $this->resource->getTableName('catalog_product_entity_tier_price')], 'entity_id')
- ->joinInner(
- ['link' => $this->resource->getTableName('catalog_product_relation')],
- 'link.child_id = t.entity_id',
- []
- )->where('link.parent_id = ? ', $productId)
- ->where('t.all_groups = 1 OR customer_group_id = ?', $this->customerSession->getCustomerGroupId())
- ->where('t.qty = ?', 1)
- ->order('t.value ' . Select::SQL_ASC)
- ->limit(1);
+ ->from(['t' => $this->resource->getTableName('catalog_product_entity_tier_price')], 'entity_id')
+ ->joinInner(
+ [
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS
+ => $this->resource->getTableName('catalog_product_relation')
+ ],
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id = t.entity_id',
+ []
+ )->where(BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.parent_id = ? ', $productId)
+ ->where('t.all_groups = 1 OR customer_group_id = ?', $this->customerSession->getCustomerGroupId())
+ ->where('t.qty = ?', 1)
+ ->order('t.value ' . Select::SQL_ASC)
+ ->limit(1);
+ $priceSelect = $this->baseSelectProcessor->process($priceSelect);
$priceSelectDefault = clone $priceSelect;
$priceSelectDefault->where('t.website_id = ?', self::DEFAULT_WEBSITE_ID);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php
new file mode 100644
index 0000000000000..b0e462445ebb5
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php
@@ -0,0 +1,71 @@
+eavConfig = $eavConfig;
+ $this->storeResolver = $storeResolver;
+ }
+
+ /**
+ * @param Select $select
+ * @return Select
+ */
+ public function process(Select $select)
+ {
+ $statusAttribute = $this->eavConfig->getAttribute(Product::ENTITY, ProductInterface::STATUS);
+
+ $select->joinLeft(
+ ['status_global_attr' => $statusAttribute->getBackendTable()],
+ "status_global_attr.entity_id = " . self::PRODUCT_RELATION_ALIAS . ".child_id"
+ . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
+ . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
+ []
+ );
+
+ $select->joinLeft(
+ ['status_attr' => $statusAttribute->getBackendTable()],
+ "status_attr.entity_id = " . self::PRODUCT_RELATION_ALIAS . ".child_id"
+ . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
+ . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(),
+ []
+ );
+
+ $select->where('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED);
+
+ return $select;
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php
index 3cd1922622f0e..61048c0ac7386 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Backend/StockTest.php
@@ -125,4 +125,17 @@ public function testBeforeSaveQtyIsZero()
$stockData = $object->getStockData();
$this->assertEquals(0, $stockData['qty']);
}
+
+ public function testBeforeSaveNoStockData()
+ {
+ $object = new \Magento\Framework\DataObject(
+ [
+ self::ATTRIBUTE_NAME => ['is_in_stock' => 1, 'qty' => 0]
+ ]
+ );
+
+ $this->model->beforeSave($object);
+ $this->assertNull($object->getStockData());
+ $this->assertNull($object->getData(self::ATTRIBUTE_NAME));
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php
new file mode 100644
index 0000000000000..12b5808d62ad7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php
@@ -0,0 +1,54 @@
+objectManager = new ObjectManager($this);
+ }
+
+ /**
+ * @expectedException \Magento\Framework\Exception\InputException
+ */
+ public function testInitializeWithWrongProcessorInstance()
+ {
+ $processorValid = $this->getMock(BaseSelectProcessorInterface::class);
+ $processorInvalid = $this->getMock(\stdClass::class);
+
+ $this->objectManager->getObject(CompositeBaseSelectProcessor::class, [
+ 'baseSelectProcessors' => [$processorValid, $processorInvalid],
+ ]);
+ }
+
+ public function testProcess()
+ {
+ $select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
+
+ $processorFirst = $this->getMock(BaseSelectProcessorInterface::class);
+ $processorFirst->expects($this->once())->method('process')->with($select)->willReturn($select);
+
+ $processorSecond = $this->getMock(BaseSelectProcessorInterface::class);
+ $processorSecond->expects($this->once())->method('process')->with($select)->willReturn($select);
+
+ /** @var CompositeBaseSelectProcessor $baseSelectProcessors */
+ $baseSelectProcessors = $this->objectManager->getObject(CompositeBaseSelectProcessor::class, [
+ 'baseSelectProcessors' => [$processorFirst, $processorSecond],
+ ]);
+ $this->assertEquals($select, $baseSelectProcessors->process($select));
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php
new file mode 100644
index 0000000000000..2fb859c8f6cac
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php
@@ -0,0 +1,109 @@
+eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock();
+ $this->storeResolver = $this->getMockBuilder(StoreResolverInterface::class)->getMock();
+ $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
+
+ $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [
+ 'eavConfig' => $this->eavConfig,
+ 'storeResolver' => $this->storeResolver,
+ ]);
+ }
+
+ public function testProcess()
+ {
+ $backendTable = 'backend_table';
+ $attributeId = 2;
+ $currentStoreId = 1;
+
+ $statusAttribute = $this->getMockBuilder(AttributeInterface::class)
+ ->setMethods(['getBackendTable', 'getAttributeId'])
+ ->getMock();
+ $statusAttribute->expects($this->atLeastOnce())
+ ->method('getBackendTable')
+ ->willReturn($backendTable);
+ $statusAttribute->expects($this->atLeastOnce())
+ ->method('getAttributeId')
+ ->willReturn($attributeId);
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(Product::ENTITY, ProductInterface::STATUS)
+ ->willReturn($statusAttribute);
+
+ $this->storeResolver->expects($this->once())
+ ->method('getCurrentStoreId')
+ ->willReturn($currentStoreId);
+
+ $this->select->expects($this->at(0))
+ ->method('joinLeft')
+ ->with(
+ ['status_global_attr' => $backendTable],
+ "status_global_attr.entity_id = "
+ . BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . ".child_id"
+ . " AND status_global_attr.attribute_id = {$attributeId}"
+ . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
+ []
+ )
+ ->willReturnSelf();
+ $this->select->expects($this->at(1))
+ ->method('joinLeft')
+ ->with(
+ ['status_attr' => $backendTable],
+ "status_attr.entity_id = " . BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . ".child_id"
+ . " AND status_attr.attribute_id = {$attributeId}"
+ . " AND status_attr.store_id = {$currentStoreId}",
+ []
+ )
+ ->willReturnSelf();
+ $this->select->expects($this->at(2))
+ ->method('where')
+ ->with('IFNULL(status_attr.value, status_global_attr.value) = ?', Status::STATUS_ENABLED)
+ ->willReturnSelf();
+
+ $this->assertEquals($this->select, $this->statusBaseSelectProcessor->process($this->select));
+ }
+}
diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml
index 01057f247875a..1536cef225aa0 100644
--- a/app/code/Magento/Catalog/etc/di.xml
+++ b/app/code/Magento/Catalog/etc/di.xml
@@ -548,4 +548,12 @@
+
+
+
+
+ - Magento\Catalog\Model\ResourceModel\Product\StatusBaseSelectProcessor
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php
new file mode 100644
index 0000000000000..a62856afc051d
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php
@@ -0,0 +1,51 @@
+resource = $resource;
+ }
+
+ /**
+ * Add stock item filter to selects
+ *
+ * @param Select $select
+ * @return Select
+ */
+ public function process(Select $select)
+ {
+ $stockStatusTable = $this->resource->getTableName('cataloginventory_stock_status');
+
+ /** @var Select $select */
+ $select->join(
+ ['stock' => $stockStatusTable],
+ 'stock.product_id = ' . self::PRODUCT_RELATION_ALIAS . '.child_id',
+ []
+ )
+ ->where('stock.stock_status = ?', Stock::STOCK_IN_STOCK);
+ return $select;
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php
new file mode 100644
index 0000000000000..65933e3e7f946
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php
@@ -0,0 +1,69 @@
+resource = $this->getMockBuilder(ResourceConnection::class)->disableOriginalConstructor()->getMock();
+ $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
+
+ $this->stockStatusBaseSelectProcessor = (new ObjectManager($this))->getObject(
+ StockStatusBaseSelectProcessor::class,
+ [
+ 'resource' => $this->resource,
+ ]
+ );
+ }
+
+ public function testProcess()
+ {
+ $tableName = 'table_name';
+
+ $this->resource->expects($this->once())
+ ->method('getTableName')
+ ->with('cataloginventory_stock_status')
+ ->willReturn($tableName);
+
+ $this->select->expects($this->once())
+ ->method('join')
+ ->with(
+ ['stock' => $tableName],
+ 'stock.product_id = ' . BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id',
+ []
+ )
+ ->willReturnSelf();
+ $this->select->expects($this->once())
+ ->method('where')
+ ->with('stock.stock_status = ?', Stock::STOCK_IN_STOCK)
+ ->willReturnSelf();
+
+ $this->stockStatusBaseSelectProcessor->process($this->select);
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml
index cfd47ea146697..0e3c1ebc05e7a 100644
--- a/app/code/Magento/CatalogInventory/etc/di.xml
+++ b/app/code/Magento/CatalogInventory/etc/di.xml
@@ -74,4 +74,11 @@
+
+
+
+ - Magento\CatalogInventory\Model\ResourceModel\Product\StockStatusBaseSelectProcessor
+
+
+
diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php
index bdffeec95b624..cb8d69279b2e1 100644
--- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php
+++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php
@@ -5,9 +5,9 @@
*/
namespace Magento\CatalogRule\Model\ResourceModel\Product;
-use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DB\Select;
-use Magento\Store\Model\Store;
use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface;
class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelectBuilderInterface
@@ -37,25 +37,34 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec
*/
private $localeDate;
+ /**
+ * @var BaseSelectProcessorInterface
+ */
+ private $baseSelectProcessor;
+
/**
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Framework\App\ResourceConnection $resourceConnection
* @param \Magento\Customer\Model\Session $customerSession
* @param \Magento\Framework\Stdlib\DateTime $dateTime
* @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ * @param BaseSelectProcessorInterface $baseSelectProcessor
*/
public function __construct(
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\App\ResourceConnection $resourceConnection,
\Magento\Customer\Model\Session $customerSession,
\Magento\Framework\Stdlib\DateTime $dateTime,
- \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
+ BaseSelectProcessorInterface $baseSelectProcessor = null
) {
$this->storeManager = $storeManager;
$this->resource = $resourceConnection;
$this->customerSession = $customerSession;
$this->dateTime = $dateTime;
$this->localeDate = $localeDate;
+ $this->baseSelectProcessor = (null !== $baseSelectProcessor)
+ ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class);
}
/**
@@ -66,17 +75,23 @@ public function build($productId)
$timestamp = $this->localeDate->scopeTimeStamp($this->storeManager->getStore());
$currentDate = $this->dateTime->formatDate($timestamp, false);
- return [$this->resource->getConnection()->select()
+ $priceSelect = $this->resource->getConnection()->select()
->from(['t' => $this->resource->getTableName('catalogrule_product_price')], 'product_id')
->joinInner(
- ['link' => $this->resource->getTableName('catalog_product_relation')],
- 'link.child_id = t.product_id',
+ [
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS
+ => $this->resource->getTableName('catalog_product_relation')
+ ],
+ BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.child_id = t.product_id',
[]
- )->where('link.parent_id = ? ', $productId)
+ )->where(BaseSelectProcessorInterface::PRODUCT_RELATION_ALIAS . '.parent_id = ? ', $productId)
->where('t.website_id = ?', $this->storeManager->getStore()->getWebsiteId())
->where('t.customer_group_id = ?', $this->customerSession->getCustomerGroupId())
->where('t.rule_date = ?', $currentDate)
->order('t.rule_price ' . Select::SQL_ASC)
- ->limit(1)];
+ ->limit(1);
+ $priceSelect = $this->baseSelectProcessor->process($priceSelect);
+
+ return [$priceSelect];
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
index 1e817f369b34f..33a48c5969e1b 100644
--- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
@@ -7,8 +7,46 @@
*/
namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price;
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
+use Magento\Store\Api\StoreResolverInterface;
+use Magento\Store\Model\Store;
+
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice
{
+ /**
+ * @var StoreResolverInterface
+ */
+ private $storeResolver;
+
+ /**
+ * Class constructor
+ *
+ * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
+ * @param \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy
+ * @param \Magento\Eav\Model\Config $eavConfig
+ * @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
+ * @param string $connectionName
+ * @param StoreResolverInterface $storeResolver
+ */
+ public function __construct(
+ \Magento\Framework\Model\ResourceModel\Db\Context $context,
+ \Magento\Framework\Indexer\Table\StrategyInterface $tableStrategy,
+ \Magento\Eav\Model\Config $eavConfig,
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ \Magento\Framework\Module\Manager $moduleManager,
+ $connectionName = null,
+ StoreResolverInterface $storeResolver = null
+ ) {
+ parent::__construct($context, $tableStrategy, $eavConfig, $eventManager, $moduleManager, $connectionName);
+ $this->storeResolver = $storeResolver ?:
+ \Magento\Framework\App\ObjectManager::getInstance()->get(StoreResolverInterface::class);
+ }
+
/**
* Reindex temporary (price result data) for all products
*
@@ -146,6 +184,8 @@ protected function _applyConfigurableOption()
$this->_prepareConfigurableOptionAggregateTable();
$this->_prepareConfigurableOptionPriceTable();
+ $statusAttribute = $this->_getAttribute(ProductInterface::STATUS);
+
$select = $connection->select()->from(
['i' => $this->_getDefaultFinalPriceTable()],
[]
@@ -162,11 +202,26 @@ protected function _applyConfigurableOption()
[]
)->where(
'le.required_options=0'
+ )->joinLeft(
+ ['status_global_attr' => $statusAttribute->getBackendTable()],
+ "status_global_attr.entity_id = le.entity_id"
+ . ' AND status_global_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
+ . ' AND status_global_attr.store_id = ' . Store::DEFAULT_STORE_ID,
+ []
+ )->joinLeft(
+ ['status_attr' => $statusAttribute->getBackendTable()],
+ "status_attr.entity_id = le.entity_id"
+ . ' AND status_attr.attribute_id = ' . (int)$statusAttribute->getAttributeId()
+ . ' AND status_attr.store_id = ' . $this->storeResolver->getCurrentStoreId(),
+ []
+ )->where(
+ 'IFNULL(status_attr.value, status_global_attr.value) = ?',
+ ProductStatus::STATUS_ENABLED
)->group(
['l.parent_id', 'i.customer_group_id', 'i.website_id', 'l.product_id']
);
$priceColumn = $this->_addAttributeToSelect($select, 'price', 'l.product_id', 0, null, true);
- $tierPriceColumn = $connection->getCheckSql("MIN(i.tier_price) IS NOT NULL", "i.tier_price", 'NULL');
+ $tierPriceColumn = $connection->getIfNullSql('MIN(i.tier_price)', 'NULL');
$select->columns(
['price' => $priceColumn, 'tier_price' => $tierPriceColumn]
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
index d29c55e477d27..872538d9babab 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
@@ -18,26 +18,6 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac
/** @var Configurable */
private $configurable;
- /**
- * @var RequestSafetyInterface
- */
- private $requestSafety;
-
- /**
- * @var ResourceConnection
- */
- private $resource;
-
- /**
- * @var LinkedProductSelectBuilderInterface
- */
- private $linkedProductSelectBuilder;
-
- /**
- * @var CollectionFactory
- */
- private $collectionFactory;
-
/**
* @var ProductInterface[]
*/
@@ -49,6 +29,7 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac
* @param LinkedProductSelectBuilderInterface $linkedProductSelectBuilder
* @param CollectionFactory $collectionFactory
* @param RequestSafetyInterface $requestSafety
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
Configurable $configurable,
@@ -58,10 +39,6 @@ public function __construct(
RequestSafetyInterface $requestSafety
) {
$this->configurable = $configurable;
- $this->resource = $resourceConnection;
- $this->linkedProductSelectBuilder = $linkedProductSelectBuilder;
- $this->collectionFactory = $collectionFactory;
- $this->requestSafety = $requestSafety;
}
/**
@@ -70,19 +47,7 @@ public function __construct(
public function getProducts(ProductInterface $product)
{
if (!isset($this->products[$product->getId()])) {
- if ($this->requestSafety->isSafeMethod()) {
- $productIds = $this->resource->getConnection()->fetchCol(
- '(' . implode(') UNION (', $this->linkedProductSelectBuilder->build($product->getId())) . ')'
- );
-
- $this->products[$product->getId()] = $this->collectionFactory->create()
- ->addIdFilter($productIds)
- ->addAttributeToSelect('*')
- ->addPriceData()
- ->addTierPriceData();
- } else {
- $this->products[$product->getId()] = $this->configurable->getUsedProducts($product);
- }
+ $this->products[$product->getId()] = $this->configurable->getUsedProducts($product);
}
return $this->products[$product->getId()];
}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php
index 0e87e35b3c0d6..68e82ed76a23f 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php
@@ -6,7 +6,6 @@
namespace Magento\ConfigurableProduct\Pricing\Price;
-use Magento\Catalog\Model\Product;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Pricing\PriceCurrencyInterface;
@@ -29,23 +28,27 @@ class ConfigurablePriceResolver implements PriceResolverInterface
protected $configurable;
/**
- * @var ConfigurableOptionsProviderInterface
+ * @var LowestPriceOptionsProviderInterface
*/
- private $configurableOptionsProvider;
+ private $lowestPriceOptionsProvider;
/**
* @param PriceResolverInterface $priceResolver
* @param Configurable $configurable
* @param PriceCurrencyInterface $priceCurrency
+ * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider
*/
public function __construct(
PriceResolverInterface $priceResolver,
Configurable $configurable,
- PriceCurrencyInterface $priceCurrency
+ PriceCurrencyInterface $priceCurrency,
+ LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider = null
) {
$this->priceResolver = $priceResolver;
$this->configurable = $configurable;
$this->priceCurrency = $priceCurrency;
+ $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?:
+ ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class);
}
/**
@@ -57,29 +60,16 @@ public function resolvePrice(\Magento\Framework\Pricing\SaleableInterface $produ
{
$price = null;
- foreach ($this->getConfigurableOptionsProvider()->getProducts($product) as $subProduct) {
+ foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) {
$productPrice = $this->priceResolver->resolvePrice($subProduct);
$price = $price ? min($price, $productPrice) : $productPrice;
}
- if (!$price) {
+ if ($price === null) {
throw new \Magento\Framework\Exception\LocalizedException(
- __('Configurable product "%1" do not have sub-products', $product->getName())
+ __('Configurable product "%1" does not have sub-products', $product->getSku())
);
}
return (float)$price;
}
-
- /**
- * @return \Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface
- * @deprecated
- */
- private function getConfigurableOptionsProvider()
- {
- if (null === $this->configurableOptionsProvider) {
- $this->configurableOptionsProvider = ObjectManager::getInstance()
- ->get(ConfigurableOptionsProviderInterface::class);
- }
- return $this->configurableOptionsProvider;
- }
}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
index d1261cf7e360d..04f5803991ca7 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
@@ -44,22 +44,31 @@ class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegu
*/
private $configurableOptionsProvider;
+ /**
+ * @var LowestPriceOptionsProviderInterface
+ */
+ private $lowestPriceOptionsProvider;
+
/**
* @param \Magento\Framework\Pricing\SaleableInterface $saleableItem
* @param float $quantity
* @param \Magento\Framework\Pricing\Adjustment\CalculatorInterface $calculator
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
* @param PriceResolverInterface $priceResolver
+ * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider
*/
public function __construct(
\Magento\Framework\Pricing\SaleableInterface $saleableItem,
$quantity,
\Magento\Framework\Pricing\Adjustment\CalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
- PriceResolverInterface $priceResolver
+ PriceResolverInterface $priceResolver,
+ LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider = null
) {
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
$this->priceResolver = $priceResolver;
+ $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?:
+ ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class);
}
/**
@@ -88,7 +97,6 @@ public function getAmount()
public function getMaxRegularAmount()
{
if (null === $this->maxRegularAmount) {
- $this->maxRegularAmount = $this->doGetMaxRegularAmount();
$this->maxRegularAmount = $this->doGetMaxRegularAmount() ?: false;
}
return $this->maxRegularAmount;
@@ -96,7 +104,7 @@ public function getMaxRegularAmount()
}
/**
- * Get max regular amount. Template method
+ * Get max regular amount
*
* @return \Magento\Framework\Pricing\Amount\AmountInterface
*/
@@ -124,14 +132,14 @@ public function getMinRegularAmount()
}
/**
- * Get min regular amount. Template method
+ * Get min regular amount
*
* @return \Magento\Framework\Pricing\Amount\AmountInterface
*/
protected function doGetMinRegularAmount()
{
$minAmount = null;
- foreach ($this->getUsedProducts() as $product) {
+ foreach ($this->lowestPriceOptionsProvider->getProducts($this->product) as $product) {
$childPriceAmount = $product->getPriceInfo()->getPrice(self::PRICE_CODE)->getAmount();
if (!$minAmount || ($childPriceAmount->getValue() < $minAmount->getValue())) {
$minAmount = $childPriceAmount;
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php
new file mode 100644
index 0000000000000..3f88178a3ab44
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php
@@ -0,0 +1,65 @@
+resource = $resourceConnection;
+ $this->linkedProductSelectBuilder = $linkedProductSelectBuilder;
+ $this->collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProducts(ProductInterface $product)
+ {
+ $productIds = $this->resource->getConnection()->fetchCol(
+ '(' . implode(') UNION (', $this->linkedProductSelectBuilder->build($product->getId())) . ')'
+ );
+
+ $lowestPriceChildProducts = $this->collectionFactory->create()
+ ->addIdFilter($productIds)
+ ->addAttributeToSelect('*')
+ ->addPriceData()
+ ->addTierPriceData()
+ ->getItems();
+ return $lowestPriceChildProducts;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProviderInterface.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProviderInterface.php
new file mode 100644
index 0000000000000..8d260c3073587
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProviderInterface.php
@@ -0,0 +1,20 @@
+configurableOptionsProvider = $configurableOptionsProvider;
parent::__construct($context, $saleableItem, $price, $rendererPool, $data);
+ $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?:
+ ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class);
}
/**
@@ -48,7 +54,7 @@ public function __construct(
public function hasSpecialPrice()
{
$product = $this->getSaleableItem();
- foreach ($this->configurableOptionsProvider->getProducts($product) as $subProduct) {
+ foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) {
$regularPrice = $subProduct->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getValue();
$finalPrice = $subProduct->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE)->getValue();
if ($finalPrice < $regularPrice) {
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php
new file mode 100644
index 0000000000000..8db61bb5e0a43
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php
@@ -0,0 +1,106 @@
+configurable = $this->getMock($className, ['getUsedProducts'], [], '', false);
+
+ $className = \Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface::class;
+ $this->priceResolver = $this->getMockForAbstractClass($className, [], '', false, true, true, ['resolvePrice']);
+
+ $this->lowestPriceOptionsProvider = $this->getMock(LowestPriceOptionsProviderInterface::class);
+
+ $objectManager = new ObjectManager($this);
+ $this->resolver = $objectManager->getObject(
+ \Magento\ConfigurableProduct\Pricing\Price\ConfigurablePriceResolver::class,
+ [
+ 'priceResolver' => $this->priceResolver,
+ 'configurable' => $this->configurable,
+ 'lowestPriceOptionsProvider' => $this->lowestPriceOptionsProvider,
+ ]
+ );
+ }
+
+ /**
+ * situation: There are no used products, thus there are no prices
+ *
+ * @expectedException \Magento\Framework\Exception\LocalizedException
+ */
+ public function testResolvePriceWithNoPrices()
+ {
+ $product = $this->getMockBuilder(
+ \Magento\Catalog\Model\Product::class
+ )->disableOriginalConstructor()->getMock();
+
+ $product->expects($this->once())->method('getSku')->willReturn('Kiwi');
+
+ $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([]);
+
+ $this->resolver->resolvePrice($product);
+ }
+
+ /**
+ * situation: one product is supplying the price, which could be a price of zero (0)
+ *
+ * @dataProvider testResolvePriceDataProvider
+ */
+ public function testResolvePrice($expectedValue)
+ {
+ $price = $expectedValue;
+
+ $product = $this->getMockBuilder(
+ \Magento\Catalog\Model\Product::class
+ )->disableOriginalConstructor()->getMock();
+
+ $product->expects($this->never())->method('getSku');
+
+ $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([$product]);
+ $this->priceResolver->expects($this->once())
+ ->method('resolvePrice')
+ ->with($product)
+ ->willReturn($price);
+
+ $this->assertEquals($expectedValue, $this->resolver->resolvePrice($product));
+ }
+
+ /**
+ * @return array
+ */
+ public function testResolvePriceDataProvider()
+ {
+ return [
+ 'price of zero' => [0.00],
+ 'price of five' => [5],
+ ];
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
index 4dbcfed531525..b102e1d81f48e 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
@@ -7,8 +7,9 @@
use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Catalog\Pricing\Price\RegularPrice;
-use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface;
+use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface;
use Magento\ConfigurableProduct\Pricing\Render\FinalPriceBox;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase
{
@@ -33,9 +34,9 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase
private $rendererPool;
/**
- * @var ConfigurableOptionsProviderInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var LowestPriceOptionsProviderInterface|\PHPUnit_Framework_MockObject_MockObject
*/
- private $configurableOptionsProvider;
+ private $lowestPriceOptionsProvider;
/**
* @var FinalPriceBox
@@ -59,15 +60,18 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
- $this->configurableOptionsProvider = $this->getMockBuilder(ConfigurableOptionsProviderInterface::class)
+ $this->lowestPriceOptionsProvider = $this->getMockBuilder(LowestPriceOptionsProviderInterface::class)
->getMockForAbstractClass();
- $this->model = new FinalPriceBox(
- $this->context,
- $this->saleableItem,
- $this->price,
- $this->rendererPool,
- $this->configurableOptionsProvider
+ $this->model = (new ObjectManager($this))->getObject(
+ FinalPriceBox::class,
+ [
+ 'context' => $this->context,
+ 'saleableItem' => $this->saleableItem,
+ 'price' => $this->price,
+ 'rendererPool' => $this->rendererPool,
+ 'lowestPriceOptionsProvider' => $this->lowestPriceOptionsProvider,
+ ]
);
}
@@ -115,7 +119,7 @@ public function testHasSpecialPrice(
->method('getPriceInfo')
->willReturn($priceInfoMock);
- $this->configurableOptionsProvider->expects($this->once())
+ $this->lowestPriceOptionsProvider->expects($this->once())
->method('getProducts')
->with($this->saleableItem)
->willReturn([$productMock]);
diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml
index 3c9394d65dbc1..357d27a4549bc 100644
--- a/app/code/Magento/ConfigurableProduct/etc/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/di.xml
@@ -13,6 +13,7 @@
+
diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Api/StockItemSaveTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Api/StockItemSaveTest.php
new file mode 100644
index 0000000000000..0dc5c3ef620c3
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Api/StockItemSaveTest.php
@@ -0,0 +1,43 @@
+get(ProductRepositoryInterface::class);
+ /** @var ProductInterface $product */
+ $product = $productRepository->get('simple', false, null, true);
+
+ /** @var ProductExtensionInterface $ea */
+ $ea = $product->getExtensionAttributes();
+ $ea->getStockItem()->setQty(555);
+ $productRepository->save($product);
+
+ $product = $productRepository->get('simple', false, null, true);
+ $this->assertEquals(555, $product->getExtensionAttributes()->getStockItem()->getQty());
+
+ $stockItem = $product->getExtensionAttributes()->getStockItem();
+ $stockItem->setQty(200);
+ /** @var StockItemRepositoryInterface $stockItemRepository */
+ $stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class);
+ $stockItemRepository->save($stockItem);
+ $this->assertEquals(200, $product->getExtensionAttributes()->getStockItem()->getQty());
+
+ $product = $productRepository->get('simple', false, null, true);
+ $this->assertEquals(200, $product->getExtensionAttributes()->getStockItem()->getQty());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php
new file mode 100644
index 0000000000000..7709332597c43
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php
@@ -0,0 +1,106 @@
+storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class);
+ $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductFinalPriceIfOneOfChildIsDisabled()
+ {
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(10, $configurableProduct->getMinimalPrice());
+
+ $childProduct = $this->productRepository->getById(10, false, null, true);
+ $childProduct->setStatus(Status::STATUS_DISABLED);
+ // update in global scope
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $this->storeManager->setCurrentStore(Store::ADMIN_CODE);
+ $this->productRepository->save($childProduct);
+ $this->storeManager->setCurrentStore($currentStoreId);
+
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(20, $configurableProduct->getMinimalPrice());
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore()
+ {
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(10, $configurableProduct->getMinimalPrice());
+
+ $childProduct = $this->productRepository->getById(10, false, null, true);
+ $childProduct->setStatus(Status::STATUS_DISABLED);
+
+ // update in default store scope
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $defaultStore = $this->storeManager->getDefaultStoreView();
+ $this->storeManager->setCurrentStore($defaultStore->getId());
+ $this->productRepository->save($childProduct);
+ $this->storeManager->setCurrentStore($currentStoreId);
+
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(20, $configurableProduct->getMinimalPrice());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php
new file mode 100644
index 0000000000000..60bd682587f9c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php
@@ -0,0 +1,133 @@
+storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class);
+ $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);
+ $this->lowestPriceOptionsProvider = Bootstrap::getObjectManager()->get(
+ LowestPriceOptionsProviderInterface::class
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductsIfOneOfChildIsDisabled()
+ {
+ $configurableProduct = $this->productRepository->getById(1, false, null, true);
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(10, $lowestPriceChildrenProduct->getPrice());
+
+ // load full aggregation root
+ $lowestPriceChildProduct = $this->productRepository->get(
+ $lowestPriceChildrenProduct->getSku(),
+ false,
+ null,
+ true
+ );
+ $lowestPriceChildProduct->setStatus(Status::STATUS_DISABLED);
+ // update in global scope
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $this->storeManager->setCurrentStore(Store::ADMIN_CODE);
+ $this->productRepository->save($lowestPriceChildProduct);
+ $this->storeManager->setCurrentStore($currentStoreId);
+
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(20, $lowestPriceChildrenProduct->getPrice());
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductsIfOneOfChildIsDisabledPerStore()
+ {
+ $configurableProduct = $this->productRepository->getById(1, false, null, true);
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(10, $lowestPriceChildrenProduct->getPrice());
+
+ // load full aggregation root
+ $lowestPriceChildProduct = $this->productRepository->get(
+ $lowestPriceChildrenProduct->getSku(),
+ false,
+ null,
+ true
+ );
+ $lowestPriceChildProduct->setStatus(Status::STATUS_DISABLED);
+ // update in default store scope
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $defaultStore = $this->storeManager->getDefaultStoreView();
+ $this->storeManager->setCurrentStore($defaultStore->getId());
+ $this->productRepository->save($lowestPriceChildProduct);
+ $this->storeManager->setCurrentStore($currentStoreId);
+
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(20, $lowestPriceChildrenProduct->getPrice());
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductsIfOneOfChildIsOutOfStock()
+ {
+ $configurableProduct = $this->productRepository->getById(1, false, null, true);
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(10, $lowestPriceChildrenProduct->getPrice());
+
+ // load full aggregation root
+ $lowestPriceChildProduct = $this->productRepository->get(
+ $lowestPriceChildrenProduct->getSku(),
+ false,
+ null,
+ true
+ );
+ $stockItem = $lowestPriceChildProduct->getExtensionAttributes()->getStockItem();
+ $stockItem->setIsInStock(0);
+ $this->productRepository->save($lowestPriceChildProduct);
+
+ $lowestPriceChildrenProducts = $this->lowestPriceOptionsProvider->getProducts($configurableProduct);
+ $this->assertCount(1, $lowestPriceChildrenProducts);
+ $lowestPriceChildrenProduct = reset($lowestPriceChildrenProducts);
+ $this->assertEquals(20, $lowestPriceChildrenProduct->getPrice());
+ }
+}