From 432335c80dce545dea133fe057b4ab0b12adeb65 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Tue, 4 Jul 2017 12:12:36 +0200 Subject: [PATCH] FEATURE: Allow custom indexer Adjust code to use defined indexer as FQCN, to allow any class to be used as indexer. Also classes defined by user. --- Classes/Command/IndexCommandController.php | 24 ++-- Classes/Domain/Index/AbstractIndexer.php | 113 ++++++++++++++++++ Classes/Domain/Index/IndexerFactory.php | 62 ++++++++-- .../Index/NoMatchingIndexerException.php | 25 ++++ Classes/Domain/Index/TcaIndexer.php | 70 ++--------- Classes/Domain/Service/DataHandler.php | 60 ++++++---- Classes/Hook/DataHandler.php | 12 +- .../DataHandler/AbstractDataHandlerTest.php | 6 +- .../Command/IndexCommandControllerTest.php | 37 ++---- 9 files changed, 255 insertions(+), 154 deletions(-) create mode 100644 Classes/Domain/Index/AbstractIndexer.php create mode 100644 Classes/Domain/Index/NoMatchingIndexerException.php diff --git a/Classes/Command/IndexCommandController.php b/Classes/Command/IndexCommandController.php index aef0863..227918f 100644 --- a/Classes/Command/IndexCommandController.php +++ b/Classes/Command/IndexCommandController.php @@ -21,6 +21,7 @@ namespace Leonmrni\SearchCore\Command; */ use Leonmrni\SearchCore\Domain\Index\IndexerFactory; +use Leonmrni\SearchCore\Domain\Index\NoMatchingIndexerException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; @@ -34,12 +35,6 @@ class IndexCommandController extends CommandController */ protected $indexerFactory; - /** - * @var \Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface - * @inject - */ - protected $configuration; - /** * @param IndexerFactory $factory */ @@ -49,19 +44,18 @@ class IndexCommandController extends CommandController } /** - * Will index the given table or everything. + * Will index the given identifier. * - * @param string $table + * @param string $identifier */ - public function indexCommand($table) + public function indexCommand($identifier) { - // TODO: Allow to index multiple tables at once? // TODO: Also allow to index everything? - if ($this->configuration->getIfExists('indexing.' . $table) === null) { - $this->outputLine('Table is not allowed for indexing.'); - $this->quit(1); + try { + $this->indexerFactory->getIndexer($identifier)->indexAllDocuments(); + $this->outputLine($identifier . ' was indexed.'); + } catch (NoMatchingIndexerException $e) { + $this->outputLine('No indexer found for: ' . $identifier); } - $this->indexerFactory->getIndexer($table)->indexAllDocuments(); - $this->outputLine('Table was indexed.'); } } diff --git a/Classes/Domain/Index/AbstractIndexer.php b/Classes/Domain/Index/AbstractIndexer.php new file mode 100644 index 0000000..acd046e --- /dev/null +++ b/Classes/Domain/Index/AbstractIndexer.php @@ -0,0 +1,113 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Connection\ConnectionInterface; + +abstract class AbstractIndexer implements IndexerInterface +{ + /** + * @var ConnectionInterface + */ + protected $connection; + + /** + * @var \TYPO3\CMS\Core\Log\Logger + */ + protected $logger; + + /** + * Inject log manager to get concrete logger from it. + * + * @param \TYPO3\CMS\Core\Log\LogManager $logManager + */ + public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager) + { + $this->logger = $logManager->getLogger(__CLASS__); + } + + /** + * @param ConnectionInterface $connection + */ + public function __construct(ConnectionInterface $connection) + { + $this->connection = $connection; + } + + public function indexAllDocuments() + { + $this->logger->info('Start indexing'); + foreach ($this->getRecordGenerator() as $records) { + $this->logger->debug('Index records.', [$records]); + if ($records === null) { + break; + } + + $this->connection->addDocuments($this->getDocumentName(), $records); + } + $this->logger->info('Finish indexing'); + } + + public function indexDocument($identifier) + { + $this->logger->info('Start indexing single record.', [$identifier]); + try { + $this->connection->addDocument($this->getDocumentName(), $this->getRecord($identifier)); + } catch (NoRecordFoundException $e) { + $this->logger->info('Could not index document.', [$e->getMessage()]); + } + $this->logger->info('Finish indexing'); + } + + /** + * @return \Generator + */ + protected function getRecordGenerator() + { + $offset = 0; + // TODO: Make configurable. + $limit = 50; + + while (($records = $this->getRecords($offset, $limit)) !== []) { + yield $records; + $offset += $limit; + } + } + + /** + * @param int $offset + * @param int $limit + * @return array|null + */ + abstract protected function getRecords($offset, $limit); + + /** + * @param int $identifier + * @return array + * @throws NoRecordFoundException If record could not be found. + */ + abstract protected function getRecord($identifier); + + /** + * @return string + */ + abstract protected function getDocumentName(); +} diff --git a/Classes/Domain/Index/IndexerFactory.php b/Classes/Domain/Index/IndexerFactory.php index 4ecbfb9..0140122 100644 --- a/Classes/Domain/Index/IndexerFactory.php +++ b/Classes/Domain/Index/IndexerFactory.php @@ -20,6 +20,11 @@ namespace Leonmrni\SearchCore\Domain\Index; * 02110-1301, USA. */ +use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Configuration\InvalidArgumentException; +use Leonmrni\SearchCore\Domain\Index\IndexerInterface; +use Leonmrni\SearchCore\Domain\Index\TcaIndexer; +use Leonmrni\SearchCore\Domain\Index\TcaIndexer\TcaTableService; use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; @@ -33,28 +38,61 @@ class IndexerFactory implements Singleton */ protected $objectManager; + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + /** * @param ObjectManagerInterface $objectManager */ - public function __construct(ObjectManagerInterface $objectManager) - { + public function __construct( + ObjectManagerInterface $objectManager, + ConfigurationContainerInterface $configuration + ) { $this->objectManager = $objectManager; + $this->configuration = $configuration; } /** - * @param string $tableName + * @param string $identifier * * @return IndexerInterface + * @throws NoMatchingIndexer */ - public function getIndexer($tableName) + public function getIndexer($identifier) { - // This is the place to use configuration to return different indexer. - return $this->objectManager->get( - TcaIndexer::Class, - $this->objectManager->get( - TcaIndexer\TcaTableService::class, - $tableName - ) - ); + try { + return $this->buildIndexer($this->configuration->get('indexing.' . $identifier . '.indexer'), $identifier); + } catch (NoMatchingIndexerException $e) { + // Nothing to do, we throw exception below + } catch (InvalidArgumentException $e) { + // Nothing to do, we throw exception below + } + + throw new NoMatchingIndexerException('Could not find an indexer for ' . $identifier, 1497341442); + } + + /** + * @param string $indexerClass + * @param string $identifier + * + * @return IndexerInterface + * @throws NoMatchingIndexer + */ + protected function buildIndexer($indexerClass, $identifier) + { + if ($indexerClass === TcaIndexer::class) { + return $this->objectManager->get( + TcaIndexer::class, + $this->objectManager->get(TcaTableService::class, $identifier) + ); + } + + if (class_exists($indexerClass) && in_array(IndexerInterface::class, class_implements($indexerClass))) { + return $this->objectManager->get($indexerClass); + } + + throw new NoMatchingIndexerException('Could not find indexer: ' . $indexerClass, 1497341442); } } diff --git a/Classes/Domain/Index/NoMatchingIndexerException.php b/Classes/Domain/Index/NoMatchingIndexerException.php new file mode 100644 index 0000000..02904cc --- /dev/null +++ b/Classes/Domain/Index/NoMatchingIndexerException.php @@ -0,0 +1,25 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +class NoMatchingIndexerException extends IndexingException +{ +} diff --git a/Classes/Domain/Index/TcaIndexer.php b/Classes/Domain/Index/TcaIndexer.php index b50c6fa..52f1efa 100644 --- a/Classes/Domain/Index/TcaIndexer.php +++ b/Classes/Domain/Index/TcaIndexer.php @@ -26,33 +26,13 @@ use Leonmrni\SearchCore\Connection\ConnectionInterface; /** * Will index the given table using configuration from TCA. */ -class TcaIndexer implements IndexerInterface +class TcaIndexer extends AbstractIndexer { - /** - * @var ConnectionInterface - */ - protected $connection; - /** * @var TcaIndexer\TcaTableService */ protected $tcaTableService; - /** - * @var \TYPO3\CMS\Core\Log\Logger - */ - protected $logger; - - /** - * Inject log manager to get concrete logger from it. - * - * @param \TYPO3\CMS\Core\Log\LogManager $logManager - */ - public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager) - { - $this->logger = $logManager->getLogger(__CLASS__); - } - /** * @param TcaIndexer\TcaTableService $tcaTableService * @param ConnectionInterface $connection @@ -65,46 +45,6 @@ class TcaIndexer implements IndexerInterface $this->connection = $connection; } - public function indexAllDocuments() - { - $this->logger->info('Start indexing'); - foreach ($this->getRecordGenerator() as $records) { - $this->logger->debug('Index records.', [$records]); - if ($records === null) { - break; - } - - $this->connection->addDocuments($this->tcaTableService->getTableName(), $records); - } - $this->logger->info('Finish indexing'); - } - - public function indexDocument($identifier) - { - $this->logger->info('Start indexing single record.', [$identifier]); - try { - $this->connection->addDocument($this->tcaTableService->getTableName(), $this->getRecord($identifier)); - } catch (NoRecordFoundException $e) { - $this->logger->info('Could not index document.', [$e->getMessage()]); - } - $this->logger->info('Finish indexing'); - } - - /** - * @return \Generator - */ - protected function getRecordGenerator() - { - $offset = 0; - // TODO: Make configurable. - $limit = 50; - - while (($records = $this->getRecords($offset, $limit)) !== []) { - yield $records; - $offset += $limit; - } - } - /** * @param int $offset * @param int $limit @@ -156,4 +96,12 @@ class TcaIndexer implements IndexerInterface return $record; } + + /** + * @return string + */ + protected function getDocumentName() + { + return $this->tcaTableService->getTableName(); + } } diff --git a/Classes/Domain/Service/DataHandler.php b/Classes/Domain/Service/DataHandler.php index 578589a..bec9eb3 100644 --- a/Classes/Domain/Service/DataHandler.php +++ b/Classes/Domain/Service/DataHandler.php @@ -21,6 +21,8 @@ namespace Leonmrni\SearchCore\Domain\Service; */ use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Domain\Index\IndexerFactory; +use Leonmrni\SearchCore\Domain\Index\NoMatchingIndexerException; use Leonmrni\SearchCore\Domain\Index\TcaIndexer; use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -47,8 +49,7 @@ class DataHandler implements Singleton protected $connection; /** - * @var \Leonmrni\SearchCore\Domain\Index\IndexerFactory - * @inject + * @var IndexerFactory */ protected $indexerFactory; @@ -74,30 +75,12 @@ class DataHandler implements Singleton /** * @param ConfigurationContainerInterface $configuration + * @param IndexerFactory $indexerFactory */ - public function __construct(ConfigurationContainerInterface $configuration) + public function __construct(ConfigurationContainerInterface $configuration, IndexerFactory $indexerFactory) { $this->configuration = $configuration; - } - - /** - * Get all tables that are allowed for indexing. - * - * @return array - */ - public function getAllowedTablesForIndexing() - { - $tables = []; - foreach ($this->configuration->get('indexing') as $tableName => $indexConfiguration) { - if ($indexConfiguration['indexer'] === TcaIndexer::class) { - $tables[] = $tableName; - continue; - } - // TODO: Support custom indexer. - // Define "interface" / option which is used. - } - - return $tables; + $this->indexerFactory = $indexerFactory; } /** @@ -107,7 +90,7 @@ class DataHandler implements Singleton public function add($table, array $record) { $this->logger->debug('Record received for add.', [$table, $record]); - $this->indexerFactory->getIndexer($table)->indexDocument($record['uid']); + $this->getIndexer($table)->indexDocument($record['uid']); } /** @@ -116,7 +99,7 @@ class DataHandler implements Singleton public function update($table, array $record) { $this->logger->debug('Record received for update.', [$table, $record]); - $this->indexerFactory->getIndexer($table)->indexDocument($record['uid']); + $this->getIndexer($table)->indexDocument($record['uid']); } /** @@ -128,4 +111,31 @@ class DataHandler implements Singleton $this->logger->debug('Record received for delete.', [$table, $identifier]); $this->connection->deleteDocument($table, $identifier); } + + /** + * @param string $table + * @return IndexerInterface + * + * @throws NoMatchingIndexerException + */ + protected function getIndexer($table) + { + return $this->indexerFactory->getIndexer($table); + } + + /** + * @param string $table + * @return bool + */ + public function canHandle($table) + { + try { + $this->getIndexer($table); + return true; + } catch (NoMatchingIndexerException $e) { + return false; + } + + return false; + } } diff --git a/Classes/Hook/DataHandler.php b/Classes/Hook/DataHandler.php index 1bcb4a5..34cf3f3 100644 --- a/Classes/Hook/DataHandler.php +++ b/Classes/Hook/DataHandler.php @@ -21,6 +21,7 @@ namespace Leonmrni\SearchCore\Hook; */ use Leonmrni\SearchCore\Configuration\NoConfigurationException; +use Leonmrni\SearchCore\Domain\Index\NoMatchingIndexerException; use Leonmrni\SearchCore\Domain\Service\DataHandler as OwnDataHandler; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\DataHandling\DataHandler as CoreDataHandler; @@ -140,7 +141,7 @@ class DataHandler implements Singleton $this->logger->debug('Datahandler could not be setup.'); return false; } - if (! $this->shouldProcessTable($table)) { + if (! $this->dataHandler->canHandle($table)) { $this->logger->debug('Table is not allowed.', [$table]); return false; } @@ -148,15 +149,6 @@ class DataHandler implements Singleton return true; } - /** - * @param string $table - * @return bool - */ - protected function shouldProcessTable($table) - { - return in_array($table, $this->dataHandler->getAllowedTablesForIndexing()); - } - /** * Wrapper to allow unit testing. * diff --git a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php index a26021d..b3c384b 100644 --- a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php +++ b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php @@ -21,6 +21,7 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler; */ use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Domain\Index\IndexerFactory; use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService; use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook; use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase; @@ -42,7 +43,10 @@ abstract class AbstractDataHandlerTest extends AbstractFunctionalTestCase $objectManager = GeneralUtility::makeInstance(ObjectManager::class); $this->subject = $this->getMockBuilder(DataHandlerService::class) - ->setConstructorArgs([$objectManager->get(ConfigurationContainerInterface::class)]) + ->setConstructorArgs([ + $objectManager->get(ConfigurationContainerInterface::class), + $objectManager->get(IndexerFactory::class) + ]) ->setMethods(['add', 'update', 'delete']) ->getMock(); diff --git a/Tests/Unit/Command/IndexCommandControllerTest.php b/Tests/Unit/Command/IndexCommandControllerTest.php index c2559f9..3dff968 100644 --- a/Tests/Unit/Command/IndexCommandControllerTest.php +++ b/Tests/Unit/Command/IndexCommandControllerTest.php @@ -21,8 +21,8 @@ namespace Leonmrni\SearchCore\Tests\Unit\Command; */ use Leonmrni\SearchCore\Command\IndexCommandController; -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; use Leonmrni\SearchCore\Domain\Index\IndexerFactory; +use Leonmrni\SearchCore\Domain\Index\NoMatchingIndexerException; use Leonmrni\SearchCore\Domain\Index\TcaIndexer; use Leonmrni\SearchCore\Tests\Unit\AbstractUnitTestCase; use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; @@ -40,11 +40,6 @@ class IndexCommandControllerTest extends AbstractUnitTestCase */ protected $indexerFactory; - /** - * @var ConfigurationContainerInterface - */ - protected $configuration; - public function setUp() { parent::setUp(); @@ -52,16 +47,12 @@ class IndexCommandControllerTest extends AbstractUnitTestCase $this->indexerFactory = $this->getMockBuilder(IndexerFactory::class) ->disableOriginalConstructor() ->getMock(); - $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class) - ->disableOriginalConstructor() - ->getMock(); $this->subject = $this->getMockBuilder(IndexCommandController::class) ->disableOriginalConstructor() ->setMethods(['quit', 'outputLine']) ->getMock(); $this->subject->injectIndexerFactory($this->indexerFactory); - $this->inject($this->subject, 'configuration', $this->configuration); } /** @@ -69,22 +60,14 @@ class IndexCommandControllerTest extends AbstractUnitTestCase */ public function indexerStopsForNonAllowedTable() { - $this->expectException(StopActionException::class); - $this->subject->expects($this->once()) - ->method('quit') - ->with(1) - ->will($this->throwException(new StopActionException)); - $this->subject->expects($this->once()) ->method('outputLine') - ->with('Table is not allowed for indexing.'); - $this->indexerFactory->expects($this->never()) - ->method('getIndexer'); + ->with('No indexer found for: nonAllowedTable'); + $this->indexerFactory->expects($this->once()) + ->method('getIndexer') + ->with('nonAllowedTable') + ->will($this->throwException(new NoMatchingIndexerException)); - $this->configuration->expects($this->once()) - ->method('getIfExists') - ->with('indexing.nonAllowedTable') - ->will($this->returnValue(null)); $this->subject->indexCommand('nonAllowedTable'); } @@ -100,18 +83,12 @@ class IndexCommandControllerTest extends AbstractUnitTestCase ->method('quit'); $this->subject->expects($this->once()) ->method('outputLine') - ->with('Table was indexed.'); + ->with('allowedTable was indexed.'); $this->indexerFactory->expects($this->once()) ->method('getIndexer') ->with('allowedTable') ->will($this->returnValue($indexerMock)); - $this->configuration->expects($this->once()) - ->method('getIfExists') - ->with('indexing.allowedTable') - ->will($this->returnValue([ - 'indexer' => 'Leonmrni\SearchCore\Domain\Index\TcaIndexer', - ])); $this->subject->indexCommand('allowedTable'); } }