diff --git a/main/src/Console/Command/ClearCommand.php b/main/src/Console/Command/ClearCommand.php new file mode 100644 index 0000000..de20f4e --- /dev/null +++ b/main/src/Console/Command/ClearCommand.php @@ -0,0 +1,91 @@ +indexer = $indexer; + $this->appState = $appState; + } + + protected function configure() + { + $options = [ + new InputOption( + self::INPUT_STORES, + null, + InputOption::VALUE_OPTIONAL, + 'Clear solr product index for given stores (can be store id, store code, comma seperated. Or "all".) ' + . 'If not set, clear all stores.' + ), + new InputOption( + self::INPUT_USESWAPCORE, + null, + InputOption::VALUE_NONE, + 'Use swap core for clearing instead of live solr core (only if configured correctly).' + ), + ]; + $this->setName('solr:clear'); + $this->setHelp('Clear Solr index for given stores (see "stores" param).'); + $this->setDescription('Clear Solr index'); + $this->setDefinition($options); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $styledOutput = new StyledOutput( + $output, + class_exists(SymfonyStyle::class) ? new SymfonyStyle($input, $output) : null + ); + $startTime = microtime(true); + $this->appState->setAreaCode(App\Area::AREA_GLOBAL); + if (!$input->getOption(self::INPUT_STORES) || $input->getOption(self::INPUT_STORES) === 'all') { + $stores = null; + $styledOutput->title('Clearing full Solr product index...'); + } else { + $stores = \explode(',', $input->getOption(self::INPUT_STORES)); + $styledOutput->title('Clearing Solr product index for stores ' . \implode(', ', $stores) . '...'); + } + try { + $this->indexer->addProgressHandler( + new ProgressInConsole($output) + ); + if ($input->getOption(self::INPUT_USESWAPCORE)) { + $this->indexer->clearStoresOnSwappedCore($stores); + } else { + $this->indexer->clearStores($stores); + } + $totalTime = number_format(microtime(true) - $startTime, 2); + $styledOutput->success("Clearing finished in $totalTime seconds."); + } catch (\Exception $e) { + $styledOutput->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/Console/Command/ReindexCommand.php b/main/src/Console/Command/ReindexCommand.php index a7a5813..b3de0be 100644 --- a/main/src/Console/Command/ReindexCommand.php +++ b/main/src/Console/Command/ReindexCommand.php @@ -17,7 +17,10 @@ */ class ReindexCommand extends Command { - const INPUT_STORES = 'stores'; + const INPUT_STORES = 'stores'; + const INPUT_EMPTYINDEX = 'emptyindex'; + const INPUT_NOEMPTYINDEX = 'noemptyindex'; + const INPUT_PROGRESS = 'progress'; /** * @var Indexer\Console */ @@ -38,26 +41,26 @@ protected function configure() { $options = [ new InputOption( - 'stores', + self::INPUT_STORES, null, InputOption::VALUE_OPTIONAL, 'Reindex given stores (can be store id, store code, comma seperated. Or "all".) ' . 'If not set, reindex all stores.' ), new InputOption( - 'emptyindex', + self::INPUT_EMPTYINDEX, null, InputOption::VALUE_NONE, 'Force emptying the solr index for the given store(s). If not set, configured value is used.' ), new InputOption( - 'noemptyindex', + self::INPUT_NOEMPTYINDEX, null, InputOption::VALUE_NONE, 'Force not emptying the solr index for the given store(s). If not set, configured value is used.' ), new InputOption( - 'progress', + self::INPUT_PROGRESS, null, InputOption::VALUE_NONE, 'Show progress bar.' @@ -88,13 +91,13 @@ class_exists(SymfonyStyle::class) ? new SymfonyStyle($input, $output) : null $this->indexer->addProgressHandler( new ProgressInConsole( $output, - $input->getOption('progress') ? ProgressInConsole::USE_PROGRESS_BAR : false + $input->getOption(self::INPUT_PROGRESS) ? ProgressInConsole::USE_PROGRESS_BAR : false ) ); - if ($input->getOption('emptyindex')) { + if ($input->getOption(self::INPUT_EMPTYINDEX)) { $styledOutput->note('Forcing empty index.'); $this->indexer->executeStoresForceEmpty($stores); - } elseif ($input->getOption('noemptyindex')) { + } elseif ($input->getOption(self::INPUT_NOEMPTYINDEX)) { $styledOutput->note('Forcing non-empty index.'); $this->indexer->executeStoresForceNotEmpty($stores); } else { diff --git a/main/src/Console/Command/ReindexSliceCommand.php b/main/src/Console/Command/ReindexSliceCommand.php index c954aee..33cc799 100644 --- a/main/src/Console/Command/ReindexSliceCommand.php +++ b/main/src/Console/Command/ReindexSliceCommand.php @@ -17,7 +17,10 @@ */ class ReindexSliceCommand extends Command { - const INPUT_STORES = 'stores'; + const INPUT_STORES = 'stores'; + const INPUT_SLICE = 'slice'; + const INPUT_USESWAPCORE = 'useswapcore'; + const INPUT_PROGRESS = 'progress'; /** * @var Indexer\Console */ @@ -38,33 +41,36 @@ protected function configure() { $options = [ new InputOption( - 'stores', + self::INPUT_STORES, null, InputOption::VALUE_OPTIONAL, 'Reindex given stores (can be store id, store code, comma seperated. Or "all".) ' . 'If not set, reindex all stores.' ), new InputOption( - 'slice', + self::INPUT_SLICE, null, InputOption::VALUE_REQUIRED, '/, i.e. "1/5" or "2/5". ' ), new InputOption( - 'useswapcore', + self::INPUT_USESWAPCORE, null, InputOption::VALUE_NONE, 'Use swap core for indexing instead of live solr core (only if configured correctly).' ), new InputOption( - 'progress', + self::INPUT_PROGRESS, null, InputOption::VALUE_NONE, 'Show progress bar.' ) ]; $this->setName('solr:reindex:slice'); - $this->setHelp('Partially reindex Solr for given stores (see "stores" param). Can be used for letting indexing run in parallel.'); + $this->setHelp( + 'Partially reindex Solr for given stores (see "stores" param). ' + . 'Can be used for letting indexing run in parallel.' + ); $this->setDescription('Partially reindex Solr'); $this->setDefinition($options); } @@ -88,17 +94,20 @@ class_exists(SymfonyStyle::class) ? new SymfonyStyle($input, $output) : null $this->indexer->addProgressHandler( new ProgressInConsole( $output, - $input->getOption('progress') ? ProgressInConsole::USE_PROGRESS_BAR : false + $input->getOption(self::INPUT_PROGRESS) ? ProgressInConsole::USE_PROGRESS_BAR : false ) ); - $styledOutput->note('Processing slice ' . $input->getOption('slice') . '...'); - if ($input->getOption('useswapcore')) { + $styledOutput->note('Processing slice ' . $input->getOption(self::INPUT_SLICE) . '...'); + if ($input->getOption(self::INPUT_USESWAPCORE)) { $this->indexer->executeStoresSliceOnSwappedCore( - Slice::fromExpression($input->getOption('slice')), + Slice::fromExpression($input->getOption(self::INPUT_SLICE)), $stores ); } else { - $this->indexer->executeStoresSlice(Slice::fromExpression($input->getOption('slice')), $stores); + $this->indexer->executeStoresSlice( + Slice::fromExpression($input->getOption(self::INPUT_SLICE)), + $stores + ); } $totalTime = number_format(microtime(true) - $startTime, 2); $styledOutput->success("Reindex finished in $totalTime seconds."); diff --git a/main/src/Console/Command/SwapCommand.php b/main/src/Console/Command/SwapCommand.php new file mode 100644 index 0000000..c7ce148 --- /dev/null +++ b/main/src/Console/Command/SwapCommand.php @@ -0,0 +1,84 @@ +indexer = $indexer; + $this->appState = $appState; + } + + protected function configure() + { + $options = [ + new InputOption( + self::INPUT_STORES, + null, + InputOption::VALUE_OPTIONAL, + 'Swap cores for given stores (can be store id, store code, comma seperated. Or "all".) ' + . 'If not set, swap cores for all stores.' + ), + ]; + $this->setName('solr:swap'); + $this->setHelp( + 'Swap cores. This is useful if using slices (see solr:reindex:slice) ' + . 'after indexing with the "--use_swap_core" param; it\'s not needed otherwise.' + ); + $this->setDescription('Swap cores'); + $this->setDefinition($options); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $styledOutput = new StyledOutput( + $output, + class_exists(SymfonyStyle::class) ? new SymfonyStyle($input, $output) : null + ); + $startTime = microtime(true); + $this->appState->setAreaCode(App\Area::AREA_GLOBAL); + if (!$input->getOption(self::INPUT_STORES) || $input->getOption(self::INPUT_STORES) === 'all') { + $stores = null; + $styledOutput->title('Swap all cores...'); + } else { + $stores = \explode(',', $input->getOption(self::INPUT_STORES)); + $styledOutput->title('Swap cores for stores ' . \implode(', ', $stores) . '...'); + } + try { + $this->indexer->addProgressHandler( + new ProgressInConsole($output) + ); + $this->indexer->swapCores($stores); + $totalTime = number_format(microtime(true) - $startTime, 2); + $styledOutput->success("Core swap finished in $totalTime seconds."); + } catch (\Exception $e) { + $styledOutput->error($e->getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/Model/Indexer/Console.php b/main/src/Model/Indexer/Console.php index 416b4bf..abc1c3c 100644 --- a/main/src/Model/Indexer/Console.php +++ b/main/src/Model/Indexer/Console.php @@ -13,6 +13,7 @@ use IntegerNet\Solr\Indexer\ProductIndexer; use IntegerNet\Solr\Indexer\Progress\ProgressHandler; use IntegerNet\Solr\Indexer\Slice; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; class Console @@ -64,15 +65,42 @@ public function executeStoresSliceOnSwappedCore($slice, $storeIds) public function clearStores(array $storeIds = null) { - //TODO fetch all store ids if NULL if (empty($storeIds)) { - throw new \BadMethodCallException("Command for 'clear all stores' not implemented yet"); + $storeIds = array_map( + function (StoreInterface $store) { + return $store->getId(); + }, + $this->storeManager->getStores() + ); } foreach ($this->getStoreIds($storeIds) as $storeId) { $this->solrIndexer->clearIndex($storeId); } } + public function clearStoresOnSwappedCore(array $storeIds = null) + { + if (empty($storeIds)) { + $storeIds = array_map( + function (StoreInterface $store) { + return $store->getId(); + }, + $this->storeManager->getStores() + ); + } + $this->solrIndexer->activateSwapCore(); + foreach ($this->getStoreIds($storeIds) as $storeId) { + $this->solrIndexer->clearIndex($storeId); + } + $this->solrIndexer->deactivateSwapCore(); + } + + public function swapCores(array $storeIds = null) + { + $this->solrIndexer->checkSwapCoresConfiguration($storeIds); + $this->solrIndexer->swapCores($storeIds); + } + public function addProgressHandler(ProgressHandler $handler) { $this->solrIndexer->addProgressHandler($handler); @@ -122,7 +150,7 @@ function ($storeId) use ($storesByCode) { if (isset($storesByCode[$storeId])) { return $storesByCode[$storeId]->getId(); } - throw new \InvalidArgumentException("'$storeId' is neither a numric ID nor an existing store code"); + throw new \InvalidArgumentException("'$storeId' is neither a numeric ID nor an existing store code"); }, $storeIds ); diff --git a/main/src/etc/di.xml b/main/src/etc/di.xml index cd2d98a..a4a9f2d 100755 --- a/main/src/etc/di.xml +++ b/main/src/etc/di.xml @@ -155,6 +155,8 @@ IntegerNet\Solr\Console\Command\ReindexCommand IntegerNet\Solr\Console\Command\ReindexSliceCommand + IntegerNet\Solr\Console\Command\ClearCommand + IntegerNet\Solr\Console\Command\SwapCommand @@ -170,6 +172,17 @@ \Magento\Framework\App\State\Proxy + + + \IntegerNet\Solr\Model\Indexer\Console\Proxy + \Magento\Framework\App\State\Proxy + + + + \IntegerNet\Solr\Model\Indexer\Console\Proxy + \Magento\Framework\App\State\Proxy + + diff --git a/main/test/integration/Console/CommandListTest.php b/main/test/integration/Console/CommandListTest.php index 1373a07..afffc64 100644 --- a/main/test/integration/Console/CommandListTest.php +++ b/main/test/integration/Console/CommandListTest.php @@ -28,7 +28,7 @@ public function testContainsReindexProductsCommand() ); $reindexCommand = $commands['solr_reindex_full']; $this->assertEquals('solr:reindex:full', $reindexCommand->getName(), 'Command name'); - $this->assertInstanceof( + $this->assertInstanceOf( Command\ReindexCommand::class, $reindexCommand, 'Command should be instantiated.' @@ -45,13 +45,47 @@ public function testContainsReindexSliceCommand() ); $reindexCommand = $commands['solr_reindex_slice']; $this->assertEquals('solr:reindex:slice', $reindexCommand->getName(), 'Command name'); - $this->assertInstanceof( + $this->assertInstanceOf( Command\ReindexSliceCommand::class, $reindexCommand, 'Command should be instantiated.' ); } + public function testContainsClearCommand() + { + $commands = $this->commandList->getCommands(); + $this->assertArrayHasKey( + 'solr_clear', + $commands, + 'Command should be listed' + ); + $clearCommand = $commands['solr_clear']; + $this->assertEquals('solr:clear', $clearCommand->getName(), 'Command name'); + $this->assertInstanceOf( + Command\ClearCommand::class, + $clearCommand, + 'Command should be instantiated.' + ); + } + + public function testContainsSwapCommand() + { + $commands = $this->commandList->getCommands(); + $this->assertArrayHasKey( + 'solr_swap', + $commands, + 'Command should be listed' + ); + $clearCommand = $commands['solr_swap']; + $this->assertEquals('solr:swap', $clearCommand->getName(), 'Command name'); + $this->assertInstanceOf( + Command\SwapCommand::class, + $clearCommand, + 'Command should be instantiated.' + ); + } + protected function setUp() { $this->fixMagento2․2Di(); diff --git a/main/test/unit/Console/Command/ClearCommandTest.php b/main/test/unit/Console/Command/ClearCommandTest.php new file mode 100644 index 0000000..4cc90f4 --- /dev/null +++ b/main/test/unit/Console/Command/ClearCommandTest.php @@ -0,0 +1,100 @@ +indexer = $this->getMockBuilder(Indexer\Console::class)->disableOriginalConstructor()->getMock(); + $appState = $this->getMockBuilder(App\State::class)->disableOriginalConstructor()->getMock(); + $this->command = new ClearCommand($this->indexer, $appState); + $this->output = new BufferedOutput(); + } + + + public function testClearsFullProductIndexWithoutArguments() + { + $this->indexer->expects($this->once())->method('clearStores')->with(null); + $exitCode = $this->runCommandWithInput([]); + $this->assertEquals(0, $exitCode, 'Exit code should be 0 for successful clear'); + $this->assertOutputMessages('Clearing full Solr product index', 'Finished'); + } + + public function testClearProductIndexWithStoreFilter() + { + $storeIds = [1, 3, 'french']; + $this->indexer->expects($this->once())->method('clearStores')->with($storeIds); + $exitCode = $this->runCommandWithInput( + [ + '--stores' => \implode(',', $storeIds), + ] + ); + $this->assertEquals(0, $exitCode, 'Exit code should be 0 for successful clearing'); + $this->assertOutputMessages('Clearing Solr product index for stores 1, 3, french', 'Finished'); + } + + public function testClearProductIndexOnSwappedCore() + { + $storeIds = [1]; + + $this->indexer->expects($this->once())->method('clearStoresOnSwappedCore')->with($storeIds); + $exitCode = $this->runCommandWithInput( + [ + '--stores' => \implode(',', $storeIds), + '--useswapcore' => true, + ] + ); + $this->assertEquals(0, $exitCode, 'Exit code should be 0 for successful clearing'); + $this->assertOutputMessages( + 'Clearing Solr product index for stores 1', + 'Finished' + ); + } + + private function runCommandWithInput($input) + { + return $this->command->run(new ArrayInput($input), $this->output); + } + + /** + * Assert that output contains all given strings. + * + * Note that this is only for output directly emitted from the command, + * not via progress updates because the indexer is mocked. + * + * @param string[] $messages + */ + private function assertOutputMessages(...$messages) + { + $this->assertThat( + $this->output->fetch(), + $this->logicalAnd( + ...array_map([Assert::class, 'stringContains'], $messages) + ) + ); + } +} diff --git a/main/test/unit/Console/Command/ReindexCommandTest.php b/main/test/unit/Console/Command/ReindexCommandTest.php index 9e11709..4630a61 100644 --- a/main/test/unit/Console/Command/ReindexCommandTest.php +++ b/main/test/unit/Console/Command/ReindexCommandTest.php @@ -23,7 +23,7 @@ class ReindexCommandTest extends TestCase private $output; /** - * @var ProductIndexer|\PHPUnit_Framework_MockObject_MockObject + * @var Indexer\Console|\PHPUnit_Framework_MockObject_MockObject */ private $indexer; diff --git a/main/test/unit/Console/Command/ReindexSliceCommandTest.php b/main/test/unit/Console/Command/ReindexSliceCommandTest.php index 2bea2e5..eab6905 100644 --- a/main/test/unit/Console/Command/ReindexSliceCommandTest.php +++ b/main/test/unit/Console/Command/ReindexSliceCommandTest.php @@ -24,7 +24,7 @@ class ReindexSliceCommandTest extends TestCase private $output; /** - * @var ProductIndexer|\PHPUnit_Framework_MockObject_MockObject + * @var Indexer\Console|\PHPUnit_Framework_MockObject_MockObject */ private $indexer; diff --git a/main/test/unit/Console/Command/SwapCommandTest.php b/main/test/unit/Console/Command/SwapCommandTest.php new file mode 100644 index 0000000..d6e5c19 --- /dev/null +++ b/main/test/unit/Console/Command/SwapCommandTest.php @@ -0,0 +1,82 @@ +indexer = $this->getMockBuilder(Indexer\Console::class)->disableOriginalConstructor()->getMock(); + $appState = $this->getMockBuilder(App\State::class)->disableOriginalConstructor()->getMock(); + $this->command = new SwapCommand($this->indexer, $appState); + $this->output = new BufferedOutput(); + } + + + public function testSwapAllCoresWithoutArguments() + { + $this->indexer->expects($this->once())->method('swapCores')->with(null); + $exitCode = $this->runCommandWithInput([]); + $this->assertEquals(0, $exitCode, 'Exit code should be 0 for successful clear'); + $this->assertOutputMessages('Swap all cores', 'Finished'); + } + + public function testSwapCoresWithStoreFilter() + { + $storeIds = [1, 3, 'french']; + $this->indexer->expects($this->once())->method('swapCores')->with($storeIds); + $exitCode = $this->runCommandWithInput( + [ + '--stores' => \implode(',', $storeIds), + ] + ); + $this->assertEquals(0, $exitCode, 'Exit code should be 0 for successful clearing'); + $this->assertOutputMessages('Swap cores for stores 1, 3, french', 'Finished'); + } + + private function runCommandWithInput($input) + { + return $this->command->run(new ArrayInput($input), $this->output); + } + + /** + * Assert that output contains all given strings. + * + * Note that this is only for output directly emitted from the command, + * not via progress updates because the indexer is mocked. + * + * @param string[] $messages + */ + private function assertOutputMessages(...$messages) + { + $this->assertThat( + $this->output->fetch(), + $this->logicalAnd( + ...array_map([Assert::class, 'stringContains'], $messages) + ) + ); + } +}