TASK: Add data processing to search result

Search results are now processed through data processing by search
service. The result will be a SearchResult model from our domain. Also
SearchResult will execute new queries, e.g. from paginate widget,
through SearchService to apply data processing again.

Remove duplicate stub code to trait, to keep own logic and code clean.
This commit is contained in:
Daniel Siepmann 2018-03-06 09:04:47 +01:00
parent 2998c43ba8
commit 769fb8237b
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
10 changed files with 369 additions and 65 deletions

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -0,0 +1,61 @@
<?php
namespace Codappix\SearchCore\Domain\Model;
/*
* Copyright (C) 2018 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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);
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Codappix\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Domain\Model;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
@ -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)

View file

@ -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(

View file

@ -0,0 +1,129 @@
<?php
namespace Codappix\SearchCore\Domain\Model;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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<ResultItemInterface>
*/
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();
}
}

View file

@ -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;
}
}
}

View file

@ -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([

View file

@ -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')