diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 3919722..867d823 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -24,10 +24,14 @@ use Codappix\SearchCore\Connection\FacetInterface; use Codappix\SearchCore\Connection\ResultItemInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Domain\Model\QueryResultInterfaceStub; +use Codappix\SearchCore\Domain\Model\ResultItem; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; class SearchResult implements SearchResultInterface { + use QueryResultInterfaceStub; + /** * @var SearchRequestInterface */ @@ -104,7 +108,7 @@ class SearchResult implements SearchResultInterface } foreach ($this->result->getResults() as $result) { - $this->results[] = new ResultItem($result); + $this->results[] = new ResultItem($result->getData()); } } @@ -153,41 +157,8 @@ class SearchResult implements SearchResultInterface $this->position = 0; } - // Extbase QueryResultInterface - Implemented to support Pagination of Fluid. - public function getQuery() { return $this->searchRequest; } - - public function getFirst() - { - throw new \BadMethodCallException('Method is not implemented yet.', 1502195121); - } - - public function toArray() - { - throw new \BadMethodCallException('Method is not implemented yet.', 1502195135); - } - - public function offsetExists($offset) - { - // Return false to allow Fluid to use appropriate getter methods. - return false; - } - - public function offsetGet($offset) - { - throw new \BadMethodCallException('Use getter to fetch properties.', 1502196933); - } - - public function offsetSet($offset, $value) - { - throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196934); - } - - public function offsetUnset($offset) - { - throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196936); - } } diff --git a/Classes/Connection/ResultItemInterface.php b/Classes/Connection/ResultItemInterface.php index 56add2a..d7fe1a9 100644 --- a/Classes/Connection/ResultItemInterface.php +++ b/Classes/Connection/ResultItemInterface.php @@ -25,5 +25,12 @@ namespace Codappix\SearchCore\Connection; */ interface ResultItemInterface extends \ArrayAccess { - + /** + * Returns every information as array. + * + * Provide key/column/field => data. + * + * Used e.g. for dataprocessing. + */ + public function getPlainData() : array; } diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index 7c7956e..e24adc3 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -20,6 +20,7 @@ namespace Codappix\SearchCore\Connection; * 02110-1301, USA. */ +use Codappix\SearchCore\Domain\Search\SearchService; use TYPO3\CMS\Extbase\Persistence\QueryInterface; interface SearchRequestInterface extends QueryInterface @@ -40,4 +41,10 @@ interface SearchRequestInterface extends QueryInterface * @return array */ public function getFilter(); + + /** + * Workaround for paginate widget support which will + * use the request to build another search. + */ + public function setSearchService(SearchService $searchService); } diff --git a/Classes/Domain/Model/QueryResultInterfaceStub.php b/Classes/Domain/Model/QueryResultInterfaceStub.php new file mode 100644 index 0000000..960fd40 --- /dev/null +++ b/Classes/Domain/Model/QueryResultInterfaceStub.php @@ -0,0 +1,61 @@ + + * + * 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. + */ + +/** + * As we have to stay compatible with QueryResultInterface + * of extbase but can and need not to provide all methods, + * this stub will provde the non implemented methods to + * keep real implementations clean. + */ +trait QueryResultInterfaceStub +{ + public function getFirst() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195121); + } + + public function toArray() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195135); + } + + public function offsetExists($offset) + { + // Return false to allow Fluid to use appropriate getter methods. + return false; + } + + public function offsetGet($offset) + { + throw new \BadMethodCallException('Use getter to fetch properties.', 1502196933); + } + + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196934); + } + + public function offsetUnset($offset) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196936); + } +} diff --git a/Classes/Connection/Elasticsearch/ResultItem.php b/Classes/Domain/Model/ResultItem.php similarity index 88% rename from Classes/Connection/Elasticsearch/ResultItem.php rename to Classes/Domain/Model/ResultItem.php index e56783c..f63b408 100644 --- a/Classes/Connection/Elasticsearch/ResultItem.php +++ b/Classes/Domain/Model/ResultItem.php @@ -1,5 +1,5 @@ @@ -29,9 +29,14 @@ class ResultItem implements ResultItemInterface */ protected $data = []; - public function __construct(\Elastica\Result $result) + public function __construct(array $result) { - $this->data = $result->getData(); + $this->data = $data; + } + + public function getPlainData() : array + { + return $this->data; } public function offsetExists($offset) diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index d751ce8..d835e7b 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -23,6 +23,7 @@ namespace Codappix\SearchCore\Domain\Model; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\FacetRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; +use Codappix\SearchCore\Domain\Search\SearchService; /** * Represents a search request used to process an actual search. @@ -63,6 +64,11 @@ class SearchRequest implements SearchRequestInterface */ protected $connection = null; + /** + * @var SearchService + */ + protected $searchService = null; + /** * @param string $query */ @@ -143,12 +149,17 @@ class SearchRequest implements SearchRequestInterface $this->connection = $connection; } + public function setSearchService(SearchService $searchService) + { + $this->searchService = $searchService; + } + // Extbase QueryInterface // Current implementation covers only paginate widget support. public function execute($returnRawQueryResult = false) { if ($this->connection instanceof ConnectionInterface) { - return $this->connection->search($this); + return $this->searchService->processResult($this->connection->search($this)); } throw new \InvalidArgumentException( diff --git a/Classes/Domain/Model/SearchResult.php b/Classes/Domain/Model/SearchResult.php new file mode 100644 index 0000000..d91820d --- /dev/null +++ b/Classes/Domain/Model/SearchResult.php @@ -0,0 +1,129 @@ + + * + * 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 Codappix\SearchCore\Connection\ResultItemInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Domain\Model\QueryResultInterfaceStub; + +/** + * Generic model for mapping a concrete search result from a connection. + */ +class SearchResult implements SearchResultInterface +{ + use QueryResultInterfaceStub; + + /** + * @var SearchResultInterface + */ + protected $originalSearchResult; + + /** + * @var array + */ + protected $resultItems = []; + + /** + * @var array + */ + protected $results = []; + + /** + * For Iterator interface. + * + * @var int + */ + protected $position = 0; + + public function __construct(SearchResultInterface $originalSearchResult, array $resultItems) + { + $this->originalSearchResult = $originalSearchResult; + $this->resultItems = $resultItems; + } + + /** + * @return array + */ + public function getResults() + { + $this->initResults(); + + return $this->results; + } + + protected function initResults() + { + if ($this->results !== []) { + return; + } + + foreach ($this->resultItems as $item) { + $this->results[] = new ResultItem($item); + } + } + + public function getFacets() + { + return $this->originalSearchResult->getFacets(); + } + + public function getCurrentCount() + { + return $this->originalSearchResult->getCurrentCount(); + } + + public function count() + { + return $this->originalSearchResult->count(); + } + + public function current() + { + return $this->getResults()[$this->position]; + } + + public function next() + { + ++$this->position; + + return $this->current(); + } + + public function key() + { + return $this->position; + } + + public function valid() + { + return isset($this->getResults()[$this->position]); + } + + public function rewind() + { + $this->position = 0; + } + + public function getQuery() + { + return $this->originalSearchResult->getQuery(); + } +} diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index 120ab3b..48be32c 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -25,7 +25,9 @@ use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Model\FacetRequest; +use Codappix\SearchCore\Domain\Model\SearchResult; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; @@ -49,19 +51,27 @@ class SearchService */ protected $objectManager; + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + /** * @param ConnectionInterface $connection * @param ConfigurationContainerInterface $configuration * @param ObjectManagerInterface $objectManager + * @param DataProcessorService $dataProcessorService */ public function __construct( ConnectionInterface $connection, ConfigurationContainerInterface $configuration, - ObjectManagerInterface $objectManager + ObjectManagerInterface $objectManager, + DataProcessorService $dataProcessorService ) { $this->connection = $connection; $this->configuration = $configuration; $this->objectManager = $objectManager; + $this->dataProcessorService = $dataProcessorService; } /** @@ -70,12 +80,15 @@ class SearchService */ public function search(SearchRequestInterface $searchRequest) { - $searchRequest->setConnection($this->connection); $this->addSize($searchRequest); $this->addConfiguredFacets($searchRequest); $this->addConfiguredFilters($searchRequest); - return $this->connection->search($searchRequest); + // Add connection to request to enable paginate widget support + $searchRequest->setConnection($this->connection); + $searchRequest->setSearchService($this); + + return $this->processResult($this->connection->search($searchRequest)); } /** @@ -138,4 +151,31 @@ class SearchService // Nothing todo, no filter configured. } } + + /** + * Processes the result, e.g. applies configured data processing to result. + */ + public function processResult(SearchResultInterface $searchResult) : SearchResultInterface + { + try { + $newSearchResultItems = []; + foreach ($this->configuration->get('searching.dataProcessing') as $configuration) { + foreach ($searchResult as $resultItem) { + $newSearchResultItems[] = $this->dataProcessorService->executeDataProcessor( + $configuration, + $resultItem->getPlainData() + ); + } + } + + return $this->objectManager->get( + SearchResult::class, + $searchResult, + $newSearchResultItems + ); + } + catch (InvalidArgumentException $e) { + return $searchResult; + } + } } diff --git a/Tests/Unit/Domain/Index/AbstractIndexerTest.php b/Tests/Unit/Domain/Index/AbstractIndexerTest.php index 3bbc97d..411cc24 100644 --- a/Tests/Unit/Domain/Index/AbstractIndexerTest.php +++ b/Tests/Unit/Domain/Index/AbstractIndexerTest.php @@ -24,6 +24,7 @@ use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\DataProcessing\CopyToProcessor; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Index\AbstractIndexer; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; @@ -44,17 +45,26 @@ class AbstractIndexerTest extends AbstractUnitTestCase */ protected $connection; + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + public function setUp() { parent::setUp(); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock(); + $this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class) + ->disableOriginalConstructor() + ->getMock(); $this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [ $this->connection, $this->configuration ]); + $this->inject($this->subject, 'dataProcessorService', $this->dataProcessorService); $this->subject->injectLogger($this->getMockedLogger()); $this->subject->setIdentifier('testTable'); $this->subject->expects($this->any()) @@ -73,7 +83,30 @@ class AbstractIndexerTest extends AbstractUnitTestCase $expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test'; $expectedRecord['search_abstract'] = ''; - $this->configuration->expects($this->exactly(2)) + $this->dataProcessorService->expects($this->any()) + ->method('executeDataProcessor') + ->withConsecutive( + [ + [ + '_typoScriptNodeValue' => CopyToProcessor::class, + 'to' => 'new_test_field', + ], + $record, + ], + [ + [ + '_typoScriptNodeValue' => CopyToProcessor::class, + 'to' => 'new_test_field2', + ], + array_merge($record, ['new_test_field' => 'test']), + ] + ) + ->will($this->onConsecutiveCalls( + array_merge($record, ['new_test_field' => 'test']), + $expectedRecord + )); + + $this->configuration->expects($this->any()) ->method('get') ->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields']) ->will($this->onConsecutiveCalls([ diff --git a/Tests/Unit/Domain/Search/SearchServiceTest.php b/Tests/Unit/Domain/Search/SearchServiceTest.php index c63e29e..aaf2e5c 100644 --- a/Tests/Unit/Domain/Search/SearchServiceTest.php +++ b/Tests/Unit/Domain/Search/SearchServiceTest.php @@ -23,6 +23,8 @@ namespace Copyright\SearchCore\Tests\Unit\Domain\Search; use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Model\SearchRequest; use Codappix\SearchCore\Domain\Search\SearchService; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; @@ -35,24 +37,59 @@ class SearchServiceTest extends AbstractUnitTestCase */ protected $subject; + /** + * @var SearchResultInterface + */ + protected $result; + + /** + * @var ConnectionInterface + */ + protected $connection; + + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + public function setUp() { parent::setUp(); + $this->result = $this->getMockBuilder(SearchResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->connection->expects($this->any()) + ->method('search') + ->willReturn($this->result); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class) ->disableOriginalConstructor() ->getMock(); $this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class) + ->setConstructorArgs([$this->objectManager]) + ->getMock(); $this->subject = new SearchService( $this->connection, $this->configuration, - $this->objectManager + $this->objectManager, + $this->dataProcessorService ); } @@ -61,13 +98,12 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function sizeIsAddedFromConfiguration() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(45, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') @@ -84,13 +120,12 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function defaultSizeIsAddedIfNothingIsConfigured() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') @@ -107,14 +142,16 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function configuredFilterAreAddedToRequestWithoutAnyFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['property' => 'something']); + ->will($this->onConsecutiveCalls( + ['property' => 'something'], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search') @@ -131,14 +168,16 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function configuredFilterAreAddedToRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['property' => 'something']); + ->will($this->onConsecutiveCalls( + ['property' => 'something'], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search') @@ -159,13 +198,12 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function nonConfiguredFilterIsNotChangingRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) @@ -184,14 +222,16 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function emptyConfiguredFilterIsNotChangingRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['anotherProperty' => '']); + ->will($this->onConsecutiveCalls( + ['anotherProperty' => ''], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search')