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()); + } +}