diff --git a/.travis.yml b/.travis.yml index f44ef422ce2a3..e477e56500b7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,9 @@ addons: - mysql-client-core-5.6 - mysql-client-5.6 - postfix + firefox: "46.0" + hosts: + - magento2.travis language: php php: - 7.0 @@ -16,6 +19,7 @@ env: - COMPOSER_BIN_DIR=~/bin - INTEGRATION_SETS=3 - NODE_JS_VERSION=6 + - MAGENTO_HOST_NAME="magento2.travis" matrix: - TEST_SUITE=unit - TEST_SUITE=integration INTEGRATION_INDEX=1 @@ -23,12 +27,18 @@ env: - TEST_SUITE=integration INTEGRATION_INDEX=3 - TEST_SUITE=static - TEST_SUITE=js + - TEST_SUITE=functional ACCEPTANCE_INDEX=1 + - TEST_SUITE=functional ACCEPTANCE_INDEX=2 matrix: exclude: - php: 7.0 env: TEST_SUITE=static - php: 7.0 env: TEST_SUITE=js + - php: 7.0 + env: TEST_SUITE=functional ACCEPTANCE_INDEX=1 + - php: 7.0 + env: TEST_SUITE=functional ACCEPTANCE_INDEX=2 cache: apt: true directories: diff --git a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php index 7988bc736df14..6f0e42bdcbef1 100644 --- a/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php +++ b/app/code/Magento/AdminNotification/Block/Grid/Renderer/Actions.php @@ -6,8 +6,6 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\AdminNotification\Block\Grid\Renderer; class Actions extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer @@ -39,9 +37,8 @@ public function __construct( */ public function render(\Magento\Framework\DataObject $row) { - $readDetailsHtml = $row->getUrl() ? '' . __( - 'Read Details' - ) . '' : ''; + $readDetailsHtml = $row->getUrl() ? '' . + __('Read Details') . '' : ''; $markAsReadHtml = !$row->getIsRead() ? ' + click: placeOrderClick, + attr: {title: $t('Place Order')}, + css: {disabled: !isPlaceOrderActionAllowed()}, + enable: isActive() + " + disabled> diff --git a/app/code/Magento/Bundle/Model/Product/Price.php b/app/code/Magento/Bundle/Model/Product/Price.php index d2b7d6bdad45d..24ba3bc2897a7 100644 --- a/app/code/Magento/Bundle/Model/Product/Price.php +++ b/app/code/Magento/Bundle/Model/Product/Price.php @@ -166,7 +166,7 @@ protected function getBundleSelectionIds(\Magento\Catalog\Model\Product $product $customOption = $product->getCustomOption('bundle_selection_ids'); if ($customOption) { $selectionIds = $this->serializer->unserialize($customOption->getValue()); - if (!empty($selectionIds) && is_array($selectionIds)) { + if (is_array($selectionIds) && !empty($selectionIds)) { return $selectionIds; } } diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index be091909cdb43..f5a1baaaed955 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -66,8 +66,8 @@ public function execute($entity, $arguments = []) $options = $bundleProductOptions ?: []; foreach ($options as $option) { $this->optionRepository->save($entity, $option); - $entity->setCopyFromView(false); } + $entity->setCopyFromView(false); } return $entity; } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php index 3fd205b742210..cbc2b8a81a61f 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php @@ -126,7 +126,7 @@ public function getParentIdsByChild($childId) 'e.' . $metadata->getLinkField() . ' = ' . $this->getMainTable() . '.parent_product_id', ['e.entity_id as parent_product_id'] )->where( - 'e.entity_id IN(?)', + $this->getMainTable() . '.product_id IN(?)', $childId ); diff --git a/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php b/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php index a1a33700bbbfe..ee96bbd0fb79a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Helper/Catalog/Product/ConfigurationTest.php @@ -12,20 +12,30 @@ */ class ConfigurationTest extends \PHPUnit_Framework_TestCase { - /** @var \Magento\Framework\Pricing\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ - protected $pricingHelper; + /** + * @var \Magento\Framework\Pricing\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + */ + private $pricingHelper; - /** @var \Magento\Catalog\Helper\Product\Configuration|\PHPUnit_Framework_MockObject_MockObject */ - protected $productConfiguration; + /** + * @var \Magento\Catalog\Helper\Product\Configuration|\PHPUnit_Framework_MockObject_MockObject + */ + private $productConfiguration; - /** @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject */ - protected $escaper; + /** + * @var \Magento\Framework\Escaper|\PHPUnit_Framework_MockObject_MockObject + */ + private $escaper; - /** @var \Magento\Bundle\Helper\Catalog\Product\Configuration */ - protected $helper; + /** + * @var \Magento\Bundle\Helper\Catalog\Product\Configuration + */ + private $helper; - /** @var \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $item; + /** + * @var \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $item; /** * @var \Magento\Framework\Serialize\Serializer\Json @@ -48,7 +58,13 @@ protected function setUp() '', false ); - $this->escaper = $this->getMock(\Magento\Framework\Escaper::class, ['escapeHtml'], [], '', false); + $this->escaper = $this->getMock( + \Magento\Framework\Escaper::class, + ['escapeHtml'], + [], + '', + false + ); $this->item = $this->getMock( \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface::class, ['getQty', 'getProduct', 'getOptionByCode', 'getFileDownloadParams'] @@ -79,12 +95,28 @@ public function testGetSelectionQty() { $selectionId = 15; $selectionQty = 35; - $product = $this->getMock(\Magento\Catalog\Model\Product::class, [], [], '', false); - $option = $this->getMock(\Magento\Catalog\Model\Product\Option::class, ['__wakeup', 'getValue'], [], '', false); + $product = $this->getMock( + \Magento\Catalog\Model\Product::class, + [], + [], + '', + false + ); + $option = $this->getMock( + \Magento\Catalog\Model\Product\Option::class, + ['__wakeup', 'getValue'], + [], + '', + false + ); - $product->expects($this->once())->method('getCustomOption')->with('selection_qty_' . $selectionId) - ->will($this->returnValue($option)); - $option->expects($this->once())->method('getValue')->will($this->returnValue($selectionQty)); + $product->expects($this->once()) + ->method('getCustomOption') + ->with('selection_qty_' . $selectionId) + ->willReturn($option); + $option->expects($this->once()) + ->method('getValue') + ->willReturn($selectionQty); $this->assertEquals($selectionQty, $this->helper->getSelectionQty($product, $selectionId)); } @@ -100,9 +132,6 @@ public function testGetSelectionQtyIfCustomOptionIsNotSet() $this->assertEquals(0, $this->helper->getSelectionQty($product, $selectionId)); } - /** - * @covers \Magento\Bundle\Helper\Catalog\Product\Configuration::getSelectionFinalPrice - */ public function testGetSelectionFinalPrice() { $itemQty = 2; @@ -144,7 +173,13 @@ public function testGetBundleOptionsEmptyBundleOptionsIds() public function testGetBundleOptionsEmptyBundleSelectionIds() { $optionIds = '{"0":"1"}'; - $collection = $this->getMock(\Magento\Bundle\Model\ResourceModel\Option\Collection::class, [], [], '', false); + $collection = $this->getMock( + \Magento\Bundle\Model\ResourceModel\Option\Collection::class, + [], + [], + '', + false + ); $product = $this->getMock( \Magento\Catalog\Model\Product::class, ['getTypeInstance', @@ -153,28 +188,49 @@ public function testGetBundleOptionsEmptyBundleSelectionIds() '', false ); - $typeInstance = $this->getMock(\Magento\Bundle\Model\Product\Type::class, ['getOptionsByIds'], [], '', false); - $selectionOption = - $this->getMock( - \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, - ['getValue'] - ); - $itemOption = - $this->getMock( - \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, - ['getValue'] - ); + $typeInstance = $this->getMock( + \Magento\Bundle\Model\Product\Type::class, + ['getOptionsByIds'], + [], + '', + false + ); + $selectionOption = $this->getMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, + ['getValue'] + ); + $itemOption = $this->getMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, + ['getValue'] + ); - $selectionOption->expects($this->once())->method('getValue')->will($this->returnValue('')); - $itemOption->expects($this->once())->method('getValue')->will($this->returnValue($optionIds)); - $typeInstance->expects($this->once())->method('getOptionsByIds')->with(json_decode($optionIds, true), $product) - ->will($this->returnValue($collection)); - $product->expects($this->once())->method('getTypeInstance')->will($this->returnValue($typeInstance)); - $this->item->expects($this->once())->method('getProduct')->will($this->returnValue($product)); - $this->item->expects($this->at(1))->method('getOptionByCode')->with('bundle_option_ids') - ->will($this->returnValue($itemOption)); - $this->item->expects($this->at(2))->method('getOptionByCode')->with('bundle_selection_ids') - ->will($this->returnValue($selectionOption)); + $selectionOption->expects($this->once()) + ->method('getValue') + ->willReturn('[]'); + $itemOption->expects($this->once()) + ->method('getValue') + ->willReturn($optionIds); + $typeInstance->expects($this->once()) + ->method('getOptionsByIds') + ->with( + json_decode($optionIds, true), + $product + ) + ->willReturn($collection); + $product->expects($this->once()) + ->method('getTypeInstance') + ->willReturn($typeInstance); + $this->item->expects($this->once()) + ->method('getProduct') + ->willReturn($product); + $this->item->expects($this->at(1)) + ->method('getOptionByCode') + ->with('bundle_option_ids') + ->willReturn($itemOption); + $this->item->expects($this->at(2)) + ->method('getOptionByCode') + ->with('bundle_selection_ids') + ->willReturn($selectionOption); $this->assertEquals([], $this->helper->getBundleOptions($this->item)); } @@ -201,38 +257,44 @@ public function testGetOptions() '', false ); - $priceModel = - $this->getMock(\Magento\Bundle\Model\Product\Price::class, ['getSelectionFinalTotalPrice'], [], '', false); - $selectionQty = - $this->getMock(\Magento\Quote\Model\Quote\Item\Option::class, ['getValue', '__wakeup'], [], '', false); - $bundleOption = - $this->getMock( - \Magento\Bundle\Model\Option::class, - ['getSelections', - 'getTitle', - '__wakeup'], - [], - '', - false - ); - $selectionOption = - $this->getMock( - \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, - ['getValue'] - ); - $collection = - $this->getMock( - \Magento\Bundle\Model\ResourceModel\Option\Collection::class, - ['appendSelections'], - [], - '', - false - ); - $itemOption = - $this->getMock( - \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, - ['getValue'] - ); + $priceModel = $this->getMock( + \Magento\Bundle\Model\Product\Price::class, + ['getSelectionFinalTotalPrice'], + [], + '', + false + ); + $selectionQty = $this->getMock( + \Magento\Quote\Model\Quote\Item\Option::class, + ['getValue', '__wakeup'], + [], + '', + false + ); + $bundleOption = $this->getMock( + \Magento\Bundle\Model\Option::class, + ['getSelections', + 'getTitle', + '__wakeup'], + [], + '', + false + ); + $selectionOption = $this->getMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, + ['getValue'] + ); + $collection = $this->getMock( + \Magento\Bundle\Model\ResourceModel\Option\Collection::class, + ['appendSelections'], + [], + '', + false + ); + $itemOption = $this->getMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface::class, + ['getValue'] + ); $collection2 = $this->getMock( \Magento\Bundle\Model\ResourceModel\Selection\Collection::class, [], @@ -241,7 +303,10 @@ public function testGetOptions() false ); - $this->escaper->expects($this->once())->method('escapeHtml')->with('name')->will($this->returnValue('name')); + $this->escaper->expects($this->once()) + ->method('escapeHtml') + ->with('name') + ->willReturn('name'); $this->pricingHelper->expects($this->once())->method('currency')->with(15) ->will($this->returnValue('$15.00')); $priceModel->expects($this->once())->method('getSelectionFinalTotalPrice')->will($this->returnValue(15)); @@ -252,8 +317,13 @@ public function testGetOptions() $collection->expects($this->once())->method('appendSelections')->with($collection2, true) ->will($this->returnValue([$bundleOption])); $itemOption->expects($this->once())->method('getValue')->will($this->returnValue($optionIds)); - $typeInstance->expects($this->once())->method('getOptionsByIds')->with(json_decode($optionIds, true), $product) - ->will($this->returnValue($collection)); + $typeInstance->expects($this->once()) + ->method('getOptionsByIds') + ->with( + json_decode($optionIds, true), + $product + ) + ->willReturn($collection); $typeInstance->expects($this->once()) ->method('getSelectionsByIds') ->with(json_decode($selectionIds, true), $product) diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index deb2e9917aceb..6744fc37d1e0c 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -2591,7 +2591,7 @@ public function testCheckProductBuyStateEmptyOptionsException() $this->mockBundleCollection(); $product = $this->getProductMock(); $product->method('getCustomOption')->willReturnMap([ - ['bundle_selection_ids', new DataObject(['value' => ''])], + ['bundle_selection_ids', new DataObject(['value' => '[]'])], ['info_buyRequest', new DataObject(['value' => json_encode(['bundle_option' => ''])])], ]); $product->setCustomOption(json_encode([])); diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundleAdvancedPricing.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundleAdvancedPricing.php index a3746bc9c814a..1b04bdc4f8104 100644 --- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundleAdvancedPricing.php +++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundleAdvancedPricing.php @@ -7,17 +7,32 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Framework\Stdlib\ArrayManager; /** * Customize Advanced Pricing modal panel */ class BundleAdvancedPricing extends AbstractModifier { + const CODE_PRICE_TYPE = 'price_type'; const CODE_MSRP = 'msrp'; const CODE_MSRP_DISPLAY_ACTUAL_PRICE_TYPE = 'msrp_display_actual_price_type'; const CODE_ADVANCED_PRICING = 'advanced-pricing'; const CODE_RECORD = 'record'; + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @param ArrayManager $arrayManager + */ + public function __construct(ArrayManager $arrayManager) + { + $this->arrayManager = $arrayManager; + } + /** * {@inheritdoc} */ @@ -29,8 +44,7 @@ public function modifyMeta(array $meta) if (isset($parentNode['container_' . self::CODE_MSRP]) && isset($parentNode['container_' . self::CODE_MSRP_DISPLAY_ACTUAL_PRICE_TYPE]) ) { - unset($parentNode['container_' . self::CODE_MSRP]); - unset($parentNode['container_' . self::CODE_MSRP_DISPLAY_ACTUAL_PRICE_TYPE]); + $parentNode = $this->modifyMsrpMeta($parentNode); } if (isset($parentNode['container_' . ProductAttributeInterface::CODE_SPECIAL_PRICE])) { $currentNode = &$parentNode['container_' . ProductAttributeInterface::CODE_SPECIAL_PRICE]['children']; @@ -55,4 +69,45 @@ public function modifyData(array $data) { return $data; } + + /** + * Modify meta for MSRP fields. + * + * @param array $meta + * @return array + */ + private function modifyMsrpMeta(array $meta) + { + $meta = $this->arrayManager->merge( + $this->arrayManager->findPath( + static::CODE_MSRP, + $meta, + null, + 'children' + ) . static::META_CONFIG_PATH, + $meta, + [ + 'imports' => [ + 'disabled' => 'ns = ${ $.ns }, index = ' . static::CODE_PRICE_TYPE . ':checked' + ] + ] + ); + + $meta = $this->arrayManager->merge( + $this->arrayManager->findPath( + static::CODE_MSRP_DISPLAY_ACTUAL_PRICE_TYPE, + $meta, + null, + 'children' + ) . static::META_CONFIG_PATH, + $meta, + [ + 'imports' => [ + 'disabled' => 'ns = ${ $.ns }, index = ' . static::CODE_PRICE_TYPE . ':checked' + ] + ] + ); + + return $meta; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php index 77a54b51ea9a3..6885a9dbe1574 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/AbstractAction.php @@ -148,12 +148,13 @@ protected function syncData($indexer, $destinationTable, $ids) * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\AbstractEav $indexer * @param array $ids * - * @return $ids + * @param bool $onlyParents + * @return array $ids */ - protected function processRelations($indexer, $ids) + protected function processRelations($indexer, $ids, $onlyParents = false) { $parentIds = $indexer->getRelationsByChild($ids); - $childIds = $indexer->getRelationsByParent($ids); + $childIds = $onlyParents ? [] : $indexer->getRelationsByParent($ids); return array_unique(array_merge($ids, $childIds, $parentIds)); } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php index 65f6e30096144..fdb933f714c3f 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Eav/Action/Full.php @@ -83,7 +83,7 @@ public function execute($ids = null) $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); $entityIds = $this->batchProvider->getBatchIds($connection, $select, $batch); if (!empty($entityIds)) { - $indexer->reindexEntities($this->processRelations($indexer, $entityIds)); + $indexer->reindexEntities($this->processRelations($indexer, $entityIds, true)); $this->syncData($indexer, $mainTable); } } diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index c9fbc7d553807..ceacfea5ec020 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -308,7 +308,7 @@ private function cacheProduct($cacheKey, \Magento\Catalog\Api\Data\ProductInterf if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); - $this->instancesById = array_slice($this->instancesById, $offset); + $this->instancesById = array_slice($this->instancesById, $offset, null, true); $this->instances = array_slice($this->instances, $offset); } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php index 523224ebd0be7..f864cc7e9db33 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/AbstractIndexer.php @@ -196,7 +196,7 @@ public function getRelationsByChild($childIds) $childIds ); - return $connection->fetchCol($select); + return array_map('intval', (array) $connection->fetchCol($select)); } /** @@ -228,7 +228,7 @@ public function getRelationsByParent($parentIds) $result = $connection->fetchCol($select); } - return $result; + return array_map('intval', $result); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php index c51d86329fe09..39df8f5e64535 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Eav/Action/FullTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Eav\Action; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class FullTest extends \PHPUnit_Framework_TestCase { public function testExecuteWithAdapterErrorThrowsException() @@ -55,4 +58,103 @@ public function testExecuteWithAdapterErrorThrowsException() $model->execute(); } + + public function testExecute() + { + $eavDecimalFactory = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\DecimalFactory::class, + ['create'], + [], + '', + false + ); + $eavSourceFactory = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\SourceFactory::class, + ['create'], + [], + '', + false + ); + + $ids = [1, 2, 3]; + $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->getMockForAbstractClass(); + + $connectionMock->expects($this->atLeastOnce())->method('describeTable')->willReturn(['id' => []]); + $eavSource = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Source::class) + ->disableOriginalConstructor() + ->getMock(); + + $eavDecimal = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\Decimal::class) + ->disableOriginalConstructor() + ->getMock(); + + $eavSource->expects($this->once())->method('getRelationsByChild')->with($ids)->willReturn([]); + $eavSource->expects($this->never())->method('getRelationsByParent')->with($ids)->willReturn([]); + + $eavDecimal->expects($this->once())->method('getRelationsByChild')->with($ids)->willReturn([]); + $eavDecimal->expects($this->never())->method('getRelationsByParent')->with($ids)->willReturn([]); + + $eavSource->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock); + $eavDecimal->expects($this->atLeastOnce())->method('getConnection')->willReturn($connectionMock); + + $eavDecimal->expects($this->once()) + ->method('reindexEntities') + ->with($ids); + + $eavSource->expects($this->once()) + ->method('reindexEntities') + ->with($ids); + + $eavDecimalFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue($eavSource)); + + $eavSourceFactory->expects($this->once()) + ->method('create') + ->will($this->returnValue($eavDecimal)); + + $metadataMock = $this->getMock(\Magento\Framework\EntityManager\MetadataPool::class, [], [], '', false); + $entityMetadataMock = $this->getMockBuilder(\Magento\Framework\EntityManager\EntityMetadataInterface::class) + ->getMockForAbstractClass(); + + $metadataMock->expects($this->atLeastOnce()) + ->method('getMetadata') + ->with(\Magento\Catalog\Api\Data\ProductInterface::class) + ->willReturn($entityMetadataMock); + + $batchProviderMock = $this->getMock(\Magento\Framework\Indexer\BatchProviderInterface::class); + $batchProviderMock->expects($this->atLeastOnce()) + ->method('getBatches') + ->willReturn([['from' => 10, 'to' => 100]]); + $batchProviderMock->expects($this->atLeastOnce()) + ->method('getBatchIds') + ->willReturn($ids); + + $batchManagementMock = $this->getMock( + \Magento\Catalog\Model\ResourceModel\Product\Indexer\Eav\BatchSizeCalculator::class, + [], + [], + '', + false + ); + $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) + ->disableOriginalConstructor() + ->getMock(); + + $connectionMock->method('select')->willReturn($selectMock); + $selectMock->expects($this->atLeastOnce())->method('distinct')->willReturnSelf(); + $selectMock->expects($this->atLeastOnce())->method('from')->willReturnSelf(); + + $model = new \Magento\Catalog\Model\Indexer\Product\Eav\Action\Full( + $eavDecimalFactory, + $eavSourceFactory, + $metadataMock, + $batchProviderMock, + $batchManagementMock, + [] + ); + + $model->execute(); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php index a0c9f4c6c92e2..d32a451a86066 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductRepositoryTest.php @@ -153,6 +153,13 @@ class ProductRepositoryTest extends \PHPUnit_Framework_TestCase */ private $serializerMock; + /** + * Product repository cache limit. + * + * @var int + */ + private $cacheLimit = 2; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -337,6 +344,7 @@ function ($value) { 'mediaGalleryProcessor' => $this->mediaGalleryProcessor, 'collectionProcessor' => $this->collectionProcessorMock, 'serializer' => $this->serializerMock, + 'cacheLimit' => $this->cacheLimit ] ); } @@ -469,6 +477,61 @@ public function testGetByIdForcedReload() $this->assertEquals($this->productMock, $this->model->getById($identifier, $editMode, $storeId, true)); } + /** + * Test for getById() method if we try to get products when cache is already filled and is reduced. + * + * @return void + */ + public function testGetByIdWhenCacheReduced() + { + $result = []; + $expectedResult = []; + $productsCount = $this->cacheLimit * 2; + + $productMocks = $this->getProductMocksForReducedCache($productsCount); + $productFactoryInvMock = $this->productFactoryMock->expects($this->exactly($productsCount)) + ->method('create'); + call_user_func_array([$productFactoryInvMock, 'willReturnOnConsecutiveCalls'], $productMocks); + $this->serializerMock->expects($this->atLeastOnce())->method('serialize'); + + for ($i = 1; $i <= $productsCount; $i++) { + $product = $this->model->getById($i, false, 0); + $result[] = $product->getId(); + $expectedResult[] = $i; + } + + $this->assertEquals($expectedResult, $result); + } + + /** + * Get product mocks for testGetByIdWhenCacheReduced() method. + * + * @param int $productsCount + * @return array + */ + private function getProductMocksForReducedCache($productsCount) + { + $productMocks = []; + + for ($i = 1; $i <= $productsCount; $i++) { + $productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getId', + 'getSku', + 'load', + 'setData', + ]) + ->getMock(); + $productMock->expects($this->once())->method('load'); + $productMock->expects($this->atLeastOnce())->method('getId')->willReturn($i); + $productMock->expects($this->atLeastOnce())->method('getSku')->willReturn($i . uniqid()); + $productMocks[] = $productMock; + } + + return $productMocks; + } + /** * Test forceReload parameter * diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 472e964e70f22..6903a17bcdcca 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -7,6 +7,7 @@ * @api */ define([ + 'jquery', 'Magento_Ui/js/lib/view/utils/async', 'jquery/ui', 'mage/translate', @@ -838,4 +839,6 @@ define([ }; productConfigure = new ProductConfigure(); + jQuery(document).trigger('productConfigure:inited'); + jQuery(document).data('productConfigureInited', true); }); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 819856e727b37..864adc039a82d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -18,6 +18,7 @@ use Magento\ImportExport\Model\Import\Entity\AbstractEntity; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Catalog\Model\Config as CatalogConfig; /** * Import entity product model @@ -657,6 +658,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $multiLineSeparatorForRegexp; + /** + * Catalog config. + * + * @var CatalogConfig + */ + private $catalogConfig; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -695,6 +703,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param array $data * @param array $dateAttrCodes + * @param CatalogConfig $catalogConfig * @throws \Magento\Framework\Exception\LocalizedException * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -737,7 +746,8 @@ public function __construct( \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\Catalog\Model\Product\Url $productUrl, array $data = [], - array $dateAttrCodes = [] + array $dateAttrCodes = [], + CatalogConfig $catalogConfig = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -767,6 +777,9 @@ public function __construct( $this->scopeConfig = $scopeConfig; $this->productUrl = $productUrl; $this->dateAttrCodes = array_merge($this->dateAttrCodes, $dateAttrCodes); + $this->catalogConfig = $catalogConfig ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(CatalogConfig::class); + parent::__construct( $jsonHelper, $importExportData, @@ -1330,7 +1343,7 @@ public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) $entityTable = $this->_resourceFactory->create()->getEntityTable(); } if ($entityRowsUp) { - $this->_connection->insertOnDuplicate($entityTable, $entityRowsUp, ['updated_at']); + $this->_connection->insertOnDuplicate($entityTable, $entityRowsUp, ['updated_at', 'attribute_set_id']); } if ($entityRowsIn) { $this->_connection->insertMultiple($entityTable, $entityRowsIn); @@ -1563,9 +1576,29 @@ protected function _saveProducts() // 1. Entity phase if (isset($this->_oldSku[$rowSku])) { // existing row + if (isset($rowData['attribute_set_code'])) { + $attributeSetId = $this->catalogConfig->getAttributeSetId( + $this->getEntityTypeId(), + $rowData['attribute_set_code'] + ); + + // wrong attribute_set_code was received + if (!$attributeSetId) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Wrong attribute set code "%1", please correct it and try again.', + $rowData['attribute_set_code'] + ) + ); + } + } else { + $attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; + } + $entityRowsUp[] = [ 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), - $this->getProductEntityLinkField() => $this->_oldSku[$rowSku][$this->getProductEntityLinkField()], + 'attribute_set_id' => $attributeSetId, + $this->getProductEntityLinkField() => $this->_oldSku[$rowSku][$this->getProductEntityLinkField()] ]; } else { if (!$productLimit || $productsQty < $productLimit) { diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php index e1e0fd4c0b2ba..258f56c2e462e 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Ui/Component/Product/Form/Element/UseConfigSettingsTest.php @@ -33,16 +33,25 @@ class UseConfigSettingsTest extends \PHPUnit_Framework_TestCase */ private $useConfigSettings; + /** + * @var \Magento\Framework\Serialize\JsonValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $jsonValidatorMock; + protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); $this->contextMock = $this->getMock(\Magento\Framework\View\Element\UiComponent\ContextInterface::class); $this->serializerMock = $this->getMock(Json::class); + $this->jsonValidatorMock = $this->getMockBuilder(\Magento\Framework\Serialize\JsonValidator::class) + ->disableOriginalConstructor() + ->getMock(); $this->useConfigSettings = $this->objectManagerHelper->getObject( UseConfigSettings::class, [ 'context' => $this->contextMock, - 'serializer' => $this->serializerMock + 'serializer' => $this->serializerMock, + 'jsonValidator' => $this->jsonValidatorMock ] ); } @@ -68,11 +77,16 @@ public function testPrepare() /** * @param array $expectedResult * @param string|int $sourceValue - * @param int $serializedCallCount + * @param int $serializedCalledNum + * @param int $isValidCalledNum * @dataProvider prepareSourceDataProvider */ - public function testPrepareSource(array $expectedResult, $sourceValue, $serializedCallCount = 0) - { + public function testPrepareSource( + array $expectedResult, + $sourceValue, + $serializedCalledNum = 0, + $isValidCalledNum = 0 + ) { $processorMock = $this->getMock( \Magento\Framework\View\Element\UiComponent\Processor::class, [], @@ -90,11 +104,15 @@ public function testPrepareSource(array $expectedResult, $sourceValue, $serializ ->with($expectedResult['keyInConfiguration']) ->willReturn($sourceValue); - $this->serializerMock->expects($this->exactly($serializedCallCount)) + $this->serializerMock->expects($this->exactly($serializedCalledNum)) ->method('unserialize') ->with($sourceValue) ->willReturn($expectedResult['valueFromConfig']); + $this->jsonValidatorMock->expects($this->exactly($isValidCalledNum)) + ->method('isValid') + ->willReturn(true); + $config = array_replace($expectedResult, ['valueFromConfig' => $source]); $this->useConfigSettings->setData('config', $config); $this->useConfigSettings->prepare(); @@ -119,7 +137,8 @@ public function prepareSourceDataProvider() 'unserialized' => true ], 'sourceValue' => '{"32000":3}', - 'serialziedCallCount' => 1 + 'serialziedCalledNum' => 1, + 'isValidCalledNum' => 1 ] ]; } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedInventoryTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedInventoryTest.php index d6f8cd630a875..b22d84d1fc1dd 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedInventoryTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Ui/DataProvider/Product/Form/Modifier/AdvancedInventoryTest.php @@ -21,28 +21,28 @@ class AdvancedInventoryTest extends AbstractModifierTest /** * @var StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockRegistryMock; + private $stockRegistryMock; /** * @var StockItemInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockItemMock; - - /** - * @var Store|\PHPUnit_Framework_MockObject_MockObject - */ - protected $storeMock; + private $stockItemMock; /** * @var StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $stockConfigurationMock; + private $stockConfigurationMock; /** * @var Json|\PHPUnit_Framework_MockObject_MockObject */ private $serializerMock; + /** + * @var \Magento\Framework\Serialize\JsonValidator|\PHPUnit_Framework_MockObject_MockObject + */ + private $jsonValidatorMock; + protected function setUp() { parent::setUp(); @@ -66,6 +66,9 @@ protected function setUp() ->method('getStore') ->willReturn($this->storeMock); $this->serializerMock = $this->getMock(Json::class); + $this->jsonValidatorMock = $this->getMockBuilder(\Magento\Framework\Serialize\JsonValidator::class) + ->disableOriginalConstructor() + ->getMock(); } /** @@ -73,13 +76,17 @@ protected function setUp() */ protected function createModel() { - return $this->objectManager->getObject(AdvancedInventory::class, [ - 'locator' => $this->locatorMock, - 'stockRegistry' => $this->stockRegistryMock, - 'stockConfiguration' => $this->stockConfigurationMock, - 'arrayManager' => $this->arrayManagerMock, - 'serializer' => $this->serializerMock - ]); + return $this->objectManager->getObject( + AdvancedInventory::class, + [ + 'locator' => $this->locatorMock, + 'stockRegistry' => $this->stockRegistryMock, + 'stockConfiguration' => $this->stockConfigurationMock, + 'arrayManager' => $this->arrayManagerMock, + 'serializer' => $this->serializerMock, + 'jsonValidator' => $this->jsonValidatorMock, + ] + ); } public function testModifyMeta() @@ -92,7 +99,8 @@ public function testModifyMeta() * @param int $someData * @param int|string $defaultConfigValue * @param null|array $unserializedValue - * @param int $serializeCallCount + * @param int $serializeCalledNum + * @param int $isValidCalledNum * @dataProvider modifyDataProvider */ public function testModifyData( @@ -100,19 +108,26 @@ public function testModifyData( $someData, $defaultConfigValue, $unserializedValue = null, - $serializeCallCount = 0 + $serializeCalledNum = 0, + $isValidCalledNum = 0 ) { - $this->productMock->expects($this->any())->method('getId')->willReturn($modelId); + $this->productMock->expects($this->any()) + ->method('getId') + ->willReturn($modelId); $this->stockConfigurationMock->expects($this->any()) ->method('getDefaultConfigValue') ->willReturn($defaultConfigValue); - $this->serializerMock->expects($this->exactly($serializeCallCount)) + $this->serializerMock->expects($this->exactly($serializeCalledNum)) ->method('unserialize') ->with($defaultConfigValue) ->willReturn($unserializedValue); + $this->jsonValidatorMock->expects($this->exactly($isValidCalledNum)) + ->method('isValid') + ->willReturn(true); + $this->stockItemMock->expects($this->once())->method('getData')->willReturn(['someData']); $this->stockItemMock->expects($this->once())->method('getManageStock')->willReturn($someData); $this->stockItemMock->expects($this->once())->method('getQty')->willReturn($someData); @@ -135,11 +150,14 @@ public function testModifyData( $this->assertArrayHasKey($modelId, $this->getModel()->modifyData([])); } + /** + * @return array + */ public function modifyDataProvider() { return [ [1, 1, 1], - [1, 1, '{"36000":2}', ['36000' => 2], 1] + [1, 1, '{"36000":2}', ['36000' => 2], 1, 1] ]; } } diff --git a/app/code/Magento/CatalogInventory/Ui/Component/Product/Form/Element/UseConfigSettings.php b/app/code/Magento/CatalogInventory/Ui/Component/Product/Form/Element/UseConfigSettings.php index 3ca78e89b0530..67b5699c17bd6 100644 --- a/app/code/Magento/CatalogInventory/Ui/Component/Product/Form/Element/UseConfigSettings.php +++ b/app/code/Magento/CatalogInventory/Ui/Component/Product/Form/Element/UseConfigSettings.php @@ -9,29 +9,43 @@ use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Form\Element\Checkbox; +use Magento\Framework\Serialize\JsonValidator; +use Magento\Framework\App\ObjectManager; /** * Class UseConfigSettings sets default value from configuration */ class UseConfigSettings extends Checkbox { - /** @var Json */ + /** + * @var Json + */ private $serializer; /** + * @var JsonValidator + */ + private $jsonValidator; + + /** + * Constructor + * * @param ContextInterface $context * @param array $components * @param array $data * @param Json|null $serializer + * @param JsonValidator|null $jsonValidator */ public function __construct( ContextInterface $context, $components = [], array $data = [], - Json $serializer = null + Json $serializer = null, + JsonValidator $jsonValidator = null ) { parent::__construct($context, $components, $data); - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->jsonValidator = $jsonValidator ?: ObjectManager::getInstance()->get(JsonValidator::class); } /** @@ -48,9 +62,8 @@ public function prepare() ) { $keyInConfiguration = $config['valueFromConfig']->getValue($config['keyInConfiguration']); if (!empty($config['unserialized']) && is_string($keyInConfiguration)) { - $unserializedValue = $this->serializer->unserialize($keyInConfiguration); - if (json_last_error() === JSON_ERROR_NONE) { - $keyInConfiguration = $unserializedValue; + if ($this->jsonValidator->isValid($keyInConfiguration)) { + $keyInConfiguration = $this->serializer->unserialize($keyInConfiguration); } } $config['valueFromConfig'] = $keyInConfiguration; diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php index 5e1aed121e246..c5d18ea727534 100644 --- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php @@ -13,6 +13,8 @@ use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\ArrayManager; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Serialize\JsonValidator; /** * Data provider for advanced inventory form @@ -52,24 +54,34 @@ class AdvancedInventory extends AbstractModifier private $serializer; /** + * @var JsonValidator + */ + private $jsonValidator; + + /** + * Constructor + * * @param LocatorInterface $locator * @param StockRegistryInterface $stockRegistry * @param ArrayManager $arrayManager * @param StockConfigurationInterface $stockConfiguration * @param Json|null $serializer + * @param JsonValidator|null $jsonValidator */ public function __construct( LocatorInterface $locator, StockRegistryInterface $stockRegistry, ArrayManager $arrayManager, StockConfigurationInterface $stockConfiguration, - Json $serializer = null + Json $serializer = null, + JsonValidator $jsonValidator = null ) { $this->locator = $locator; $this->stockRegistry = $stockRegistry; $this->arrayManager = $arrayManager; $this->stockConfiguration = $stockConfiguration; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class); + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->jsonValidator = $jsonValidator ?: ObjectManager::getInstance()->get(JsonValidator::class); } /** @@ -100,16 +112,20 @@ public function modifyData(array $data) if (!empty($this->stockConfiguration->getDefaultConfigValue(StockItemInterface::MIN_SALE_QTY))) { $minSaleQtyData = $this->stockConfiguration->getDefaultConfigValue(StockItemInterface::MIN_SALE_QTY); - if (is_string($minSaleQtyData)) { - // Set data source for dynamicRows Minimum Qty Allowed in Shopping Cart + if (is_string($minSaleQtyData) && $this->jsonValidator->isValid($minSaleQtyData)) { + // Set data source for dynamicRows minimum qty allowed in shopping cart $unserializedMinSaleQty = $this->serializer->unserialize($minSaleQtyData); - if (is_array($unserializedMinSaleQty) && json_last_error() === JSON_ERROR_NONE) { - $minSaleQtyData = array_map(function ($group, $qty) { - return [ - StockItemInterface::CUSTOMER_GROUP_ID => $group, - StockItemInterface::MIN_SALE_QTY => $qty - ]; - }, array_keys($unserializedMinSaleQty), array_values($unserializedMinSaleQty)); + if (is_array($unserializedMinSaleQty)) { + $minSaleQtyData = array_map( + function ($group, $qty) { + return [ + StockItemInterface::CUSTOMER_GROUP_ID => $group, + StockItemInterface::MIN_SALE_QTY => $qty + ]; + }, + array_keys($unserializedMinSaleQty), + array_values($unserializedMinSaleQty) + ); } } diff --git a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml index 85eab3de4ad56..3472f4368d617 100644 --- a/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml +++ b/app/code/Magento/CatalogInventory/view/adminhtml/ui_component/product_form.xml @@ -431,65 +431,6 @@ - - - - container - Use Deferred Stock Update - stock_data - [GLOBAL] - - - - - - true - Magento\CatalogInventory\Model\Source\StockConfiguration - - - - [GLOBAL] - - deferred_stock_update - - - - - - - - - 1 - Magento\CatalogInventory\Model\Source\StockConfiguration - deferred_stock_update - - - - use_config_deferred_stock_update - - ${$.provider}:data.product.stock_data.deferred_stock_update - - - ${$.parentName}.deferred_stock_update:disabled - - - - - - Use Config Settings - - 0 - 1 - - - - - - diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 72cd539973050..bcf2515d67f48 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -94,6 +94,11 @@ class DataProvider */ private $metadata; + /** + * @var array + */ + private $attributeOptions = []; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -177,23 +182,13 @@ public function getSearchableProducts( return $result; } - /** - * Retrieve EAV Config Singleton - * - * @return \Magento\Eav\Model\Config - */ - private function getEavConfig() - { - return $this->eavConfig; - } - /** * Retrieve searchable attributes * * @param string $backendType * @return \Magento\Eav\Model\Entity\Attribute[] */ - private function getSearchableAttributes($backendType = null) + public function getSearchableAttributes($backendType = null) { if (null === $this->searchableAttributes) { $this->searchableAttributes = []; @@ -210,7 +205,7 @@ private function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->getEavConfig()->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -239,7 +234,7 @@ private function getSearchableAttributes($backendType = null) * @param int|string $attribute * @return \Magento\Eav\Model\Entity\Attribute */ - private function getSearchableAttribute($attribute) + public function getSearchableAttribute($attribute) { $attributes = $this->getSearchableAttributes(); if (is_numeric($attribute)) { @@ -254,7 +249,7 @@ private function getSearchableAttribute($attribute) } } - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); } /** @@ -487,11 +482,21 @@ private function getAttributeValue($attributeId, $valueId, $storeId) && $attribute->usesSource() && $this->engine->allowAdvancedIndex() ) { - $attribute->setStoreId($storeId); + if (!isset($this->attributeOptions[$attributeId][$storeId])) { + $attribute->setStoreId($storeId); + $options = $attribute->getSource()->toOptionArray(); + $this->attributeOptions[$attributeId][$storeId] = array_combine( + array_column($options, 'value'), + array_column($options, 'label') + ); + } - $valueText = (array) $attribute->getSource()->getIndexOptionText($valueId); + $valueText = ''; + if (isset($this->attributeOptions[$attributeId][$storeId][$valueId])) { + $valueText = $this->attributeOptions[$attributeId][$storeId][$valueId]; + } - $pieces = array_filter(array_merge([$value], $valueText)); + $pieces = array_filter(array_merge([$value], [$valueText])); $value = implode($this->separator, $pieces); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 42fd149b7301c..cfdfa0c1a3c8d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -6,9 +6,17 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; /** + * Class provides iterator through number of products suitable for fulltext indexation + * + * To be suitable for fulltext index product must meet set of requirements: + * - to be visible on frontend + * - to be enabled + * - in case product is composite at least one sub product must be visible and enabled + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -30,6 +38,8 @@ class Full * Index values separator * * @var string + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -37,6 +47,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] + * @deprecated Not used anymore */ protected $dates = []; @@ -44,6 +55,8 @@ class Full * Product Type Instances cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -51,6 +64,8 @@ class Full * Product Emulators cache * * @var array + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -77,6 +92,8 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -91,6 +108,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @deprecated Not used anymore */ protected $scopeConfig; @@ -98,6 +116,8 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -108,21 +128,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface + * @deprecated As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime + * @deprecated Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface + * @deprecated Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @deprecated Not used anymore */ protected $localeDate; @@ -133,16 +157,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext + * @deprecated Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config + * @deprecated As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory + * @deprecated As part of self::cleanIndex() */ private $dimensionFactory; @@ -153,6 +180,8 @@ class Full /** * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory + * @deprecated DataProvider used directly without IndexIterator + * @see self::$dataProvider */ private $iteratorFactory; @@ -161,6 +190,11 @@ class Full */ private $metadataPool; + /** + * @var DataProvider + */ + private $dataProvider; + /** * @param ResourceConnection $resource * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -181,6 +215,7 @@ class Full * @param \Magento\Framework\Indexer\ConfigInterface $indexerConfig * @param \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param DataProvider $dataProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +237,8 @@ public function __construct( \Magento\Framework\Search\Request\DimensionFactory $dimensionFactory, \Magento\Framework\Indexer\ConfigInterface $indexerConfig, \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\IndexIteratorFactory $indexIteratorFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool = null + \Magento\Framework\EntityManager\MetadataPool $metadataPool = null, + DataProvider $dataProvider = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -223,13 +259,16 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->dimensionFactory = $dimensionFactory; $this->iteratorFactory = $indexIteratorFactory; - $this->metadataPool = $metadataPool ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->dataProvider = $dataProvider ?: ObjectManager::getInstance()->get(DataProvider::class); } /** * Rebuild whole fulltext index for all stores * + * @deprecated Please use \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull instead + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext::executeFull * @return void */ public function reindexAll() @@ -282,52 +321,160 @@ protected function getProductIdsFromParents(array $entityIds) /** * Regenerate search index for specific store * + * To be suitable for indexation product must meet set of requirements: + * - to be visible on frontend + * - to be enabled + * - in case product is composite at least one sub product must be enabled + * * @param int $storeId Store View Id - * @param int|array $productIds Product Entity Id + * @param int[] $productIds Product Entity Id * @return \Generator - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ public function rebuildStoreIndex($storeId, $productIds = null) { if ($productIds !== null) { $productIds = array_unique(array_merge($productIds, $this->getProductIdsFromParents($productIds))); } + // prepare searchable attributes $staticFields = []; foreach ($this->getSearchableAttributes('static') as $attribute) { $staticFields[] = $attribute->getAttributeCode(); } + $dynamicFields = [ - 'int' => array_keys($this->getSearchableAttributes('int')), - 'varchar' => array_keys($this->getSearchableAttributes('varchar')), - 'text' => array_keys($this->getSearchableAttributes('text')), - 'decimal' => array_keys($this->getSearchableAttributes('decimal')), - 'datetime' => array_keys($this->getSearchableAttributes('datetime')), + 'int' => array_keys($this->dataProvider->getSearchableAttributes('int')), + 'varchar' => array_keys($this->dataProvider->getSearchableAttributes('varchar')), + 'text' => array_keys($this->dataProvider->getSearchableAttributes('text')), + 'decimal' => array_keys($this->dataProvider->getSearchableAttributes('decimal')), + 'datetime' => array_keys($this->dataProvider->getSearchableAttributes('datetime')), ]; - // status and visibility filter - $visibility = $this->getSearchableAttribute('visibility'); - $status = $this->getSearchableAttribute('status'); - $statusIds = $this->catalogProductStatus->getVisibleStatusIds(); + $lastProductId = 0; + $products = $this->dataProvider + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + while (count($products) > 0) { + + $productsIds = array_column($products, 'entity_id'); + $relatedProducts = $this->getRelatedProducts($products); + $productsIds = array_merge($productsIds, array_values($relatedProducts)); + + $productsAttributes = $this->dataProvider->getProductAttributes($storeId, $productsIds, $dynamicFields); + + foreach ($products as $productData) { + $lastProductId = $productData['entity_id']; + + if (!$this->isProductVisible($productData['entity_id'], $productsAttributes) || + !$this->isProductEnabled($productData['entity_id'], $productsAttributes) + ) { + continue; + } + + $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]]; + if (isset($relatedProducts[$productData['entity_id']])) { + $childProductsIndex = $this->getChildProductsIndex( + $productData['entity_id'], + $relatedProducts, + $productsAttributes + ); + if (empty($childProductsIndex)) { + continue; + } + $productIndex = $productIndex + $childProductsIndex; + } + + $index = $this->dataProvider->prepareProductIndex($productIndex, $productData, $storeId); + yield $productData['entity_id'] => $index; + } + $products = $this->dataProvider + ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId); + }; + } + + /** + * Get related (child) products ids + * + * Load related (child) products ids for composite product type + * + * @param array $products + * @return array + */ + private function getRelatedProducts($products) + { + $relatedProducts = []; + foreach ($products as $productData) { + $relatedProducts[$productData['entity_id']] = $this->dataProvider->getProductChildIds( + $productData['entity_id'], + $productData['type_id'] + ); + } + return array_filter($relatedProducts); + } + + /** + * Performs check that product is visible on Store Front + * + * Check that product is visible on Store Front using visibility attribute + * and allowed visibility values. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductVisible($productId, array $productsAttributes) + { + $visibility = $this->dataProvider->getSearchableAttribute('visibility'); $allowedVisibility = $this->engine->getAllowedVisibility(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$visibility->getId()]) && + in_array($productsAttributes[$productId][$visibility->getId()], $allowedVisibility); + } - return $this->iteratorFactory->create([ - 'storeId' => $storeId, - 'productIds' => $productIds, - 'staticFields' => $staticFields, - 'dynamicFields' => $dynamicFields, - 'visibility' => $visibility, - 'allowedVisibility' => $allowedVisibility, - 'status' => $status, - 'statusIds' => $statusIds - ]); + /** + * Performs check that product is enabled on Store Front + * + * Check that product is enabled on Store Front using status attribute + * and statuses allowed to be visible on Store Front. + * + * @param int $productId + * @param array $productsAttributes + * @return bool + */ + private function isProductEnabled($productId, array $productsAttributes) + { + $status = $this->dataProvider->getSearchableAttribute('status'); + $allowedStatuses = $this->catalogProductStatus->getVisibleStatusIds(); + return isset($productsAttributes[$productId]) && + isset($productsAttributes[$productId][$status->getId()]) && + in_array($productsAttributes[$productId][$status->getId()], $allowedStatuses); + } + + /** + * Get data for index using related(child) products data + * + * Build data for index using child products(if any). + * Use only enabled child products {@see isProductEnabled}. + * + * @param int $parentId + * @param array $relatedProducts + * @param array $productsAttributes + * @return array + */ + private function getChildProductsIndex($parentId, array $relatedProducts, array $productsAttributes) + { + $productIndex = []; + foreach ($relatedProducts[$parentId] as $productChildId) { + if ($this->isProductEnabled($productChildId, $productsAttributes)) { + $productIndex[$productChildId] = $productsAttributes[$productChildId]; + } + } + return $productIndex; } /** * Clean search index data for store * + * @deprecated As part of self::reindexAll() * @param int $storeId * @return void */ @@ -341,6 +488,7 @@ protected function cleanIndex($storeId) * Retrieve EAV Config Singleton * * @return \Magento\Eav\Model\Config + * @deprecated Use $self::$eavConfig directly */ protected function getEavConfig() { @@ -351,74 +499,31 @@ protected function getEavConfig() * Retrieve searchable attributes * * @param string $backendType + * @deprecated see DataProvider::getSearchableAttributes() * @return \Magento\Eav\Model\Entity\Attribute[] */ protected function getSearchableAttributes($backendType = null) { - if (null === $this->searchableAttributes) { - $this->searchableAttributes = []; - - $productAttributes = $this->productAttributeCollectionFactory->create(); - $productAttributes->addToIndexFilter(true); - - /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ - $attributes = $productAttributes->getItems(); - - $this->eventManager->dispatch( - 'catelogsearch_searchable_attributes_load_after', - ['engine' => $this->engine, 'attributes' => $attributes] - ); - - $entity = $this->getEavConfig()->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); - - foreach ($attributes as $attribute) { - $attribute->setEntity($entity); - } - - $this->searchableAttributes = $attributes; - } - - if ($backendType !== null) { - $attributes = []; - foreach ($this->searchableAttributes as $attributeId => $attribute) { - if ($attribute->getBackendType() == $backendType) { - $attributes[$attributeId] = $attribute; - } - } - - return $attributes; - } - - return $this->searchableAttributes; + return $this->dataProvider->getSearchableAttributes($backendType); } /** * Retrieve searchable attribute by Id or code * * @param int|string $attribute + * @deprecated see DataProvider::getSearchableAttributes() * @return \Magento\Eav\Model\Entity\Attribute */ protected function getSearchableAttribute($attribute) { - $attributes = $this->getSearchableAttributes(); - if (is_numeric($attribute)) { - if (isset($attributes[$attribute])) { - return $attributes[$attribute]; - } - } elseif (is_string($attribute)) { - foreach ($attributes as $attributeModel) { - if ($attributeModel->getAttributeCode() == $attribute) { - return $attributeModel; - } - } - } - - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->dataProvider->getSearchableAttribute($attribute); } /** * Returns expression for field unification * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::unifyField() * @param string $field * @param string $backendType * @return \Zend_Db_Expr @@ -436,6 +541,8 @@ protected function unifyField($field, $backendType = 'varchar') /** * Retrieve Product Type Instance * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductTypeInstance() * @param string $typeId * @return \Magento\Catalog\Model\Product\Type\AbstractType */ @@ -452,6 +559,8 @@ protected function getProductTypeInstance($typeId) /** * Retrieve Product Emulator (Magento Object) * + * @deprecated Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getProductEmulator() * @param string $typeId * @return \Magento\Framework\DataObject */ diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php index a8001645929ac..e62a0d058d8d4 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/IndexIterator.php @@ -10,6 +10,8 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @deprecated No more used + * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ class IndexIterator implements \Iterator { @@ -132,6 +134,8 @@ public function __construct( /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function current() { @@ -140,6 +144,8 @@ public function current() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function next() { @@ -236,6 +242,8 @@ public function next() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function key() { @@ -244,6 +252,8 @@ public function key() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function valid() { @@ -252,6 +262,8 @@ public function valid() /** * {@inheritDoc} + * + * @deprecated Since class is deprecated */ public function rewind() { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index 490a67dc892cc..bace3d5a0e7dc 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -362,8 +362,6 @@ protected function _renderFiltersBefore() [] ); - $this->_totalRecords = $this->searchResult->getTotalCount(); - if ($this->order && 'relevance' === $this->order['field']) { $this->getSelect()->order('search_result.'. TemporaryStorage::FIELD_SCORE . ' ' . $this->order['dir']); } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index 50f24a5170c08..e4c019ad9c9cd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -6,22 +6,22 @@ namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\Observer; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Event\ObserverInterface; use Magento\ImportExport\Model\Import as ImportExport; use Magento\Store\Model\Store; +use Magento\UrlRewrite\Model\MergeDataProviderFactory; +use Magento\UrlRewrite\Model\OptionProvider; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; use Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory; -use Magento\UrlRewrite\Model\OptionProvider; -use Magento\UrlRewrite\Model\UrlFinderInterface; -use Magento\Framework\Event\ObserverInterface; -use Magento\Catalog\Model\Product\Visibility; -use Magento\Framework\App\ObjectManager; -use Magento\UrlRewrite\Model\MergeDataProviderFactory; /** * Class AfterImportDataObserver @@ -102,6 +102,20 @@ class AfterImportDataObserver implements ObserverInterface /** @var \Magento\UrlRewrite\Model\MergeDataProvider */ private $mergeDataProviderPrototype; + /** + * Factory for creating category collection. + * + * @var CategoryCollectionFactory + */ + private $categoryCollectionFactory; + + /** + * Array of invoked categories during url rewrites generation. + * + * @var array + */ + private $categoriesCache = []; + /** * @param \Magento\Catalog\Model\ProductFactory $catalogProductFactory * @param \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory $objectRegistryFactory @@ -112,7 +126,7 @@ class AfterImportDataObserver implements ObserverInterface * @param UrlRewriteFactory $urlRewriteFactory * @param UrlFinderInterface $urlFinder * @param \Magento\UrlRewrite\Model\MergeDataProviderFactory|null $mergeDataProviderFactory - * @throws \InvalidArgumentException + * @param CategoryCollectionFactory|null $categoryCollectionFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -124,7 +138,8 @@ public function __construct( UrlPersistInterface $urlPersist, UrlRewriteFactory $urlRewriteFactory, UrlFinderInterface $urlFinder, - MergeDataProviderFactory $mergeDataProviderFactory = null + MergeDataProviderFactory $mergeDataProviderFactory = null, + CategoryCollectionFactory $categoryCollectionFactory = null ) { $this->urlPersist = $urlPersist; $this->catalogProductFactory = $catalogProductFactory; @@ -138,6 +153,8 @@ public function __construct( $mergeDataProviderFactory = ObjectManager::getInstance()->get(MergeDataProviderFactory::class); } $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create(); + $this->categoryCollectionFactory = $categoryCollectionFactory ?: + ObjectManager::getInstance()->get(CategoryCollectionFactory::class); } /** @@ -318,7 +335,7 @@ protected function canonicalUrlRewriteGenerate() } /** - * Generate list based on categories + * Generate list based on categories. * * @return UrlRewrite[] */ @@ -328,7 +345,7 @@ protected function categoriesUrlRewriteGenerate() foreach ($this->products as $productId => $productsByStores) { foreach ($productsByStores as $storeId => $product) { foreach ($this->categoryCache[$productId] as $categoryId) { - $category = $this->import->getCategoryProcessor()->getCategoryById($categoryId); + $category = $this->getCategoryById($categoryId, $storeId); if ($category->getParentId() == Category::TREE_ROOT_ID) { continue; } @@ -481,4 +498,27 @@ protected function isCategoryProperForGenerating($category, $storeId) $this->acceptableCategories[$storeId][$category->getId()] = $acceptable; return $acceptable; } + + /** + * Get category by id considering store scope. + * + * @param int $categoryId + * @param int $storeId + * @return Category|\Magento\Framework\DataObject + */ + private function getCategoryById($categoryId, $storeId) + { + if (!isset($this->categoriesCache[$categoryId][$storeId])) { + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->categoryCollectionFactory->create(); + $categoryCollection->addIdFilter([$categoryId]) + ->setStoreId($storeId) + ->addAttributeToSelect('name') + ->addAttributeToSelect('url_key') + ->addAttributeToSelect('url_path'); + $this->categoriesCache[$categoryId][$storeId] = $categoryCollection->getFirstItem(); + } + + return $this->categoriesCache[$categoryId][$storeId]; + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php index 1cb44a354c36f..995d9540ebc94 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php @@ -7,10 +7,12 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; -use Magento\Store\Model\Store; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\Store; /** * Class AfterImportDataObserverTest @@ -110,9 +112,16 @@ class AfterImportDataObserverTest extends \PHPUnit_Framework_TestCase */ private $product; - /** @var \Magento\UrlRewrite\Model\MergeDataProvider|\PHPUnit_Framework_MockObject_MockObject */ + /** + * @var \Magento\UrlRewrite\Model\MergeDataProvider|\PHPUnit_Framework_MockObject_MockObject + */ private $mergeDataProvider; + /** + * @var CategoryCollectionFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $categoryCollectionFactory; + /** * Test products returned by getBunch method of event object. * @@ -245,37 +254,6 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $categoryProcessor = $this->getMock( - \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor::class, - [ - 'getCategoryById', - ], - [], - '', - false - ); - $category = $this->getMock( - \Magento\Catalog\Model\Category::class, - [ - 'getId', - ], - [], - '', - false - ); - $category - ->expects($this->any()) - ->method('getId') - ->willReturn($this->categoryId); - $categoryProcessor - ->expects($this->any()) - ->method('getCategoryById') - ->with($this->categoryId) - ->willReturn($category); - $this->importProduct - ->expects($this->any()) - ->method('getCategoryProcessor') - ->willReturn($categoryProcessor); $mergeDataProviderFactory = $this->getMock( \Magento\UrlRewrite\Model\MergeDataProviderFactory::class, ['create'], @@ -286,6 +264,11 @@ protected function setUp() $this->mergeDataProvider = new \Magento\UrlRewrite\Model\MergeDataProvider; $mergeDataProviderFactory->expects($this->once())->method('create')->willReturn($this->mergeDataProvider); + $this->categoryCollectionFactory = $this->getMockBuilder(CategoryCollectionFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManager = new ObjectManager($this); $this->import = $this->objectManager->getObject( \Magento\CatalogUrlRewrite\Observer\AfterImportDataObserver::class, @@ -298,7 +281,8 @@ protected function setUp() 'urlPersist' => $this->urlPersist, 'urlRewriteFactory' => $this->urlRewriteFactory, 'urlFinder' => $this->urlFinder, - 'mergeDataProviderFactory' => $mergeDataProviderFactory + 'mergeDataProviderFactory' => $mergeDataProviderFactory, + 'categoryCollectionFactory' => $this->categoryCollectionFactory ] ); } @@ -563,6 +547,7 @@ public function testCanonicalUrlRewriteGenerateWithEmptyUrlPath() /** * Cover categoriesUrlRewriteGenerate(). + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testCategoriesUrlRewriteGenerate() { @@ -601,6 +586,33 @@ public function testCategoriesUrlRewriteGenerate() ->expects($this->any()) ->method('getId') ->will($this->returnValue($this->categoryId)); + + $categoryCollection = $this->getMockBuilder(CategoryCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $categoryCollection->expects($this->once()) + ->method('addIdFilter') + ->with([$this->categoryId]) + ->willReturnSelf(); + $categoryCollection->expects($this->once()) + ->method('setStoreId') + ->with($storeId) + ->willReturnSelf(); + $categoryCollection->expects($this->exactly(3)) + ->method('addAttributeToSelect') + ->withConsecutive( + ['name'], + ['url_key'], + ['url_path'] + )->willReturnSelf(); + $categoryCollection->expects($this->once()) + ->method('getFirstItem') + ->willReturn($category); + + $this->categoryCollectionFactory->expects($this->once()) + ->method('create') + ->willReturn($categoryCollection); + $this->urlRewrite ->expects($this->any()) ->method('setStoreId') diff --git a/app/code/Magento/Checkout/view/frontend/web/template/cart/shipping-rates.html b/app/code/Magento/Checkout/view/frontend/web/template/cart/shipping-rates.html index a922b8eac0c53..db2ff7df0e0b1 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/cart/shipping-rates.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/cart/shipping-rates.html @@ -11,7 +11,7 @@
-
+
@@ -29,7 +29,7 @@ "/>
diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js index a952639dac071..434676fc04116 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/js/view/checkout-agreements.js @@ -45,6 +45,26 @@ define([ agreementsModal.showModal(); }, + /** + * build a unique id for the term checkbox + * + * @param {Object} context - the ko context + * @param {Number} agreementId + */ + getCheckboxId: function (context, agreementId) { + var paymentMethodName = '', + paymentMethodRenderer = context.$parents[1]; + + // corresponding payment method fetched from parent context + if (paymentMethodRenderer) { + // item looks like this: {title: "Check / Money order", method: "checkmo"} + paymentMethodName = paymentMethodRenderer.item ? + paymentMethodRenderer.item.method : ''; + } + + return 'agreement_' + paymentMethodName + '_' + agreementId; + }, + /** * Init modal window for rendered element * diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index 4b657748dc988..a448537d64e83 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -11,11 +11,11 @@
-