diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 08e75ceebecc6..337134c9453c0 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -6,11 +6,16 @@ namespace Magento\CatalogSearch\Model\Indexer; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory; +use Magento\CatalogSearch\Model\Indexer\Scope\State; use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; -use \Magento\Framework\Search\Request\Config as SearchRequestConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Search\Request\Config as SearchRequestConfig; use Magento\Framework\Search\Request\DimensionFactory; use Magento\Store\Model\StoreManagerInterface; +/** + * Provide functionality for Fulltext Search indexing + */ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\Framework\Mview\ActionInterface { /** @@ -18,34 +23,51 @@ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\F */ const INDEXER_ID = 'catalogsearch_fulltext'; - /** @var array index structure */ + /** + * @var array index structure + */ protected $data; /** * @var IndexerHandlerFactory */ private $indexerHandlerFactory; + /** * @var StoreManagerInterface */ private $storeManager; + /** - * @var DimensionFactory + * @var \Magento\Framework\Search\Request\DimensionFactory */ private $dimensionFactory; + /** - * @var Full + * @var \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full */ private $fullAction; + /** * @var FulltextResource */ private $fulltextResource; + /** - * @var SearchRequestConfig + * @var \Magento\Framework\Search\Request\Config */ private $searchRequestConfig; + /** + * @var IndexSwitcherInterface + */ + private $indexSwitcher; + + /** + * @var \Magento\CatalogSearch\Model\Indexer\Scope\State + */ + private $indexScopeState; + /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -54,6 +76,8 @@ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\F * @param FulltextResource $fulltextResource * @param SearchRequestConfig $searchRequestConfig * @param array $data + * @param IndexSwitcherInterface $indexSwitcher + * @param Scope\State $indexScopeState */ public function __construct( FullFactory $fullActionFactory, @@ -62,7 +86,9 @@ public function __construct( DimensionFactory $dimensionFactory, FulltextResource $fulltextResource, SearchRequestConfig $searchRequestConfig, - array $data + array $data, + IndexSwitcherInterface $indexSwitcher = null, + State $indexScopeState = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -71,6 +97,14 @@ public function __construct( $this->fulltextResource = $fulltextResource; $this->searchRequestConfig = $searchRequestConfig; $this->data = $data; + if (null === $indexSwitcher) { + $indexSwitcher = ObjectManager::getInstance()->get(IndexSwitcherInterface::class); + } + if (null === $indexScopeState) { + $indexScopeState = ObjectManager::getInstance()->get(State::class); + } + $this->indexSwitcher = $indexSwitcher; + $this->indexScopeState = $indexScopeState; } /** @@ -106,10 +140,14 @@ public function executeFull() 'data' => $this->data ]); foreach ($storeIds as $storeId) { - $dimension = $this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId]); - $saveHandler->cleanIndex([$dimension]); - $saveHandler->saveIndex([$dimension], $this->fullAction->rebuildStoreIndex($storeId)); + $dimensions = [$this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId])]; + $this->indexScopeState->useTemporaryIndex(); + + $saveHandler->cleanIndex($dimensions); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId)); + $this->indexSwitcher->switchIndex($dimensions); + $this->indexScopeState->useRegularIndex(); } $this->fulltextResource->resetSearchResults(); $this->searchRequestConfig->reset(); diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php index 9a2bb76c04ce7..d2d76bc71ccbf 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexStructure.php @@ -12,6 +12,7 @@ use Magento\Framework\Search\Request\Dimension; use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; class IndexStructure implements IndexStructureInterface { @@ -19,6 +20,7 @@ class IndexStructure implements IndexStructureInterface * @var Resource */ private $resource; + /** * @var IndexScopeResolver */ @@ -26,11 +28,11 @@ class IndexStructure implements IndexStructureInterface /** * @param ResourceConnection $resource - * @param IndexScopeResolver $indexScopeResolver + * @param IndexScopeResolverInterface $indexScopeResolver */ public function __construct( ResourceConnection $resource, - IndexScopeResolver $indexScopeResolver + IndexScopeResolverInterface $indexScopeResolver ) { $this->resource = $resource; $this->indexScopeResolver = $indexScopeResolver; diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php new file mode 100644 index 0000000000000..ba5fb461dde35 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexSwitcherInterface.php @@ -0,0 +1,22 @@ +objectManager = $objectManager; + $this->scopeConfig = $scopeConfig; + $this->configPath = $configPath; + $this->handlers = $handlers; + } + + /** + * {@inheritDoc} + * + * As index switcher is an optional part of the search SPI, it may be not defined by a search engine. + * It is especially reasonable for search engines with pre-defined indexes declaration (like old SOLR and Sphinx) + * which cannot create temporary indexes on the fly. + * That's the reason why this method do nothing for the case + * when switcher is not defined for a specific search engine. + */ + public function switchIndex(array $dimensions) + { + $currentHandler = $this->scopeConfig->getValue($this->configPath, ScopeInterface::SCOPE_STORE); + if (!isset($this->handlers[$currentHandler])) { + return; + } + $this->create($currentHandler)->switchIndex($dimensions); + } + + /** + * Create indexer handler + * + * @param string $handler + * @return IndexSwitcherInterface + */ + private function create($handler) + { + $indexSwitcher = $this->objectManager->create($this->handlers[$handler]); + + if (!$indexSwitcher instanceof IndexSwitcherInterface) { + throw new \InvalidArgumentException( + $handler . ' index switcher doesn\'t implement ' . IndexSwitcherInterface::class + ); + } + + return $indexSwitcher; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php index 8d99cd4492bf5..2aa9a418bd725 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/IndexerHandler.php @@ -7,13 +7,11 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Search\Request\Dimension; use Magento\Framework\Search\Request\IndexScopeResolverInterface; use Magento\Framework\Indexer\SaveHandler\Batch; -use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; class IndexerHandler implements IndexerInterface { @@ -62,7 +60,7 @@ class IndexerHandler implements IndexerInterface * @param ResourceConnection $resource * @param Config $eavConfig * @param Batch $batch - * @param \Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver $indexScopeResolver + * @param IndexScopeResolverInterface $indexScopeResolver * @param array $data * @param int $batchSize */ @@ -71,7 +69,7 @@ public function __construct( ResourceConnection $resource, Config $eavConfig, Batch $batch, - IndexScopeResolver $indexScopeResolver, + IndexScopeResolverInterface $indexScopeResolver, array $data, $batchSize = 100 ) { diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php new file mode 100644 index 0000000000000..87a7b7110d3aa --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexSwitcher.php @@ -0,0 +1,76 @@ +resource = $resource; + $this->resolver = $indexScopeResolver; + $this->state = $state; + } + + /** + * {@inheritdoc} + * @throws IndexTableNotExistException + */ + public function switchIndex(array $dimensions) + { + if (State::USE_TEMPORARY_INDEX === $this->state->getState()) { + $index = \Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID; + + $temporalIndexTable = $this->resolver->resolve($index, $dimensions); + if (!$this->resource->getConnection()->isTableExists($temporalIndexTable)) { + throw new IndexTableNotExistException( + __( + "Temporary table for index $index doesn't exist," + . " which is inconsistent with state of scope resolver" + ) + ); + } + + $this->state->useRegularIndex(); + $tableName = $this->resolver->resolve($index, $dimensions); + if ($this->resource->getConnection()->isTableExists($tableName)) { + $this->resource->getConnection()->dropTable($tableName); + } + + $this->resource->getConnection()->renameTable($temporalIndexTable, $tableName); + $this->state->useTemporaryIndex(); + } + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php new file mode 100644 index 0000000000000..6974f8c278ab5 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/IndexTableNotExistException.php @@ -0,0 +1,18 @@ +objectManager = $objectManager; + $this->scopeState = $scopeState; + $this->states = $states; + } + + /** + * Creates class instance with specified parameters + * + * @param string $state + * @return \Magento\Framework\Search\Request\IndexScopeResolverInterface + * @throws UnknownStateException + */ + private function create($state) + { + if (!array_key_exists($state, $this->states)) { + throw new UnknownStateException(__("Requested resolver for unknown indexer state: $state")); + } + return $this->objectManager->create($this->states[$state]); + } + + /** + * @param string $index + * @param Dimension[] $dimensions + * @return string + */ + public function resolve($index, array $dimensions) + { + return $this->create($this->scopeState->getState())->resolve($index, $dimensions); + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php new file mode 100644 index 0000000000000..2bba29ae8d842 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/State.php @@ -0,0 +1,65 @@ +state = self::USE_TEMPORARY_INDEX; + } + + /** + * Set the state to use regular Index + * @return void + */ + public function useRegularIndex() + { + $this->state = self::USE_REGULAR_INDEX; + } + + /** + * @return string + */ + public function getState() + { + return $this->state; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php new file mode 100644 index 0000000000000..51037eb637cf3 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/TemporaryResolver.php @@ -0,0 +1,43 @@ +indexScopeResolver = $indexScopeResolver; + } + + /** + * @param string $index + * @param Dimension[] $dimensions + * @return string + */ + public function resolve($index, array $dimensions) + { + $tableName = $this->indexScopeResolver->resolve($index, $dimensions); + $tableName .= \Magento\Framework\Indexer\Table\StrategyInterface::TMP_SUFFIX; + + return $tableName; + } +} diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php new file mode 100644 index 0000000000000..04803ef27480e --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php @@ -0,0 +1,18 @@ +fullAction = $this->getClassMock(\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::class); @@ -80,27 +88,29 @@ protected function setUp() [] ); - $this->dimension = $this->getClassMock(\Magento\Framework\Search\Request\Dimension::class); - $dimensionFactory = $this->getMock( - \Magento\Framework\Search\Request\DimensionFactory::class, - ['create'], - [], - '', - false - ); - $dimensionFactory->expects($this->any())->method('create')->willReturn($this->dimension); + $this->dimensionFactory = $this->getMock(DimensionFactory::class, ['create'], [], '', false); $this->fulltextResource = $this->getClassMock(\Magento\CatalogSearch\Model\ResourceModel\Fulltext::class); $this->searchRequestConfig = $this->getClassMock(\Magento\Framework\Search\Request\Config::class); - $this->model = new \Magento\CatalogSearch\Model\Indexer\Fulltext( - $fullActionFactory, - $indexerHandlerFactory, - $this->storeManager, - $dimensionFactory, - $this->fulltextResource, - $this->searchRequestConfig, - [] + $this->indexSwitcher = $this->getMockBuilder(\Magento\CatalogSearch\Model\Indexer\Scope\IndexSwitcher::class) + ->disableOriginalConstructor() + ->setMethods(['switchIndex']) + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + \Magento\CatalogSearch\Model\Indexer\Fulltext::class, + [ + 'fullActionFactory' => $fullActionFactory, + 'indexerHandlerFactory' => $indexerHandlerFactory, + 'storeManager' => $this->storeManager, + 'dimensionFactory' => $this->dimensionFactory, + 'fulltextResource' => $this->fulltextResource, + 'searchRequestConfig' => $this->searchRequestConfig, + 'data' => [], + 'indexSwitcher' => $this->indexSwitcher, + ] ); } @@ -131,13 +141,38 @@ public function testExecute() public function testExecuteFull() { $stores = [0 => 'Store 1', 1 => 'Store 2']; - $indexData = new \ArrayObject([]); + $indexData = new \ArrayObject([new \ArrayObject([]), new \ArrayObject([])]); $this->storeManager->expects($this->once())->method('getStores')->willReturn($stores); - $this->saveHandler->expects($this->exactly(count($stores)))->method('cleanIndex'); - $this->saveHandler->expects($this->exactly(2))->method('saveIndex'); + + $dimensionScope1 = $this->getMock(Dimension::class, [], ['scope', '1']); + $dimensionScope2 = $this->getMock(Dimension::class, [], ['scope', '2']); + + $this->dimensionFactory->expects($this->any())->method('create')->willReturnOnConsecutiveCalls( + $dimensionScope1, + $dimensionScope2 + ); + $this->indexSwitcher->expects($this->exactly(2))->method('switchIndex') + ->withConsecutive( + [$this->equalTo([$dimensionScope1])], + [$this->equalTo([$dimensionScope2])] + ); + + $this->saveHandler->expects($this->exactly(count($stores)))->method('cleanIndex') + ->withConsecutive( + [$this->equalTo([$dimensionScope1])], + [$this->equalTo([$dimensionScope2])] + ); + + $this->saveHandler->expects($this->exactly(2))->method('saveIndex') + ->withConsecutive( + [$this->equalTo([$dimensionScope1]), $this->equalTo($indexData)], + [$this->equalTo([$dimensionScope2]), $this->equalTo($indexData)] + ); $this->fullAction->expects($this->exactly(2)) ->method('rebuildStoreIndex') - ->willReturn(new \ArrayObject([$indexData, $indexData])); + ->withConsecutive([0], [1]) + ->willReturn($indexData); + $this->fulltextResource->expects($this->once())->method('resetSearchResults'); $this->searchRequestConfig->expects($this->once())->method('reset'); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Scope/IndexSwitcherTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Scope/IndexSwitcherTest.php new file mode 100644 index 0000000000000..31bec5906ae5a --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Scope/IndexSwitcherTest.php @@ -0,0 +1,212 @@ +resource = $this->getMockBuilder(ResourceConnection::class) + ->setMethods(['getConnection']) + ->disableOriginalConstructor() + ->getMock(); + $this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) + ->setMethods(['isTableExists', 'dropTable', 'renameTable']) + ->getMockForAbstractClass(); + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + $this->indexScopeResolver = $this->getMockBuilder( + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class + ) + ->setMethods(['resolve']) + ->getMockForAbstractClass(); + $this->scopeState = $this->getMockBuilder(State::class) + ->setMethods(['getState', 'useRegularIndex', 'useTemporaryIndex']) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->indexSwitcher = $objectManagerHelper->getObject( + \Magento\CatalogSearch\Model\Indexer\Scope\IndexSwitcher::class, + [ + 'resource' => $this->resource, + 'indexScopeResolver' => $this->indexScopeResolver, + 'state' => $this->scopeState, + ] + ); + } + + public function testSwitchRegularIndex() + { + $dimensions = [$this->getMockBuilder(Dimension::class)->setConstructorArgs(['scope', '1'])]; + + $this->scopeState->expects($this->once()) + ->method('getState') + ->willReturn(State::USE_REGULAR_INDEX); + + $this->indexScopeResolver->expects($this->never())->method('resolve'); + $this->connection->expects($this->never())->method('renameTable'); + + $this->indexSwitcher->switchIndex($dimensions); + } + + public function testSwitchIndexWithUnknownState() + { + $dimensions = [$this->getMockBuilder(Dimension::class)->setConstructorArgs(['scope', '1'])]; + + $this->scopeState->expects($this->once()) + ->method('getState') + ->willReturn('unknown_state'); + + $this->indexScopeResolver->expects($this->never())->method('resolve'); + $this->connection->expects($this->never())->method('renameTable'); + + $this->indexSwitcher->switchIndex($dimensions); + } + + public function testSwitchTemporaryIndexWhenRegularIndexExist() + { + $dimensions = [$this->getMockBuilder(Dimension::class)->setConstructorArgs(['scope', '1'])]; + + $this->scopeState->expects($this->once()) + ->method('getState') + ->willReturn(State::USE_TEMPORARY_INDEX); + + $this->scopeState->expects($this->at(1))->method('useRegularIndex'); + $this->scopeState->expects($this->at(2))->method('useTemporaryIndex'); + + $this->indexScopeResolver->expects($this->exactly(2))->method('resolve') + ->withConsecutive( + [$this->equalTo(FulltextIndexer::INDEXER_ID), $this->equalTo($dimensions)], + [$this->equalTo(FulltextIndexer::INDEXER_ID), $this->equalTo($dimensions)] + ) + ->willReturnOnConsecutiveCalls( + 'catalogsearch_fulltext_scope1_tmp1', + 'catalogsearch_fulltext_scope1' + ); + + $this->connection->expects($this->exactly(2))->method('isTableExists') + ->withConsecutive( + [$this->equalTo('catalogsearch_fulltext_scope1_tmp1'), $this->equalTo(null)], + [$this->equalTo('catalogsearch_fulltext_scope1'), $this->equalTo(null)] + ) + ->willReturnOnConsecutiveCalls( + true, + true + ); + + $this->connection->expects($this->once())->method('dropTable')->with('catalogsearch_fulltext_scope1', null); + $this->connection->expects($this->once()) + ->method('renameTable') + ->with('catalogsearch_fulltext_scope1_tmp1', 'catalogsearch_fulltext_scope1'); + + $this->indexSwitcher->switchIndex($dimensions); + } + + public function testSwitchTemporaryIndexWhenRegularIndexNotExist() + { + $dimensions = [$this->getMockBuilder(Dimension::class)->setConstructorArgs(['scope', '1'])]; + + $this->scopeState->expects($this->once()) + ->method('getState') + ->willReturn(State::USE_TEMPORARY_INDEX); + + $this->scopeState->expects($this->at(1))->method('useRegularIndex'); + $this->scopeState->expects($this->at(2))->method('useTemporaryIndex'); + + $this->indexScopeResolver->expects($this->exactly(2))->method('resolve') + ->withConsecutive( + [$this->equalTo(FulltextIndexer::INDEXER_ID), $this->equalTo($dimensions)], + [$this->equalTo(FulltextIndexer::INDEXER_ID), $this->equalTo($dimensions)] + ) + ->willReturnOnConsecutiveCalls( + 'catalogsearch_fulltext_scope1_tmp1', + 'catalogsearch_fulltext_scope1' + ); + + $this->connection->expects($this->exactly(2))->method('isTableExists') + ->withConsecutive( + [$this->equalTo('catalogsearch_fulltext_scope1_tmp1'), $this->equalTo(null)], + [$this->equalTo('catalogsearch_fulltext_scope1'), $this->equalTo(null)] + ) + ->willReturnOnConsecutiveCalls( + true, + false + ); + + $this->connection->expects($this->never())->method('dropTable')->with('catalogsearch_fulltext_scope1', null); + $this->connection->expects($this->once()) + ->method('renameTable') + ->with('catalogsearch_fulltext_scope1_tmp1', 'catalogsearch_fulltext_scope1'); + + $this->indexSwitcher->switchIndex($dimensions); + } + + /** + * @expectedException \Magento\CatalogSearch\Model\Indexer\Scope\IndexTableNotExistException + * @expectedExceptionMessage Temporary table for index catalogsearch_fulltext doesn't exist + */ + public function testSwitchWhenTemporaryIndexNotExist() + { + $dimensions = [$this->getMockBuilder(Dimension::class)->setConstructorArgs(['scope', '1'])]; + + $this->scopeState->expects($this->once()) + ->method('getState') + ->willReturn(State::USE_TEMPORARY_INDEX); + + $this->scopeState->expects($this->never())->method('useRegularIndex'); + $this->scopeState->expects($this->never())->method('useTemporaryIndex'); + + $this->indexScopeResolver->expects($this->once())->method('resolve') + ->with(FulltextIndexer::INDEXER_ID, $dimensions) + ->willReturn('catalogsearch_fulltext_scope1_tmp1'); + + $this->connection->expects($this->once()) + ->method('isTableExists') + ->with('catalogsearch_fulltext_scope1_tmp1', null) + ->willReturn(false); + + $this->connection->expects($this->never())->method('dropTable')->with('catalogsearch_fulltext_scope1', null); + $this->connection->expects($this->never())->method('renameTable'); + + $this->indexSwitcher->switchIndex($dimensions); + } +} diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php index 4b04e7d66ab56..d44fef54683fc 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php @@ -74,7 +74,10 @@ protected function setUp() $productLimitationMock = $this->getMock( \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class ); - $productLimitationFactoryMock = $this->getMock(ProductLimitationFactory::class, ['create']); + $productLimitationFactoryMock = $this->getMockBuilder(ProductLimitationFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); $productLimitationFactoryMock->method('create') ->willReturn($productLimitationMock); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php index 5e56a25356359..5732ed2c8aee1 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php @@ -80,7 +80,10 @@ protected function setUp() $productLimitationMock = $this->getMock( \Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitation::class ); - $productLimitationFactoryMock = $this->getMock(ProductLimitationFactory::class, ['create']); + $productLimitationFactoryMock = $this->getMockBuilder(ProductLimitationFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); $productLimitationFactoryMock->method('create') ->willReturn($productLimitationMock); diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php index 3bb270e990617..8f4bb7736cd37 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/Indexer/IndexStructureTest.php @@ -48,9 +48,8 @@ protected function setUp() ->method('getConnection') ->willReturn($this->connection); $this->indexScopeResolver = $this->getMockBuilder( - \Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver::class + \Magento\Framework\Search\Request\IndexScopeResolverInterface::class )->setMethods(['resolve']) - ->disableOriginalConstructor() ->getMock(); $objectManager = new ObjectManager($this); @@ -64,11 +63,6 @@ protected function setUp() ); } - /** - * @param string $table - * @param array $dimensions - * @param bool $isTableExist - */ public function testDelete() { $index = 'index_name'; diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 7e9451f9b83e7..7116abf263b0d 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -12,6 +12,7 @@ + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH @@ -20,6 +21,14 @@ + + + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH + + \Magento\CatalogSearch\Model\Indexer\Scope\IndexSwitcher + + + Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH @@ -246,4 +255,27 @@ + + + \Magento\CatalogSearch\Model\Indexer\Scope\ScopeProxy + + + + + \Magento\CatalogSearch\Model\Indexer\Scope\ScopeProxy + + + + + \Magento\CatalogSearch\Model\Indexer\Scope\ScopeProxy + + + + + + \Magento\CatalogSearch\Model\Indexer\Scope\TemporaryResolver + \Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver + + + diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php index 6f2b989e5e9bf..794034b446f72 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php @@ -29,18 +29,25 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $connection = $this->getConnection(); $idxTable = $usePrimaryTable ? $this->getMainTable() : $this->getIdxTable(); $select = parent::_getStockStatusSelect($entityIds, $usePrimaryTable); + $linkField = $metadata->getLinkField(); $select->reset( \Magento\Framework\DB\Select::COLUMNS )->columns( ['e.entity_id', 'cis.website_id', 'cis.stock_id'] )->joinLeft( ['l' => $this->getTable('catalog_product_super_link')], - 'l.parent_id = e.' . $metadata->getLinkField(), + 'l.parent_id = e.' . $linkField, [] )->join( ['le' => $this->getTable('catalog_product_entity')], 'le.entity_id = l.product_id', [] + )->joinInner( + ['cpei' => $this->getTable('catalog_product_entity_int')], + 'le.' . $linkField . ' = cpei.' . $linkField + . ' AND cpei.attribute_id = ' . $this->_getAttribute('status')->getId() + . ' AND cpei.value = ' . ProductStatus::STATUS_ENABLED, + [] )->joinLeft( ['i' => $idxTable], 'i.product_id = l.product_id AND cis.website_id = i.website_id AND cis.stock_id = i.stock_id', diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js index c182d9f8216c0..be44c110c2b62 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.js @@ -32,6 +32,7 @@ define([ identificationProperty: 'id', 'attribute_set_id': '', attributesTmp: [], + changedFlag: 'was_changed', listens: { 'insertDataFromGrid': 'processingInsertDataFromGrid', 'insertDataFromWizard': 'processingInsertDataFromWizard', @@ -391,9 +392,9 @@ define([ 'small_image': row['small_image'], image: row.image, 'thumbnail': row.thumbnail, - 'attributes': attributesText, - 'was_changed': true + 'attributes': attributesText }; + product[this.changedFlag] = true; product[this.canEditField] = row.editable; product[this.newProductField] = row.newProduct; tmpArray.push(product); @@ -515,6 +516,7 @@ define([ tmpArray[rowIndex].status = 1; } + tmpArray[rowIndex][this.changedFlag] = true; this.unionInsertData(tmpArray); } }); diff --git a/app/code/Magento/Customer/Model/Indexer/Source.php b/app/code/Magento/Customer/Model/Indexer/Source.php new file mode 100644 index 0000000000000..60522227eb222 --- /dev/null +++ b/app/code/Magento/Customer/Model/Indexer/Source.php @@ -0,0 +1,108 @@ +customerCollection = $collectionFactory->create(); + $this->batchSize = $batchSize; + } + + /** + * {@inheritdoc} + */ + public function getMainTable() + { + return $this->customerCollection->getMainTable(); + } + + /** + * {@inheritdoc} + */ + public function getIdFieldName() + { + return $this->customerCollection->getIdFieldName(); + } + + /** + * {@inheritdoc} + */ + public function addFieldToSelect($fieldName, $alias = null) + { + $this->customerCollection->addFieldToSelect($fieldName, $alias); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSelect() + { + return $this->customerCollection->getSelect(); + } + + /** + * {@inheritdoc} + */ + public function addFieldToFilter($attribute, $condition = null) + { + $this->customerCollection->addFieldToFilter($attribute, $condition); + return $this; + } + + /** + * @return int + */ + public function count() + { + return $this->customerCollection->getSize(); + } + + /** + * Retrieve an iterator + * + * @return Traversable + */ + public function getIterator() + { + $this->customerCollection->setPageSize($this->batchSize); + $lastPage = $this->customerCollection->getLastPageNumber(); + $pageNumber = 0; + do { + $this->customerCollection->clear(); + $this->customerCollection->setCurPage($pageNumber); + foreach ($this->customerCollection->getItems() as $key => $value) { + yield $key => $value; + } + $pageNumber++; + } while ($pageNumber <= $lastPage); + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer/Indexer/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Customer/Indexer/Collection.php new file mode 100644 index 0000000000000..5b9716af5393a --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer/Indexer/Collection.php @@ -0,0 +1,20 @@ +Customer Grid Rebuild Customer grid index -
diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php index 0b761d8c191ab..6a09bea591cb8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php @@ -391,6 +391,7 @@ public function validOptionDataProvider() */ public function testUpdateNegative($optionData, $message) { + $this->_markTestAsRestOnly(); $productSku = 'simple'; /** @var ProductRepository $productRepository */ $productRepository = $this->objectManager->create(ProductRepository::class); @@ -403,18 +404,9 @@ public function testUpdateNegative($optionData, $message) 'resourcePath' => '/V1/products/options/' . $optionId, 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => 'V1', - 'operation' => self::SERVICE_NAME . 'Save', - ], ]; - if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { - $this->setExpectedException('SoapFault'); - } else { - $this->setExpectedException('Exception', $message, 400); - } + $this->setExpectedException('Exception', $message, 400); $this->_webApiCall($serviceInfo, ['option' => $optionData]); } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index cd02c6d4d7500..ed641ac0f85b8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -134,15 +134,6 @@ public function productCreationProvider() ]; } - private function markAreaAsSecure() - { - /** @var \Magento\Framework\Registry $registry */ - $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Framework\Registry::class); - $registry->unregister("isSecureArea"); - $registry->register("isSecureArea", true); - } - /** * Test removing association between product and website 1 * @magentoApiDataFixture Magento/Catalog/_files/product_with_two_websites.php @@ -169,9 +160,6 @@ public function testUpdateWithDeleteWebsites() $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"], $websitesData["website_ids"] ); - $this->deleteProduct($productBuilder[ProductInterface::SKU]); - $this->markAreaAsSecure(); - $website->delete(); } /** @@ -198,9 +186,6 @@ public function testDeleteAllWebsiteAssociations() $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"], $websitesData["website_ids"] ); - $this->deleteProduct($productBuilder[ProductInterface::SKU]); - $this->markAreaAsSecure(); - $website->delete(); } /** @@ -222,7 +207,7 @@ public function testCreateWithMultipleWebsites() $websitesData = [ 'website_ids' => [ 1, - $website->getId(), + (int) $website->getId(), ] ]; $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData; @@ -231,9 +216,6 @@ public function testCreateWithMultipleWebsites() $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]["website_ids"], $websitesData["website_ids"] ); - $this->deleteProduct($productBuilder[ProductInterface::SKU]); - $this->markAreaAsSecure(); - $website->delete(); } /** diff --git a/dev/tests/integration/framework/Magento/TestFramework/Bootstrap/DocBlock.php b/dev/tests/integration/framework/Magento/TestFramework/Bootstrap/DocBlock.php index bf890b2448e23..d23985bc38ef9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Bootstrap/DocBlock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Bootstrap/DocBlock.php @@ -40,6 +40,7 @@ public function registerAnnotations(\Magento\TestFramework\Application $applicat * * Note: order of registering (and applying) annotations is important. * To allow config fixtures to deal with fixture stores, data fixtures should be processed first. + * ConfigFixture applied twice because data fixtures could clean config and clean custom settings * * @param \Magento\TestFramework\Application $application * @return array @@ -68,6 +69,7 @@ protected function _getSubscribers(\Magento\TestFramework\Application $applicati new \Magento\TestFramework\Annotation\AppArea($application), new \Magento\TestFramework\Annotation\Cache($application), new \Magento\TestFramework\Annotation\AdminConfigFixture(), + new \Magento\TestFramework\Annotation\ConfigFixture(), ]; } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php b/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php index ad685845cde00..03fb9aa9ddae6 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php +++ b/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php @@ -24,8 +24,6 @@ class ObjectManager extends \Magento\Framework\App\ObjectManager * @var array */ protected $persistedInstances = [ - \Magento\TestFramework\App\Config::class, - \Magento\Framework\App\Config\ScopeConfigInterface::class, \Magento\Framework\App\ResourceConnection::class, \Magento\Framework\Config\Scope::class, \Magento\Framework\ObjectManager\RelationsInterface::class, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/DataTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/DataTest.php index 071c2499bb332..c2db58728d33d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/DataTest.php @@ -248,6 +248,7 @@ public function testGetPageTemplateProcessor() * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php * @magentoDbIsolation enabled + * @magentoAppIsolation enabled * @dataProvider getTaxPriceDataProvider */ public function testGetTaxPrice( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites.php index fa64de38e4311..426ee922f2614 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites.php @@ -9,25 +9,23 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var Magento\Store\Model\Website $website */ $website = $objectManager->get(Magento\Store\Model\Website::class); -$website->load('second_website', 'code'); -if (!$website->getId()) { - $website->setData( - [ - 'code' => 'second_website', - 'name' => 'Test Website', - ] - ); +$website->setData( + [ + 'code' => 'second_website', + 'name' => 'Test Website', + ] +); - $website->save(); -} +$website->save(); + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); /** @var $product \Magento\Catalog\Model\Product */ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(ProductInterface::class); $product ->setTypeId('simple') - ->setId(1) ->setAttributeSetId(4) ->setWebsiteIds([1, $website->getId()]) ->setName('Simple Product') diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php new file mode 100644 index 0000000000000..68f1106dcfe30 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_two_websites_rollback.php @@ -0,0 +1,35 @@ +get(\Magento\Framework\Registry::class); +$registry->unregister("isSecureArea"); +$registry->register("isSecureArea", true); + +/** @var Magento\Store\Model\Website $website */ +$website = $objectManager->create(\Magento\Store\Model\Website::class); +$website->load('second_website'); +$website->delete(); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $firstProduct = $productRepository->get('unique-simple-azaza'); + $firstProduct->delete(); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website.php index 6749d421e7998..4dc026d6fad4d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website.php @@ -19,3 +19,5 @@ $website->save(); } + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website_rollback.php new file mode 100644 index 0000000000000..173793a1caa49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_website_rollback.php @@ -0,0 +1,22 @@ +get('Magento\Framework\Registry'); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$website = $objectManager->get(Magento\Store\Model\Website::class); +$website->load('test_website', 'code'); + +if ($website->getId()) { + $website->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->reinitStores(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/IndexSwitcherMock.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/IndexSwitcherMock.php new file mode 100644 index 0000000000000..955ccce3bb59e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/IndexSwitcherMock.php @@ -0,0 +1,54 @@ +indexSwitcher = $indexSwitcher; + } + + /** + * Switch current index with temporary index + * + * It will drop current index table and rename temporary index table to the current index table. + * + * @param array $dimensions + * @return void + */ + public function switchIndex(array $dimensions) + { + $this->isSwitched |= true; + $this->indexSwitcher->switchIndex($dimensions); + } + + /** + * @return bool + */ + public function isSwitched() + { + return (bool) $this->isSwitched; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/SwitcherUsedInFulltextTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/SwitcherUsedInFulltextTest.php new file mode 100644 index 0000000000000..afcfef5fc4c8e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/SwitcherUsedInFulltextTest.php @@ -0,0 +1,214 @@ +configure( + [ + ltrim(Fulltext::class, '\\') => [ + 'arguments' => [ + 'indexSwitcher' => [ + 'instance' => ltrim(IndexSwitcherMock::class, '\\'), + ], + ], + ], + ] + ); + + /** @var \Magento\Framework\Indexer\IndexerInterface indexer */ + $this->indexer = $objectManager->create( + \Magento\Indexer\Model\Indexer::class + ); + $this->indexer->load('catalogsearch_fulltext'); + + $this->engine = $objectManager->get( + \Magento\CatalogSearch\Model\ResourceModel\Engine::class + ); + + $this->resourceFulltext = $objectManager->get( + \Magento\CatalogSearch\Model\ResourceModel\Fulltext::class + ); + + $this->queryFactory = $objectManager->get( + \Magento\Search\Model\QueryFactory::class + ); + + $this->dimension = $objectManager->create( + \Magento\Framework\Search\Request\Dimension::class, + ['name' => 'scope', 'value' => '1'] + ); + + $this->indexSwitcher = Bootstrap::getObjectManager()->get( + IndexSwitcherMock::class + ); + + $this->productApple = $this->getProductBySku('fulltext-1'); + $this->productBanana = $this->getProductBySku('fulltext-2'); + $this->productOrange = $this->getProductBySku('fulltext-3'); + $this->productPapaya = $this->getProductBySku('fulltext-4'); + $this->productCherry = $this->getProductBySku('fulltext-5'); + } + + /** + * @magentoAppIsolation enabled + */ + public function testReindexAll() + { + $this->indexer->reindexAll(); + + /** @var IndexSwitcherMock $indexSwitcher */ + $indexSwitcher = Bootstrap::getObjectManager()->get( + IndexSwitcherMock::class + ); + $this->assertTrue($indexSwitcher->isSwitched()); + } + + /** + * @magentoAppIsolation enabled + */ + public function testReindexList() + { + $this->indexer->reindexList([$this->productApple->getId(), $this->productBanana->getId()]); + + /** @var IndexSwitcherMock $indexSwitcher */ + $indexSwitcher = Bootstrap::getObjectManager()->get( + IndexSwitcherMock::class + ); + $this->assertFalse($indexSwitcher->isSwitched()); + } + + /** + * @magentoAppIsolation enabled + */ + public function testReindexRow() + { + $this->indexer->reindexRow($this->productPapaya->getId()); + + /** @var IndexSwitcherMock $indexSwitcher */ + $indexSwitcher = Bootstrap::getObjectManager()->get( + IndexSwitcherMock::class + ); + $this->assertFalse($indexSwitcher->isSwitched()); + } + + /** + * Search the text and return result collection + * + * @param string $text + * @return Product[] + */ + protected function search($text) + { + $this->resourceFulltext->resetSearchResults(); + $query = $this->queryFactory->get(); + $query->unsetData(); + $query->setQueryText($text); + $query->saveIncrementalPopularity(); + $products = []; + $collection = Bootstrap::getObjectManager()->create( + Collection::class, + [ + 'searchRequestName' => 'quick_search_container' + ] + ); + $collection->addSearchFilter($text); + foreach ($collection as $product) { + $products[] = $product; + } + return $products; + } + + /** + * Return product by SKU + * + * @param string $sku + * @return Product + */ + protected function getProductBySku($sku) + { + /** @var Product $product */ + $product = Bootstrap::getObjectManager()->get( + \Magento\Catalog\Model\Product::class + ); + return $product->loadByAttribute('sku', $sku); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php index 67a6e41697349..7f50638f469de 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php @@ -331,6 +331,10 @@ public function testBoolFilterWithNestedRangeInNegativeBoolFilter() * * @magentoConfigFixture current_store catalog/search/engine mysql * @dataProvider advancedSearchDataProvider + * @param string $nameQuery + * @param string $descriptionQuery + * @param array $rangeFilter + * @param int $expectedRecordsCount */ public function testSimpleAdvancedSearch( $nameQuery, @@ -445,6 +449,37 @@ public function testAdvancedSearchCompositeProductWithOutOfStockOption() $this->assertEquals(1, $queryResponse->count()); } + /** + * @magentoDataFixture Magento/Framework/Search/_files/product_configurable_with_disabled_child.php + * @magentoConfigFixture current_store catalog/search/engine mysql + */ + public function testAdvancedSearchCompositeProductWithDisabledChild() + { + /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ + $attribute = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) + ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable'); + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $selectOptions */ + $selectOptions = $this->objectManager + ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class) + ->setAttributeFilter($attribute->getId()); + + $firstOption = $selectOptions->getFirstItem(); + $firstOptionId = $firstOption->getId(); + $this->requestBuilder->bind('test_configurable', $firstOptionId); + $this->requestBuilder->setRequestName('filter_out_of_stock_child'); + + $queryResponse = $this->executeQuery(); + $this->assertEquals(0, $queryResponse->count()); + + $secondOption = $selectOptions->getLastItem(); + $secondOptionId = $secondOption->getId(); + $this->requestBuilder->bind('test_configurable', $secondOptionId); + $this->requestBuilder->setRequestName('filter_out_of_stock_child'); + + $queryResponse = $this->executeQuery(); + $this->assertEquals(0, $queryResponse->count()); + } + public function dateDataProvider() { return [ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php index 590f3c8041c6d..37b8731fc1d4c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php @@ -15,7 +15,7 @@ use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\TestFramework\Helper\Bootstrap; -\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); +Bootstrap::getInstance()->reinitialize(); require __DIR__ . '/configurable_attribute.php'; @@ -33,7 +33,7 @@ $attributeValues = []; $attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; -$productIds = [10, 20]; +$productIds = [1010, 1020]; array_shift($options); //remove the first option which is empty $isFirstOption = true; @@ -108,29 +108,8 @@ $product->setExtensionAttributes($extensionConfigurableAttributes); -// Remove any previously created product with the same id. -/** @var \Magento\Framework\Registry $registry */ -$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); -try { - $productToDelete = $productRepository->getById(1); - $productRepository->delete($productToDelete); - - /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ - $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); - $itemResource->getConnection()->delete( - $itemResource->getMainTable(), - 'product_id = ' . $productToDelete->getId() - ); -} catch (\Exception $e) { - // Nothing to remove -} -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); - $product->setTypeId(Configurable::TYPE_CODE) - ->setId(1) + ->setId(1001) ->setAttributeSetId($attributeSetId) ->setWebsiteIds([1]) ->setName('Configurable Product') @@ -140,8 +119,3 @@ ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); $productRepository->save($product); -// -///** @var \Magento\Catalog\Model\Indexer\Product\Eav\Processor $eavIndexer */ -//$eavIndexer = Bootstrap::getObjectManager() -// ->get(\Magento\Catalog\Model\Indexer\Product\Eav\Processor::class); -//$eavIndexer->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php index 8ba4e3abe21cc..cc585d177aedb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php @@ -16,7 +16,7 @@ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { +foreach (['simple_1010', 'simple_1020', 'configurable'] as $sku) { try { $product = $productRepository->get($sku, false, null, true); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child.php new file mode 100644 index 0000000000000..39c9782ba712a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child.php @@ -0,0 +1,18 @@ +create(ProductRepositoryInterface::class); + +$product = $productRepository->get('simple_1020'); +$product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child_rollback.php new file mode 100644 index 0000000000000..58d1dcd79acde --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_with_disabled_child_rollback.php @@ -0,0 +1,7 @@ + ['type' => Table::TYPE_TEXT, 'size' => 255], @@ -71,11 +75,13 @@ class Base implements ActionInterface /** * @var array + * @deprecated */ protected $filterColumns; /** * @var array + * @deprecated */ protected $searchColumns; @@ -96,6 +102,7 @@ class Base implements ActionInterface /** * @var String + * @deprecated */ protected $string; @@ -106,11 +113,13 @@ class Base implements ActionInterface /** * @var array + * @deprecated */ protected $filterable = []; /** * @var array + * @deprecated */ protected $searchable = []; @@ -272,6 +281,7 @@ protected function getPrimaryFieldset() protected function createResultCollection() { $select = $this->getPrimaryResource()->getSelect(); + $select->reset(\Magento\Framework\DB\Select::COLUMNS); $select->columns($this->getPrimaryResource()->getIdFieldName()); foreach ($this->data['fieldsets'] as $fieldset) { if (isset($fieldset['references'])) { @@ -342,6 +352,8 @@ protected function prepareFields() * * @param array $field * @return void + * + * @deprecated */ protected function saveFieldByType($field) { diff --git a/lib/internal/Magento/Framework/Indexer/Action/Entity.php b/lib/internal/Magento/Framework/Indexer/Action/Entity.php index e88d281fd4b81..b8390b9d5a002 100644 --- a/lib/internal/Magento/Framework/Indexer/Action/Entity.php +++ b/lib/internal/Magento/Framework/Indexer/Action/Entity.php @@ -24,6 +24,6 @@ protected function prepareDataSource(array $ids = []) { return !count($ids) ? $this->createResultCollection() - : $this->createResultCollection()->addFieldToFilter($this->getPrimaryResource()->getRowIdFieldName(), $ids); + : $this->createResultCollection()->addFieldToFilter($this->getPrimaryResource()->getIdFieldName(), $ids); } } diff --git a/lib/internal/Magento/Framework/Indexer/SaveHandler/Batch.php b/lib/internal/Magento/Framework/Indexer/SaveHandler/Batch.php index 31883fa8516ea..7d53e558287bd 100644 --- a/lib/internal/Magento/Framework/Indexer/SaveHandler/Batch.php +++ b/lib/internal/Magento/Framework/Indexer/SaveHandler/Batch.php @@ -10,27 +10,23 @@ class Batch /** * @param \Traversable $documents * @param int $size - * @return array + * @return \Generator */ public function getItems(\Traversable $documents, $size) { - if (count($documents) == 0) { - return []; - } - $i = 0; - $batch = $items = []; + $batch = []; + foreach ($documents as $documentName => $documentValue) { $batch[$documentName] = $documentValue; - if (++$i >= $size) { - $items[] = $batch; + if (++$i == $size) { + yield $batch; $i = 0; $batch = []; } } if (count($batch) > 0) { - $items[] = $batch; + yield $batch; } - return $items; } } diff --git a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php index eaab236bbca5b..8bcb4acaa11a3 100644 --- a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php +++ b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php @@ -52,6 +52,7 @@ public function resolve($index, array $dimensions) $tableNameParts[] = $dimension->getName() . $dimension->getValue(); } } + return $this->resource->getTableName(implode('_', $tableNameParts)); } @@ -63,10 +64,12 @@ public function resolve($index, array $dimensions) */ private function getScopeId($dimension) { - if (is_numeric($dimension->getValue())) { - return $dimension->getValue(); - } else { - return $this->scopeResolver->getScope($dimension->getValue())->getId(); + $scopeId = $dimension->getValue(); + + if (!is_numeric($scopeId)) { + $scopeId = $this->scopeResolver->getScope($scopeId)->getId(); } + + return $scopeId; } } diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/BatchTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/BatchTest.php index 0df98125f9a08..1571bba0a7d23 100644 --- a/lib/internal/Magento/Framework/Indexer/Test/Unit/BatchTest.php +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/BatchTest.php @@ -27,7 +27,8 @@ protected function setUp() public function testGetItems(array $itemsData, $size, array $expected) { $items = new \ArrayObject($itemsData); - $this->assertSame($expected, $this->object->getItems($items, $size)); + $data = $this->object->getItems($items, $size); + $this->assertSame($expected, iterator_to_array($data)); } /**