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 5c3fd4730aaed4907cbf197f6a5298060ad12acf..0bb468b77ee6ea18df11e7fd349e8315c6f74318 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 @@ class Stock extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend 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 0000000000000000000000000000000000000000..e6a995b654703402175f79ccc6e50b5172db5fd4 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/BaseSelectProcessorInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\ResourceModel\Product; + +use Magento\Framework\DB\Select; + +/** + * Interface BaseSelectProcessorInterface + * @api + */ +interface BaseSelectProcessorInterface +{ + /** + * Product table alias + */ + const PRODUCT_TABLE_ALIAS = 'child'; + + /** + * @param Select $select + * @return Select + */ + public function process(Select $select); +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..86f5a23708400ef40a58677f07e57552a39bd6bd --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CompositeBaseSelectProcessor.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\ResourceModel\Product; + +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\InputException; + +/** + * Class CompositeBaseSelectProcessor + */ +class CompositeBaseSelectProcessor implements BaseSelectProcessorInterface +{ + /** + * @var BaseSelectProcessorInterface[] + */ + private $baseSelectProcessors; + + /** + * @param BaseSelectProcessorInterface[] $baseSelectProcessors + * @throws InputException + */ + public function __construct( + array $baseSelectProcessors + ) { + foreach ($baseSelectProcessors as $baseSelectProcessor) { + if (!$baseSelectProcessor instanceof BaseSelectProcessorInterface) { + throw new InputException( + __('Processor %1 doesn\'t implement BaseSelectProcessorInterface', get_class($baseSelectProcessor)) + ); + } + } + $this->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 ea72691ea003939fc277299bee214da316f4f757..3aa6642c82d845eb6ca1143f9c82484c53c659f9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/LinkedProductSelectBuilderByIndexPrice.php @@ -6,7 +6,8 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Indexer; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; @@ -32,22 +33,31 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild */ private $metadataPool; + /** + * @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\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\App\ResourceConnection $resourceConnection, \Magento\Customer\Model\Session $customerSession, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->customerSession = $customerSession; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -58,24 +68,27 @@ class LinkedProductSelectBuilderByIndexPrice implements LinkedProductSelectBuild $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); - return [$this->resource->getConnection()->select() + $priceSelect = $this->resource->getConnection()->select() ->from(['parent' => $productTable], '') ->joinInner( ['link' => $this->resource->getTableName('catalog_product_relation')], "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $this->resource->getTableName('catalog_product_index_price')], - 't.entity_id = child.entity_id', + sprintf('t.entity_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_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 d325ab1a9a08d1175c0fedb2a94c10ac5d7dc3c2..7caa72b367979bc4ecbded5641490a59f7835928 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByBasePrice.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; @@ -37,25 +38,34 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde */ private $metadataPool; + /** + * @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 \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @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\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->eavConfig = $eavConfig; $this->catalogHelper = $catalogHelper; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -74,18 +84,19 @@ class LinkedProductSelectBuilderByBasePrice implements LinkedProductSelectBuilde "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), ['entity_id'] )->joinInner( ['t' => $priceAttribute->getBackendTable()], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_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 792a8f5b86d102204a93491c0413ae2839b16a73..68eaf206e293fea3e90c04e9dfb7bc0329704162 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderBySpecialPrice.php @@ -7,9 +7,13 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Store\Model\Store; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBuilderInterface { /** @@ -47,6 +51,11 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection @@ -55,6 +64,7 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, @@ -63,7 +73,8 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui \Magento\Catalog\Helper\Data $catalogHelper, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -72,6 +83,8 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui $this->dateTime = $dateTime; $this->localeDate = $localeDate; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -95,12 +108,12 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $specialPriceAttribute->getBackendTable()], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] )->joinLeft( ['special_from' => $specialPriceFromDate->getBackendTable()], @@ -116,7 +129,7 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui $specialPriceToDate->getAttributeId() ), '' - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_id = ?', $productId) ->where('t.attribute_id = ?', $specialPriceAttribute->getAttributeId()) ->where('t.value IS NOT NULL') ->where( @@ -127,6 +140,7 @@ class LinkedProductSelectBuilderBySpecialPrice implements LinkedProductSelectBui $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 d2d6d89c0a2a5828290e1191147451e4f6d20b7b..25bf83f837de72cec501379378e6960084d6b1fd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderByTierPrice.php @@ -6,7 +6,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilderInterface @@ -41,25 +41,34 @@ class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilde */ private $metadataPool; + /** + * @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 \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @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\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; $this->customerSession = $customerSession; $this->catalogHelper = $catalogHelper; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -77,18 +86,19 @@ class LinkedProductSelectBuilderByTierPrice implements LinkedProductSelectBuilde "link.parent_id = parent.$linkField", [] )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), ['entity_id'] )->joinInner( ['t' => $this->resource->getTableName('catalog_product_entity_tier_price')], - "t.$linkField = child.$linkField", + sprintf('t.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), [] - )->where('parent.entity_id = ? ', $productId) + )->where('parent.entity_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 0000000000000000000000000000000000000000..656998113fdb92e50e5177db4876651cfcd2023a --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/StatusBaseSelectProcessor.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Model\ResourceModel\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Eav\Model\Config; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Class StatusBaseSelectProcessor + */ +class StatusBaseSelectProcessor implements BaseSelectProcessorInterface +{ + /** + * @var Config + */ + private $eavConfig; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param Config $eavConfig + * @param MetadataPool $metadataPool + */ + public function __construct( + Config $eavConfig, + MetadataPool $metadataPool + ) { + $this->eavConfig = $eavConfig; + $this->metadataPool = $metadataPool; + } + + /** + * @param Select $select + * @return Select + */ + public function process(Select $select) + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $statusAttribute = $this->eavConfig->getAttribute(Product::ENTITY, ProductInterface::STATUS); + + $select->join( + ['status_attr' => $statusAttribute->getBackendTable()], + sprintf('status_attr.%s = %s.%1$s', $linkField, self::PRODUCT_TABLE_ALIAS), + [] + ) + ->where('status_attr.attribute_id = ?', $statusAttribute->getAttributeId()) + ->where('status_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 ec9cb624db79af510c2625ffee2680ede7653a19..32d62fd7a998e7bf820b7b0c26b6a7202350c26a 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 @@ class StockTest extends \PHPUnit_Framework_TestCase $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 0000000000000000000000000000000000000000..d296474c202f091b4c1435453ec7941007e10885 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/CompositeBaseSelectProcessorTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class CompositeBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp() + { + $this->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 0000000000000000000000000000000000000000..0909f754a01c2c937e8c3692c52b1a256a27b0c3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Product/StatusBaseSelectProcessorTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Catalog\Model\ResourceModel\Product\StatusBaseSelectProcessor; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AttributeInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class StatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + private $eavConfig; + + /** + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject + */ + private $metadataPool; + + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $select; + + /** + * @var StatusBaseSelectProcessor + */ + private $statusBaseSelectProcessor; + + protected function setUp() + { + $this->eavConfig = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class)->disableOriginalConstructor()->getMock(); + $this->select = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock(); + + $this->statusBaseSelectProcessor = (new ObjectManager($this))->getObject(StatusBaseSelectProcessor::class, [ + 'eavConfig' => $this->eavConfig, + 'metadataPool' => $this->metadataPool, + ]); + } + + public function testProcess() + { + $linkField = 'link_field'; + $backendTable = 'backend_table'; + $attributeId = 'attribute_id'; + + $metadata = $this->getMock(EntityMetadataInterface::class); + $metadata->expects($this->once()) + ->method('getLinkField') + ->willReturn($linkField); + $this->metadataPool->expects($this->once()) + ->method('getMetadata') + ->with(ProductInterface::class) + ->willReturn($metadata); + + $statusAttribute = $this->getMockBuilder(AttributeInterface::class) + ->setMethods(['getBackendTable', 'getAttributeId']) + ->getMock(); + $statusAttribute->expects($this->once()) + ->method('getBackendTable') + ->willReturn($backendTable); + $statusAttribute->expects($this->once()) + ->method('getAttributeId') + ->willReturn($attributeId); + $this->eavConfig->expects($this->once()) + ->method('getAttribute') + ->with(Product::ENTITY, ProductInterface::STATUS) + ->willReturn($statusAttribute); + + $this->select->expects($this->once()) + ->method('join') + ->with( + ['status_attr' => $backendTable], + sprintf('status_attr.%s = %s.%1$s', $linkField, BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->willReturnSelf(); + $this->select->expects($this->at(1)) + ->method('where') + ->with('status_attr.attribute_id = ?', $attributeId) + ->willReturnSelf(); + $this->select->expects($this->at(2)) + ->method('where') + ->with('status_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 d191f0332f5f28d28f22e981f8ebc74f0c0c8b98..6b39520ae021e9eb3bfafa20bda03e42c7db907a 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -837,4 +837,12 @@ <type name="Magento\Quote\Model\Quote\Item\ToOrderItem"> <plugin name="copy_quote_files_to_order" type="Magento\Catalog\Model\Plugin\QuoteItemProductOption"/> </type> + <preference for="Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface" type="Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor" /> + <type name="Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor"> + <arguments> + <argument name="baseSelectProcessors" xsi:type="array"> + <item name="status" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\StatusBaseSelectProcessor</item> + </argument> + </arguments> + </type> </config> 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 0000000000000000000000000000000000000000..829fa8decda7d74b554810ba17118faba2ca970c --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Product/StockStatusBaseSelectProcessor.php @@ -0,0 +1,51 @@ +<?php +/** + * + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; + +/** + * Class StockStatusBaseSelectProcessor + */ +class StockStatusBaseSelectProcessor implements BaseSelectProcessorInterface +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->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], + sprintf('stock.product_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->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 0000000000000000000000000000000000000000..4756e42ffe602bfc3b258656e0f2abe490a3acc1 --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/ResourceModel/Product/StockStatusBaseSelectProcessorTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Test\Unit\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\CatalogInventory\Model\ResourceModel\Product\StockStatusBaseSelectProcessor; +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + +class StockStatusBaseSelectProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var ResourceConnection|\PHPUnit_Framework_MockObject_MockObject + */ + private $resource; + + /** + * @var Select|\PHPUnit_Framework_MockObject_MockObject + */ + private $select; + + /** + * @var StockStatusBaseSelectProcessor + */ + private $stockStatusBaseSelectProcessor; + + protected function setUp() + { + $this->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], + sprintf('stock.product_id = %s.entity_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + [] + ) + ->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 71b42ef89f73ce981f25393179fbbd05f59a4d08..e3ca5c01cedab6b5041a16248308208b3cccbcf5 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -79,4 +79,11 @@ <argument name="indexerProcessor" xsi:type="object">Magento\CatalogInventory\Model\Indexer\Stock\Processor</argument> </arguments> </type> + <type name="Magento\Catalog\Model\ResourceModel\Product\CompositeBaseSelectProcessor"> + <arguments> + <argument name="baseSelectProcessors" xsi:type="array"> + <item name="stock_status" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Product\StockStatusBaseSelectProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php index 7d8d44dcbb68ad2539f0556fb7aec8eebe0caa2e..55b76eb225028a1a3d96d115066a4abc4b78fd60 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Product/LinkedProductSelectBuilderByCatalogRulePrice.php @@ -6,7 +6,8 @@ namespace Magento\CatalogRule\Model\ResourceModel\Product; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DB\Select; use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; @@ -42,6 +43,11 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec */ private $metadataPool; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\App\ResourceConnection $resourceConnection @@ -49,6 +55,7 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param BaseSelectProcessorInterface $baseSelectProcessor */ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, @@ -56,7 +63,8 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec \Magento\Customer\Model\Session $customerSession, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->storeManager = $storeManager; $this->resource = $resourceConnection; @@ -64,6 +72,8 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec $this->dateTime = $dateTime; $this->localeDate = $localeDate; $this->metadataPool = $metadataPool; + $this->baseSelectProcessor = (null !== $baseSelectProcessor) + ? $baseSelectProcessor : ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -76,25 +86,28 @@ class LinkedProductSelectBuilderByCatalogRulePrice implements LinkedProductSelec $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); $productTable = $this->resource->getTableName('catalog_product_entity'); - return [$this->resource->getConnection()->select() - ->from(['parent' => $productTable], '') - ->joinInner( - ['link' => $this->resource->getTableName('catalog_product_relation')], - "link.parent_id = parent.$linkField", - [] - )->joinInner( - ['child' => $productTable], - "child.entity_id = link.child_id", - ['entity_id'] - )->joinInner( - ['t' => $this->resource->getTableName('catalogrule_product_price')], - 't.product_id = child.entity_id', - [] - )->where('parent.entity_id = ? ', $productId) + $priceSelect = $this->resource->getConnection()->select() + ->from(['parent' => $productTable], '') + ->joinInner( + ['link' => $this->resource->getTableName('catalog_product_relation')], + "link.parent_id = parent.$linkField", + [] + )->joinInner( + [BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS => $productTable], + sprintf('%s.entity_id = link.child_id', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS), + ['entity_id'] + )->joinInner( + ['t' => $this->resource->getTableName('catalogrule_product_price')], + sprintf('t.product_id = %s.%s', BaseSelectProcessorInterface::PRODUCT_TABLE_ALIAS, $linkField), + [] + )->where('parent.entity_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/Block/Plugin/Product/Media/Gallery.php b/app/code/Magento/ConfigurableProduct/Block/Plugin/Product/Media/Gallery.php deleted file mode 100644 index a4390bf94865084fd27d401e47982c3bdb3cabbf..0000000000000000000000000000000000000000 --- a/app/code/Magento/ConfigurableProduct/Block/Plugin/Product/Media/Gallery.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -/** - * Copyright © 2016 Magento. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\ConfigurableProduct\Block\Plugin\Product\Media; - -use Magento\ConfigurableProduct\Model\Product\Type\Configurable; - -/** - * Class Gallery - */ -class Gallery extends \Magento\Catalog\Block\Product\View\AbstractView -{ - /** - * @var \Magento\Catalog\Model\Product\Gallery\ReadHandler - */ - private $productGalleryReadHandler; - - /** - * @var \Magento\Framework\Json\EncoderInterface - */ - private $jsonEncoder; - - /** - * @var \Magento\Framework\Json\DecoderInterface - */ - private $jsonDecoder; - - /** - * Gallery constructor. - * @param \Magento\Catalog\Block\Product\Context $context - * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils - * @param \Magento\Catalog\Model\Product\Gallery\ReadHandler $productGalleryReadHandler - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder - * @param \Magento\Framework\Json\DecoderInterface $jsonDecoder - * @param array $data - */ - public function __construct( - \Magento\Catalog\Block\Product\Context $context, - \Magento\Framework\Stdlib\ArrayUtils $arrayUtils, - \Magento\Catalog\Model\Product\Gallery\ReadHandler $productGalleryReadHandler, - \Magento\Framework\Json\EncoderInterface $jsonEncoder, - \Magento\Framework\Json\DecoderInterface $jsonDecoder, - array $data = [] - ) { - $this->productGalleryReadHandler = $productGalleryReadHandler; - $this->jsonEncoder = $jsonEncoder; - $this->jsonDecoder = $jsonDecoder; - parent::__construct($context, $arrayUtils, $data); - } - - /** - * @param \Magento\Catalog\Block\Product\View\Gallery $subject - * @param string $result - * @return string - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetOptionsMediaGalleryDataJson( - \Magento\Catalog\Block\Product\View\Gallery $subject, - $result - ) { - $result = $this->jsonDecoder->decode($result); - if ($this->getProduct()->getTypeId() == 'configurable') { - /** @var Configurable $productType */ - $productType = $this->getProduct()->getTypeInstance(); - $products = $productType->getUsedProducts($this->getProduct()); - $attributes = $productType->getConfigurableAttributesAsArray($this->getProduct()); - /** @var \Magento\Catalog\Model\Product $product */ - foreach ($attributes as $attribute) { - foreach ($products as $product) { - $attributeValue = $product->getData($attribute['attribute_code']); - if ($attributeValue) { - $key = $attribute['attribute_code'] . '_' . $attributeValue; - $result[$key] = $this->getProductGallery($product); - } - } - } - } - return $this->jsonEncoder->encode($result); - } - - /** - * @param \Magento\Catalog\Model\Product $product - * @return array - */ - private function getProductGallery($product) - { - $result = []; - $this->productGalleryReadHandler->execute($product); - $images = $product->getMediaGalleryImages(); - foreach ($images as $image) { - $result[] = [ - 'mediaType' => $image->getMediaType(), - 'videoUrl' => $image->getVideoUrl(), - 'isBase' => $product->getImage() == $image->getFile(), - ]; - } - return $result; - } -} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php index dcb75fe725dc20d74aa245831e8046dbd9665949..872538d9babab02aec00763ee317298f9c93697e 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 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac RequestSafetyInterface $requestSafety ) { $this->configurable = $configurable; - $this->resource = $resourceConnection; - $this->linkedProductSelectBuilder = $linkedProductSelectBuilder; - $this->collectionFactory = $collectionFactory; - $this->requestSafety = $requestSafety; } /** @@ -70,17 +47,7 @@ class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterfac 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() - ->addAttributeToSelect(['price', 'special_price']) - ->addIdFilter($productIds); - } 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/ConfigurableOptionsProviderInterface.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProviderInterface.php index c7ac9446f42e306b78badec5f4b0825ce8c6f6c1..c0f2c218cc77c80a8e6785aba97431baea793975 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProviderInterface.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProviderInterface.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; /** * Provide configurable sub-products for price calculation + * @api */ interface ConfigurableOptionsProviderInterface { diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index 75d08b5aa419bdec6d9db0c3112f2aa1b7e836a6..68e82ed76a23fbdda404c32eb7406d3117b3cd0e 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,7 +60,7 @@ class ConfigurablePriceResolver implements PriceResolverInterface { $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; } @@ -69,17 +72,4 @@ class ConfigurablePriceResolver implements PriceResolverInterface 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 14b788258ab75502183aaf0b3f842b1943fcec2d..7a710e2caacc96f0a20eae124d973de9fa6a4c49 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 @@ class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegu public function getMaxRegularAmount() { if (null === $this->maxRegularAmount) { - $this->maxRegularAmount = $this->doGetMaxRegularAmount(); $this->maxRegularAmount = $this->doGetMaxRegularAmount() ?: false; } return $this->maxRegularAmount; @@ -96,7 +104,7 @@ class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegu } /** - * Get max regular amount. Template method + * Get max regular amount * * @return \Magento\Framework\Pricing\Amount\AmountInterface */ @@ -124,14 +132,14 @@ class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegu } /** - * 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 0000000000000000000000000000000000000000..1b758866100eb8f52faa371e4bc1695ce385703f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProvider.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Pricing\Price; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\LinkedProductSelectBuilderInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; + +/** + * Retrieve list of products where each product contains lower price than others at least for one possible price type + */ +class LowestPriceOptionsProvider implements LowestPriceOptionsProviderInterface +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var LinkedProductSelectBuilderInterface + */ + private $linkedProductSelectBuilder; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param ResourceConnection $resourceConnection + * @param LinkedProductSelectBuilderInterface $linkedProductSelectBuilder + * @param CollectionFactory $collectionFactory + */ + public function __construct( + ResourceConnection $resourceConnection, + LinkedProductSelectBuilderInterface $linkedProductSelectBuilder, + CollectionFactory $collectionFactory + ) { + $this->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() + ->addAttributeToSelect(['price', 'special_price']) + ->addIdFilter($productIds) + ->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 0000000000000000000000000000000000000000..c0989a929be55ce46bdaa3451ba88a8a3ae8272a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionsProviderInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Pricing\Price; + +use Magento\Catalog\Api\Data\ProductInterface; + +/** + * Retrieve list of products where each product contains lower price than others at least for one possible price type + * @api + */ +interface LowestPriceOptionsProviderInterface +{ + /** + * @param ProductInterface $product + * @return \Magento\Catalog\Api\Data\ProductInterface[] + */ + public function getProducts(\Magento\Catalog\Api\Data\ProductInterface $product); +} diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php index 16a296a3554548d31f1692f397cd0124e272a1af..19bf0eee888f181902824505a2808f8ae5106c15 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Render/FinalPriceBox.php @@ -8,6 +8,8 @@ namespace Magento\ConfigurableProduct\Pricing\Render; 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\Framework\App\ObjectManager; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\Render\RendererPool; use Magento\Framework\Pricing\SaleableInterface; @@ -16,9 +18,9 @@ use Magento\Framework\View\Element\Template\Context; class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox { /** - * @var ConfigurableOptionsProviderInterface + * @var LowestPriceOptionsProviderInterface */ - private $configurableOptionsProvider; + private $lowestPriceOptionsProvider; /** * @param Context $context @@ -27,6 +29,8 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox * @param RendererPool $rendererPool * @param ConfigurableOptionsProviderInterface $configurableOptionsProvider * @param array $data + * @param LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( Context $context, @@ -34,10 +38,12 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox PriceInterface $price, RendererPool $rendererPool, ConfigurableOptionsProviderInterface $configurableOptionsProvider, - array $data = [] + array $data = [], + LowestPriceOptionsProviderInterface $lowestPriceOptionsProvider = null ) { - $this->configurableOptionsProvider = $configurableOptionsProvider; parent::__construct($context, $saleableItem, $price, $rendererPool, $data); + $this->lowestPriceOptionsProvider = $lowestPriceOptionsProvider ?: + ObjectManager::getInstance()->get(LowestPriceOptionsProviderInterface::class); } /** @@ -48,7 +54,7 @@ class FinalPriceBox extends \Magento\Catalog\Pricing\Render\FinalPriceBox 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/Block/Plugin/Product/Media/GalleryTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Plugin/Product/Media/GalleryTest.php deleted file mode 100644 index 60aa30c8d40784796192d8c1b7a2ea871e505a25..0000000000000000000000000000000000000000 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Plugin/Product/Media/GalleryTest.php +++ /dev/null @@ -1,111 +0,0 @@ -<?php -/** - * Copyright © 2016 Magento. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\ConfigurableProduct\Test\Unit\Block\Plugin\Product\Media; - -/** - * Class GalleryTest - */ -class GalleryTest extends \PHPUnit_Framework_TestCase -{ - /** - * @var \Magento\ConfigurableProduct\Block\Plugin\Product\Media\Gallery - */ - private $plugin; - - /** - * @var \Magento\Framework\Json\EncoderInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $jsonEncoder; - - /** - * @var \Magento\Framework\Json\DecoderInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $jsonDecoder; - - /** - * @var \Magento\Catalog\Model\Product\Gallery\ReadHandler - */ - private $galleryHandler; - - protected function setUp() - { - $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->galleryHandler = $this->getMockBuilder(\Magento\Catalog\Model\Product\Gallery\ReadHandler::class) - ->disableOriginalConstructor() - ->setMethods(['execute']) - ->getMock(); - - $this->jsonEncoder = $this->getMock(\Magento\Framework\Json\EncoderInterface::class); - $this->jsonDecoder = $this->getMock(\Magento\Framework\Json\DecoderInterface::class); - - $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['getTypeId', 'getTypeInstance']) - ->disableOriginalConstructor() - ->getMock(); - - $variationProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->setMethods(['setMediaGalleryEntries', 'getSku', 'getMediaGalleryImages', 'getImage', 'getData']) - ->disableOriginalConstructor() - ->getMock(); - $image = new \Magento\Framework\DataObject( - ['media_type' => 'type', 'video_url' => 'url', 'file' => 'image.jpg'] - ); - $variationProduct->expects($this->any())->method('setMediaGalleryEntries')->willReturn([]); - $variationProduct->expects($this->any())->method('getSku')->willReturn('sku'); - $variationProduct->expects($this->any())->method('getMediaGalleryImages')->willReturn([$image]); - $variationProduct->expects($this->any())->method('getImage')->willReturn('image.jpg'); - $variationProduct->expects($this->any())->method('getData')->with('configurable_attribute')->willReturn(1); - - $this->galleryHandler->expects($this->once())->method('execute')->with($variationProduct); - - $configurableType = $this->getMockBuilder(\Magento\ConfigurableProduct\Model\Product\Type\Configurable::class) - ->disableOriginalConstructor() - ->setMethods(['getUsedProducts', 'getConfigurableAttributesAsArray']) - ->getMock(); - $configurableType->expects($this->any())->method('getUsedProducts')->with($productMock) - ->willReturn([$variationProduct]); - $configurableType->expects($this->any())->method('getConfigurableAttributesAsArray')->with($productMock) - ->willReturn([['attribute_code' => 'configurable_attribute']]); - - $productMock->expects($this->any())->method('getTypeId')->willReturn('configurable'); - $productMock->expects($this->any())->method('getTypeInstance')->willReturn($configurableType); - - $this->plugin = $helper->getObject( - \Magento\ConfigurableProduct\Block\Plugin\Product\Media\Gallery::class, - [ - 'productGalleryReadHandler' => $this->galleryHandler, - 'jsonEncoder' => $this->jsonEncoder, - 'jsonDecoder' => $this->jsonDecoder - ] - ); - $this->plugin->setData('product', $productMock); - } - - public function testAfterGetOptions() - { - $resultJson = '[]'; - $this->jsonDecoder->expects($this->once())->method('decode')->with('[]')->willReturn([]); - $expected = [ - 'configurable_attribute_1' => [ - [ - 'mediaType' => 'type', - 'videoUrl' => 'url', - 'isBase' => true - ] - ] - ]; - $this->jsonEncoder->expects($this->any())->method('encode')->with($expected) - ->willReturn(json_encode($expected)); - - $blockMock = $this->getMockBuilder(\Magento\ProductVideo\Block\Product\View\Gallery::class) - ->disableOriginalConstructor() - ->getMock(); - - $result = $this->plugin->afterGetOptionsMediaGalleryDataJson($blockMock, $resultJson); - $this->assertEquals(json_encode($expected), $result); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php index 0e2cfc0630226d500db55db0fd0fb0b60bb4d121..8db61bb5e0a4322ff3d614348616dfc0e2f34e27 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php @@ -5,13 +5,15 @@ */ namespace Magento\ConfigurableProduct\Test\Unit\Pricing\Price; -use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface; +use Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase { - /** @var ConfigurableOptionsProviderInterface */ - private $cofigurableOptionProvider; + /** + * @var LowestPriceOptionsProviderInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $lowestPriceOptionsProvider; /** * @var \Magento\ConfigurableProduct\Pricing\Price\ConfigurablePriceResolver @@ -19,12 +21,12 @@ class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase protected $resolver; /** - * @var PHPUnit_Framework_MockObject_MockObject | \Magento\ConfigurableProduct\Model\Product\Type\Configurable + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\ConfigurableProduct\Model\Product\Type\Configurable */ protected $configurable; /** - * @var PHPUnit_Framework_MockObject_MockObject | \Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface + * @var \PHPUnit_Framework_MockObject_MockObject | \Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface */ protected $priceResolver; @@ -36,8 +38,7 @@ class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase $className = \Magento\ConfigurableProduct\Pricing\Price\PriceResolverInterface::class; $this->priceResolver = $this->getMockForAbstractClass($className, [], '', false, true, true, ['resolvePrice']); - $this->cofigurableOptionProvider = $this->getMockBuilder(ConfigurableOptionsProviderInterface::class) - ->disableOriginalConstructor()->getMock(); + $this->lowestPriceOptionsProvider = $this->getMock(LowestPriceOptionsProviderInterface::class); $objectManager = new ObjectManager($this); $this->resolver = $objectManager->getObject( @@ -45,7 +46,7 @@ class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase [ 'priceResolver' => $this->priceResolver, 'configurable' => $this->configurable, - 'configurableOptionsProvider' => $this->cofigurableOptionProvider, + 'lowestPriceOptionsProvider' => $this->lowestPriceOptionsProvider, ] ); } @@ -63,7 +64,7 @@ class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase $product->expects($this->once())->method('getSku')->willReturn('Kiwi'); - $this->cofigurableOptionProvider->expects($this->once())->method('getProducts')->willReturn([]); + $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([]); $this->resolver->resolvePrice($product); } @@ -83,8 +84,11 @@ class ConfigurablePriceResolverTest extends \PHPUnit_Framework_TestCase $product->expects($this->never())->method('getSku'); - $this->cofigurableOptionProvider->expects($this->once())->method('getProducts')->willReturn([$product]); - $this->priceResolver->expects($this->atLeastOnce())->method('resolvePrice')->willReturn($price); + $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)); } 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 4dbcfed5315252c62494b6ede30d4d14f9a598b2..b102e1d81f48ea421ce29a7cd914a758e94ccb68 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 @@ namespace Magento\ConfigurableProduct\Test\Unit\Pricing\Render; 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 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase ->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 @@ class FinalPriceBoxTest extends \PHPUnit_Framework_TestCase ->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 89b271de838f992e9acd93ecdd27bc321f59d54e..ca06ce5cc974bbab05232bdee33920ba87918ade 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -13,6 +13,7 @@ <preference for="Magento\ConfigurableProduct\Api\Data\OptionValueInterface" type="Magento\ConfigurableProduct\Model\Product\Type\Configurable\OptionValue" /> <preference for="Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface" type="Magento\ConfigurableProduct\Model\Quote\Item\ConfigurableItemOptionValue" /> <preference for="Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface" type="Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProvider" /> + <preference for="Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProviderInterface" type="Magento\ConfigurableProduct\Pricing\Price\LowestPriceOptionsProvider" /> <type name="Magento\CatalogInventory\Model\Quote\Item\QuantityValidator\Initializer\Option"> <plugin name="configurable_product" type="Magento\ConfigurableProduct\Model\Quote\Item\QuantityValidator\Initializer\Option\Plugin\ConfigurableProduct" sortOrder="50" /> @@ -142,9 +143,6 @@ <type name="Magento\Catalog\Model\Product\Attribute\Backend\Price"> <plugin name="configurable" type="Magento\ConfigurableProduct\Model\Plugin\PriceBackend" sortOrder="100" /> </type> - <type name="\Magento\ProductVideo\Block\Product\View\Gallery"> - <plugin name="product_video_gallery" type="\Magento\ConfigurableProduct\Block\Plugin\Product\Media\Gallery" /> - </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <arguments> <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Collection</argument> diff --git a/app/code/Magento/Developer/Model/View/Asset/PreProcessor/FrontendCompilation.php b/app/code/Magento/Developer/Model/View/Asset/PreProcessor/FrontendCompilation.php index 4573f15b016fa47bab5cc0fa7c7010644c4ada6e..8cd04f7d1e4c54804ba32c3c8b5235f9da3dd1db 100644 --- a/app/code/Magento/Developer/Model/View/Asset/PreProcessor/FrontendCompilation.php +++ b/app/code/Magento/Developer/Model/View/Asset/PreProcessor/FrontendCompilation.php @@ -74,10 +74,6 @@ class FrontendCompilation implements PreProcessorInterface */ public function process(PreProcessor\Chain $chain) { - $content = $chain->getContent(); - if (trim($content) !== '') { - return; - } try { $this->lockerProcess->lockProcess($this->lockName); @@ -88,7 +84,7 @@ class FrontendCompilation implements PreProcessorInterface /** @var FallbackContext $context */ $context = $chain->getAsset()->getContext(); - $result = $this->processContent($path, $content, $module, $context); + $result = $this->processContent($path, $chain->getContent(), $module, $context); $chain->setContent($result['content']); $chain->setContentType($result['sourceType']); } finally { @@ -107,14 +103,14 @@ class FrontendCompilation implements PreProcessorInterface */ private function processContent($path, $content, $module, FallbackContext $context) { - $sourceType = '#\.' . preg_quote(pathinfo($path, PATHINFO_EXTENSION), '#') . '$#'; + $sourceTypePattern = '#\.' . preg_quote(pathinfo($path, PATHINFO_EXTENSION), '#') . '$#'; foreach ($this->alternativeSource->getAlternativesExtensionsNames() as $name) { $asset = $this->assetBuilder->setArea($context->getAreaCode()) ->setTheme($context->getThemePath()) ->setLocale($context->getLocale()) ->setModule($module) - ->setPath(preg_replace($sourceType, '.' . $name, $path)) + ->setPath(preg_replace($sourceTypePattern, '.' . $name, $path)) ->build(); $processedContent = $this->assetSource->getContent($asset); @@ -129,7 +125,7 @@ class FrontendCompilation implements PreProcessorInterface return [ 'content' => $content, - 'sourceType' => $sourceType + 'sourceType' => pathinfo($path, PATHINFO_EXTENSION) ]; } } diff --git a/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/FrontendCompilationTest.php b/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/FrontendCompilationTest.php index 97f0b1d5a6bdb081a47f22884f6dac93b3a743f1..b43d91c5e4dc6bc7768c47b1f8599c73b5855932 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/FrontendCompilationTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/View/Asset/PreProcessor/FrontendCompilationTest.php @@ -189,38 +189,6 @@ class FrontendCompilationTest extends \PHPUnit_Framework_TestCase $frontendCompilation->process($this->getChainMockExpects('', 1, 1, $newContentType)); } - /** - * Run test for process method (content not empty) - */ - public function testProcessContentNotEmpty() - { - $chainMock = $this->getChainMock(); - $assetMock = $this->getAssetMock(); - - $chainMock->expects(self::once()) - ->method('getContent') - ->willReturn('test-content'); - - $chainMock->expects(self::never()) - ->method('getAsset') - ->willReturn($assetMock); - - $this->lockerProcessMock->expects(self::never()) - ->method('lockProcess'); - $this->lockerProcessMock->expects(self::never()) - ->method('unlockProcess'); - - $frontendCompilation = new FrontendCompilation( - $this->assetSourceMock, - $this->assetBuilderMock, - $this->alternativeSourceMock, - $this->lockerProcessMock, - 'lock' - ); - - $frontendCompilation->process($chainMock); - } - /** * @return Chain|\PHPUnit_Framework_MockObject_MockObject */ diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml index 678e9306cbadb3bc22dafd9bca089a8b7f304ba7..211e09861995f27e22a446b93e465a7197936965 100644 --- a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/CreateBundleProductEntityTest.xml @@ -195,8 +195,8 @@ <data name="product/data/price_type" xsi:type="string">Yes</data> <data name="product/data/price/dataset" xsi:type="string">dynamic-8</data> <data name="product/data/special_price" xsi:type="string">20</data> - <data name="product/data/special_from_date/pattern" xsi:type="string">M j, Y -1 day</data> - <data name="product/data/special_to_date/pattern" xsi:type="string">M j, Y +3 days</data> + <data name="product/data/special_from_date/pattern" xsi:type="string">m/d/y -1 day</data> + <data name="product/data/special_to_date/pattern" xsi:type="string">m/d/y +3 days</data> <data name="product/data/bundle_selections/dataset" xsi:type="string">default_dynamic</data> <data name="product/data/bundle_selections/products" xsi:type="string">catalogProductSimple::product_100_dollar,catalogProductSimple::product_40_dollar</data> <data name="product/data/checkout_data/dataset" xsi:type="string">bundle_default</data> 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 0000000000000000000000000000000000000000..0dc5c3ef620c3c307d4b35b6b7b0e61091128775 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Api/StockItemSaveTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogInventory\Api; + +use Magento\Catalog\Api\Data\ProductExtensionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; + +class StockItemSaveTest extends \PHPUnit_Framework_TestCase +{ + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSave() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->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/Pricing/Price/LowestPriceOptionProviderTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..50787c7962412b0611edf093825563061da67119 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Pricing/Price/LowestPriceOptionProviderTest.php @@ -0,0 +1,98 @@ +<?php +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Pricing\Price; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +class LowestPriceOptionProviderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var LowestPriceOptionsProviderInterface + */ + private $lowestPriceOptionsProvider; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + protected function setUp() + { + $this->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 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()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/LayoutDirectivesTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/LayoutDirectivesTest.php index 7a20762bbe9d4493131bb0f2acb8e432a0a9ddbf..607ee4c919a62437cdfaf0d2219248dfc77f7a27 100755 --- a/dev/tests/integration/testsuite/Magento/Framework/View/LayoutDirectivesTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/LayoutDirectivesTest.php @@ -198,6 +198,18 @@ class LayoutDirectivesTest extends \PHPUnit_Framework_TestCase $this->assertTrue($layout->isBlock('child_block2')); } + /** + * @magentoAppIsolation enabled + */ + public function testRemoveCancellation() + { + $layout = $this->_getLayoutModel('remove_cancellation.xml'); + $this->assertTrue($layout->isContainer('container1')); + $this->assertTrue($layout->isBlock('child_block1')); + $this->assertTrue($layout->isBlock('no_name2')); + $this->assertFalse($layout->getBlock('not_exist')); + } + /** * @magentoAppIsolation enabled */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/layout_directives_test/remove_cancellation.xml b/dev/tests/integration/testsuite/Magento/Framework/View/_files/layout_directives_test/remove_cancellation.xml new file mode 100644 index 0000000000000000000000000000000000000000..ff086b19fd1b02d80ffc849d9563bdb9419ae75e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/layout_directives_test/remove_cancellation.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<layout> + <container name="container1" label="Container 1"> + <block class="Magento\Framework\View\Element\Text" name="no_name2"/> + </container> + <referenceContainer name="container1" remove="true"/> + <referenceBlock name="child_block1" remove="true"/> + <block class="Magento\Framework\View\Element\Text" name="block_container" as="block.container"> + <block class="Magento\Framework\View\Element\Text" name="child_block1"/> + <block class="Magento\Framework\View\Element\Text" name="child_block2"/> + </block> + <referenceContainer name="not_exist" remove="false"/> + <referenceContainer name="container1" remove="false"/> + <referenceBlock name="child_block1" remove="false"/> +</layout> diff --git a/lib/internal/Magento/Framework/View/Layout/Reader/Block.php b/lib/internal/Magento/Framework/View/Layout/Reader/Block.php index 9831c52a4656310e99c424ce7cc7dd8cd17f68fe..ff5ade125521acff54a182cea51e7f34985292d0 100755 --- a/lib/internal/Magento/Framework/View/Layout/Reader/Block.php +++ b/lib/internal/Magento/Framework/View/Layout/Reader/Block.php @@ -207,13 +207,15 @@ class Block implements Layout\ReaderInterface $elementRemove = filter_var($currentElement->getAttribute('remove'), FILTER_VALIDATE_BOOLEAN); if ($elementRemove) { $scheduledStructure->setElementToRemoveList($elementName); - } else { - $data = $scheduledStructure->getStructureElementData($elementName, []); - $data['attributes'] = $this->mergeBlockAttributes($data, $currentElement); - $this->updateScheduledData($currentElement, $data); - $this->evaluateArguments($currentElement, $data); - $scheduledStructure->setStructureElementData($elementName, $data); + return; + } elseif ($currentElement->getAttribute('remove')) { + $scheduledStructure->unsetElementFromListToRemove($elementName); } + $data = $scheduledStructure->getStructureElementData($elementName, []); + $data['attributes'] = $this->mergeBlockAttributes($data, $currentElement); + $this->updateScheduledData($currentElement, $data); + $this->evaluateArguments($currentElement, $data); + $scheduledStructure->setStructureElementData($elementName, $data); } /** diff --git a/lib/internal/Magento/Framework/View/Layout/Reader/Container.php b/lib/internal/Magento/Framework/View/Layout/Reader/Container.php index c4111a13c33b37fe7e5fcc202ec0773928a1844d..cd4956447a3b14a305e0ca90614633def896a6c7 100755 --- a/lib/internal/Magento/Framework/View/Layout/Reader/Container.php +++ b/lib/internal/Magento/Framework/View/Layout/Reader/Container.php @@ -141,11 +141,12 @@ class Container implements Layout\ReaderInterface ) { $containerName = $currentElement->getAttribute('name'); $containerRemove = filter_var($currentElement->getAttribute('remove'), FILTER_VALIDATE_BOOLEAN); - if ($containerRemove) { $scheduledStructure->setElementToRemoveList($containerName); - } else { - $this->mergeContainerAttributes($scheduledStructure, $currentElement); + return; + } elseif ($currentElement->getAttribute('remove')) { + $scheduledStructure->unsetElementFromListToRemove($containerName); } + $this->mergeContainerAttributes($scheduledStructure, $currentElement); } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/BlockTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/BlockTest.php index 24bbd9391807463226bbe5c276df9aca4c31fd59..8cf049f56f1743484f0d559e4f8abf0caaa6b5f3 100755 --- a/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/BlockTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/BlockTest.php @@ -188,6 +188,12 @@ class BlockTest extends \PHPUnit_Framework_TestCase $setCondition, $setRemoveCondition ) { + if ($literal == 'referenceBlock' && $remove == 'false') { + $this->scheduledStructure->expects($this->once()) + ->method('unsetElementFromListToRemove') + ->with($literal); + } + $this->context->expects($this->once())->method('getScheduledStructure') ->will($this->returnValue($this->scheduledStructure)); diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/ContainerTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/ContainerTest.php index 4e10a5a6a08415ca9d06cb95f5e10fa3a9361c2e..198a70d2121f16962471d3e5f5587a1e89130c3f 100755 --- a/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/ContainerTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Layout/Reader/ContainerTest.php @@ -102,6 +102,12 @@ class ContainerTest extends \PHPUnit_Framework_TestCase ->with($contextMock, $elementCurrent) ->willReturnSelf(); + if ($elementCurrent->getAttribute('remove') == 'false') { + $scheduledStructureMock->expects($this->once()) + ->method('unsetElementFromListToRemove') + ->with($elementCurrent->getAttribute('name')); + } + $this->container->interpret($contextMock, $elementCurrent); }