Merge pull request #122 from Codappix/feature/116-execute-dataprocessor-on-result

FEATURE: 116 execute dataprocessor on result
This commit is contained in:
Daniel Siepmann 2018-03-06 13:47:35 +01:00 committed by GitHub
commit 951edf3871
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 909 additions and 184 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

@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing;
*/
class CopyToProcessor implements ProcessorInterface
{
public function processRecord(array $record, array $configuration) : array
public function processData(array $record, array $configuration) : array
{
$all = [];

View file

@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing;
*/
class GeoPointProcessor implements ProcessorInterface
{
public function processRecord(array $record, array $configuration) : array
public function processData(array $record, array $configuration) : array
{
if (! $this->canApply($record, $configuration)) {
return $record;

View file

@ -21,14 +21,13 @@ namespace Codappix\SearchCore\DataProcessing;
*/
/**
* All DataProcessing Processors should implement this interface, otherwise they
* will not be executed.
* All DataProcessing Processors should implement this interface.
*/
interface ProcessorInterface
{
/**
* Processes the given record.
* Processes the given data.
* Also retrieves the configuration for this processor instance.
*/
public function processRecord(array $record, array $configuration) : array;
public function processData(array $record, array $configuration) : array;
}

View file

@ -27,7 +27,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class RemoveProcessor implements ProcessorInterface
{
public function processRecord(array $record, array $configuration) : array
public function processData(array $record, array $configuration) : array
{
if (!isset($configuration['fields'])) {
return $record;

View file

@ -0,0 +1,56 @@
<?php
namespace Codappix\SearchCore\DataProcessing;
/*
* 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.
*/
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* Eases work with data processing.
*/
class Service
{
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
public function __construct(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* Executes the dataprocessor depending on configuration and returns the result.
*
* @param array|string $configuration Either the full configuration or only the class name.
*/
public function executeDataProcessor($configuration, array $data) : array
{
if (is_string($configuration)) {
$configuration = [
'_typoScriptNodeValue' => $configuration,
];
}
return $this->objectManager->get($configuration['_typoScriptNodeValue'])
->processData($data, $configuration);
}
}

View file

@ -43,17 +43,17 @@ abstract class AbstractIndexer implements IndexerInterface
*/
protected $identifier = '';
/**
* @var \Codappix\SearchCore\DataProcessing\Service
* @inject
*/
protected $dataProcessorService;
/**
* @var \TYPO3\CMS\Core\Log\Logger
*/
protected $logger;
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
* @inject
*/
protected $objectManager;
/**
* Inject log manager to get concrete logger from it.
*
@ -140,18 +140,7 @@ abstract class AbstractIndexer implements IndexerInterface
{
try {
foreach ($this->configuration->get('indexing.' . $this->identifier . '.dataProcessing') as $configuration) {
$className = '';
if (is_string($configuration)) {
$className = $configuration;
$configuration = [];
} else {
$className = $configuration['_typoScriptNodeValue'];
}
$dataProcessor = $this->objectManager->get($className);
if ($dataProcessor instanceof ProcessorInterface) {
$record = $dataProcessor->processRecord($record, $configuration);
}
$record = $this->dataProcessorService->executeDataProcessor($configuration, $record);
}
} catch (InvalidArgumentException $e) {
// Nothing to do.

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 = $result;
}
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,19 +149,30 @@ 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);
}
if (! ($this->connection instanceof ConnectionInterface)) {
throw new \InvalidArgumentException(
'Connection was not set before, therefore execute can not work. Use `setConnection` before.',
1502197732
);
}
if (! ($this->searchService instanceof SearchService)) {
throw new \InvalidArgumentException(
'SearchService was not set before, therefore execute can not work. Use `setSearchService` before.',
1520325175
);
}
return $this->searchService->processResult($this->connection->search($this));
}
public function setLimit($limit)
{

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));
}
/**
@ -133,4 +146,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

@ -36,5 +36,7 @@ DataProcessing
Before data is transfered to search service, it can be processed by "DataProcessors" like already
known by :ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing` of :ref:`t3tsref:cobj-fluidtemplate`.
The same is true for retrieved search results. They can be processed again by "DataProcessors" to
prepare data for display in Templates or further usage.
Configuration is done through TypoScript, see :ref:`dataProcessing`.

View file

@ -0,0 +1,35 @@
The following Processor are available:
.. toctree::
:maxdepth: 1
:glob:
/configuration/dataProcessing/CopyToProcessor
/configuration/dataProcessing/RemoveProcessor
/configuration/dataProcessing/GeoPointProcessor
The following Processor are planned:
``Codappix\SearchCore\DataProcessing\ReplaceProcessor``
Will execute a search and replace on configured fields.
``Codappix\SearchCore\DataProcessing\RootLevelProcessor``
Will attach the root level to the record.
``Codappix\SearchCore\DataProcessing\ChannelProcessor``
Will add a configurable channel to the record, e.g. if you have different areas in your
website like "products" and "infos".
``Codappix\SearchCore\DataProcessing\RelationResolverProcessor``
Resolves all relations using the TCA.
Of course you are able to provide further processors. Just implement
``Codappix\SearchCore\DataProcessing\ProcessorInterface`` and use the FQCN (=Fully qualified
class name) as done in the examples above.
By implementing also the same interface as necessary for TYPO3
:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`, you are able to reuse the same code
also for Fluid to prepare the same record fetched from DB for your fluid.
Dependency injection is possible inside of processors, as we instantiate through extbase
``ObjectManager``.

View file

@ -146,7 +146,7 @@ Example::
dataProcessing
--------------
Used by: All connections while indexing.
Used by: All connections while indexing, due to implementation inside ``AbstractIndexer``.
Configure modifications on each document before sending it to the configured connection. Same as
provided by TYPO3 for :ref:`t3tsref:cobj-fluidtemplate` through
@ -170,41 +170,7 @@ Example::
The above example will copy all existing fields to the field ``search_spellcheck``. Afterwards
all fields, including ``search_spellcheck`` will be copied to ``search_all``.
E.g. used to index all information into a field for :ref:`spellchecking` or searching with
different :ref:`mapping`.
The following Processor are available:
.. include:: /configuration/dataProcessing/availableAndPlanned.rst
.. toctree::
:maxdepth: 1
:glob:
dataProcessing/CopyToProcessor
dataProcessing/RemoveProcessor
dataProcessing/GeoPointProcessor
The following Processor are planned:
``Codappix\SearchCore\DataProcessing\ReplaceProcessor``
Will execute a search and replace on configured fields.
``Codappix\SearchCore\DataProcessing\RootLevelProcessor``
Will attach the root level to the record.
``Codappix\SearchCore\DataProcessing\ChannelProcessor``
Will add a configurable channel to the record, e.g. if you have different areas in your
website like "products" and "infos".
``Codappix\SearchCore\DataProcessing\RelationResolverProcessor``
Resolves all relations using the TCA.
Of course you are able to provide further processors. Just implement
``Codappix\SearchCore\DataProcessing\ProcessorInterface`` and use the FQCN (=Fully qualified
class name) as done in the examples above.
By implementing also the same interface as necessary for TYPO3
:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`, you are able to reuse the same code
also for Fluid to prepare the same record fetched from DB for your fluid.
Dependency injection is possible inside of processors, as we instantiate through extbase
``ObjectManager``.
Also data processors are available for search results too, see :ref:`searching_dataProcessing`.

View file

@ -216,3 +216,37 @@ Example::
}
Only ``filter`` is allowed as value. Will submit an empty query to switch to filter mode.
.. _searching_dataProcessing:
dataProcessing
--------------
Used by: All connections while indexing, due to implementation inside ``SearchService``.
Configure modifications on each document before returning search result. Same as provided by TYPO3
for :ref:`t3tsref:cobj-fluidtemplate` through
:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`.
All processors are applied in configured order. Allowing to work with already processed data.
Example::
plugin.tx_searchcore.settings.searching.dataProcessing {
1 = Codappix\SearchCore\DataProcessing\CopyToProcessor
1 {
to = search_spellcheck
}
2 = Codappix\SearchCore\DataProcessing\CopyToProcessor
2 {
to = search_all
}
}
The above example will copy all existing fields to the field ``search_spellcheck``. Afterwards
all fields, including ``search_spellcheck`` will be copied to ``search_all``.
.. include:: /configuration/dataProcessing/availableAndPlanned.rst
Also data processors are available while indexing too, see :ref:`dataProcessing`.

View file

@ -29,6 +29,16 @@ Also multiple filter are supported. Filtering results by fields for string conte
Facets / aggregates are also possible. Therefore a mapping has to be defined in TypoScript for
indexing, and the facets itself while searching.
.. _features_dataProcessing:
DataProcessing
==============
DataProcessing, as known from ``FLUIDTEMPLATE`` is available while indexing and for search results.
Each item can be processed by multiple processor to prepare data for indexing and output.
See :ref:`concepts_indexing_dataprocessing` in :ref:`concepts` section.
.. _features_planned:
Planned

View file

@ -27,15 +27,15 @@ class CopyToProcessorTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider getPossibleRecordConfigurationCombinations
* @dataProvider getPossibleDataConfigurationCombinations
*/
public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedRecord)
public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedData)
{
$subject = new CopyToProcessor();
$processedRecord = $subject->processRecord($record, $configuration);
$processedData = $subject->processData($record, $configuration);
$this->assertSame(
$expectedRecord,
$processedRecord,
$expectedData,
$processedData,
'The processor did not return the expected processed record.'
);
}
@ -43,7 +43,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase
/**
* @return array
*/
public function getPossibleRecordConfigurationCombinations()
public function getPossibleDataConfigurationCombinations()
{
return [
'Copy all fields to new field' => [
@ -54,7 +54,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase
'configuration' => [
'to' => 'new_field',
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
'field 2' => 'Some more content like ipsum',
'new_field' => 'Some content like lorem' . PHP_EOL . 'Some more content like ipsum',
@ -71,7 +71,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase
'configuration' => [
'to' => 'new_field',
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
'field with sub2' => [
'Tag 1',

View file

@ -27,15 +27,15 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider getPossibleRecordConfigurationCombinations
* @dataProvider getPossibleDataConfigurationCombinations
*/
public function geoPointsAreAddedAsConfigured(array $record, array $configuration, array $expectedRecord)
public function geoPointsAreAddedAsConfigured(array $record, array $configuration, array $expectedData)
{
$subject = new GeoPointProcessor();
$processedRecord = $subject->processRecord($record, $configuration);
$processedData = $subject->processData($record, $configuration);
$this->assertSame(
$expectedRecord,
$processedRecord,
$expectedData,
$processedData,
'The processor did not return the expected processed record.'
);
}
@ -43,7 +43,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
/**
* @return array
*/
public function getPossibleRecordConfigurationCombinations()
public function getPossibleDataConfigurationCombinations()
{
return [
'Create new field with existing lat and lng' => [
@ -56,7 +56,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'expectedData' => [
'lat' => 23.232,
'lng' => 45.43,
'location' => [
@ -73,7 +73,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
'configuration' => [
'to' => 'location',
],
'expectedRecord' => [
'expectedData' => [
'lat' => 23.232,
'lng' => 45.43,
],
@ -88,7 +88,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'expectedData' => [
'lat' => '',
'lng' => '',
],
@ -103,7 +103,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'expectedData' => [
'lat' => 'av',
'lng' => 'dsf',
],

View file

@ -27,15 +27,15 @@ class RemoveProcessorTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider getPossibleRecordConfigurationCombinations
* @dataProvider getPossibleDataConfigurationCombinations
*/
public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedRecord)
public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedData)
{
$subject = new RemoveProcessor();
$processedRecord = $subject->processRecord($record, $configuration);
$processedData = $subject->processData($record, $configuration);
$this->assertSame(
$expectedRecord,
$processedRecord,
$expectedData,
$processedData,
'The processor did not return the expected processed record.'
);
}
@ -43,7 +43,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
/**
* @return array
*/
public function getPossibleRecordConfigurationCombinations()
public function getPossibleDataConfigurationCombinations()
{
return [
'Nothing configured' => [
@ -56,7 +56,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
],
'configuration' => [
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
'field with sub2' => [
'Tag 1',
@ -76,7 +76,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
'fields' => 'field with sub2',
'_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor',
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
],
],
@ -92,7 +92,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
'fields' => 'non existing',
'_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor',
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
'field with sub2' => [
'Tag 1',
@ -113,7 +113,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
'fields' => 'field 3, field with sub2',
'_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor',
],
'expectedRecord' => [
'expectedData' => [
'field 1' => 'Some content like lorem',
],
],
@ -125,7 +125,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase
'fields' => 'field 1',
'_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor',
],
'expectedRecord' => [
'expectedData' => [
],
],
];

View file

@ -24,9 +24,9 @@ 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;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
class AbstractIndexerTest extends AbstractUnitTestCase
{
@ -46,9 +46,9 @@ class AbstractIndexerTest extends AbstractUnitTestCase
protected $connection;
/**
* @var ObjectManagerInterface
* @var DataProcessorService
*/
protected $objectManager;
protected $dataProcessorService;
public function setUp()
{
@ -56,14 +56,15 @@ class AbstractIndexerTest extends AbstractUnitTestCase
$this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock();
$this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock();
$this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class)->getMock();
$this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class)
->disableOriginalConstructor()
->getMock();
$this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [
$this->connection,
$this->configuration
]);
$this->inject($this->subject, 'objectManager', $this->objectManager);
$this->inject($this->subject, 'dataProcessorService', $this->dataProcessorService);
$this->subject->injectLogger($this->getMockedLogger());
$this->subject->setIdentifier('testTable');
$this->subject->expects($this->any())
@ -82,12 +83,30 @@ class AbstractIndexerTest extends AbstractUnitTestCase
$expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test';
$expectedRecord['search_abstract'] = '';
$this->objectManager->expects($this->any())
->method('get')
->with(CopyToProcessor::class)
->willReturn(new CopyToProcessor());
$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->exactly(2))
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields'])
->will($this->onConsecutiveCalls([

View file

@ -0,0 +1,110 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\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.
*/
use Codappix\SearchCore\Domain\Model\ResultItem;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class ResultItemTest extends AbstractUnitTestCase
{
/**
* @test
*/
public function plainDataCanBeRetrieved()
{
$originalData = [
'uid' => 10,
'title' => 'Some title',
];
$expectedData = $originalData;
$subject = new ResultItem($originalData);
$this->assertSame(
$expectedData,
$subject->getPlainData(),
'Could not retrieve plain data from result item.'
);
}
/**
* @test
*/
public function dataCanBeRetrievedInArrayNotation()
{
$originalData = [
'uid' => 10,
'title' => 'Some title',
];
$expectedData = $originalData;
$subject = new ResultItem($originalData);
$this->assertSame(
$originalData['title'],
$subject['title'],
'Could not retrieve title in array notation.'
);
}
/**
* @test
*/
public function existenceOfDataCanBeChecked()
{
$originalData = [
'uid' => 10,
'title' => 'Some title',
];
$subject = new ResultItem($originalData);
$this->assertTrue(isset($subject['title']), 'Could not determine that title exists.');
$this->assertFalse(isset($subject['title2']), 'Could not determine that title2 does not exists.');
}
/**
* @test
*/
public function dataCanNotBeChanged()
{
$originalData = [
'uid' => 10,
'title' => 'Some title',
];
$subject = new ResultItem($originalData);
$this->expectException(\BadMethodCallException::class);
$subject['title'] = 'New Title';
}
/**
* @test
*/
public function dataCanNotBeRemoved()
{
$originalData = [
'uid' => 10,
'title' => 'Some title',
];
$subject = new ResultItem($originalData);
$this->expectException(\BadMethodCallException::class);
unset($subject['title']);
}
}

View file

@ -20,7 +20,10 @@ namespace Codappix\SearchCore\Tests\Unit\Domain\Model;
* 02110-1301, USA.
*/
use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\SearchService;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class SearchRequestTest extends AbstractUnitTestCase
@ -31,12 +34,12 @@ class SearchRequestTest extends AbstractUnitTestCase
*/
public function emptyFilterWillNotBeSet(array $filter)
{
$searchRequest = new SearchRequest();
$searchRequest->setFilter($filter);
$subject = new SearchRequest();
$subject->setFilter($filter);
$this->assertSame(
[],
$searchRequest->getFilter(),
$subject->getFilter(),
'Empty filter were set, even if they should not.'
);
}
@ -68,13 +71,67 @@ class SearchRequestTest extends AbstractUnitTestCase
public function filterIsSet()
{
$filter = ['someField' => 'someValue'];
$searchRequest = new SearchRequest();
$searchRequest->setFilter($filter);
$subject = new SearchRequest();
$subject->setFilter($filter);
$this->assertSame(
$filter,
$searchRequest->getFilter(),
$subject->getFilter(),
'Filter was not set.'
);
}
/**
* @test
*/
public function exceptionIsThrownIfSearchServiceWasNotSet()
{
$subject = new SearchRequest();
$subject->setConnection($this->getMockBuilder(ConnectionInterface::class)->getMock());
$this->expectException(\InvalidArgumentException::class);
$subject->execute();
}
/**
* @test
*/
public function exceptionIsThrownIfConnectionWasNotSet()
{
$subject = new SearchRequest();
$subject->setSearchService(
$this->getMockBuilder(SearchService::class)
->disableOriginalConstructor()
->getMock()
);
$this->expectException(\InvalidArgumentException::class);
$subject->execute();
}
/**
* @test
*/
public function executionMakesUseOfProvidedConnectionAndSearchService()
{
$searchServiceMock = $this->getMockBuilder(SearchService::class)
->disableOriginalConstructor()
->getMock();
$connectionMock = $this->getMockBuilder(ConnectionInterface::class)
->getMock();
$searchResultMock = $this->getMockBuilder(SearchResultInterface::class)
->getMock();
$subject = new SearchRequest();
$subject->setSearchService($searchServiceMock);
$subject->setConnection($connectionMock);
$connectionMock->expects($this->once())
->method('search')
->with($subject)
->willReturn($searchResultMock);
$searchServiceMock->expects($this->once())
->method('processResult')
->with($searchResultMock);
$subject->execute();
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\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.
*/
use Codappix\SearchCore\Connection\ResultItemInterface;
use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Domain\Model\SearchResult;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class SearchResultTest extends AbstractUnitTestCase
{
/**
* @test
*/
public function countIsRetrievedFromOriginalResult()
{
$originalSearchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$originalSearchResultMock->expects($this->once())->method('count');
$subject = new SearchResult($originalSearchResultMock, []);
$subject->count();
}
/**
* @test
*/
public function currentCountIsRetrievedFromOriginalResult()
{
$originalSearchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$originalSearchResultMock->expects($this->once())->method('getCurrentCount');
$subject = new SearchResult($originalSearchResultMock, []);
$subject->getCurrentCount();
}
/**
* @test
*/
public function facetsAreRetrievedFromOriginalResult()
{
$originalSearchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$originalSearchResultMock->expects($this->once())->method('getFacets');
$subject = new SearchResult($originalSearchResultMock, []);
$subject->getFacets();
}
/**
* @test
*/
public function resultItemsCanBeRetrieved()
{
$originalSearchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$data = [
[
'uid' => 10,
'title' => 'Some Title',
],
[
'uid' => 11,
'title' => 'Some Title 2',
],
[
'uid' => 12,
'title' => 'Some Title 3',
],
];
$subject = new SearchResult($originalSearchResultMock, $data);
$resultItems = $subject->getResults();
$this->assertCount(3, $resultItems);
$this->assertSame($data[0]['uid'], $resultItems[0]['uid']);
$this->assertSame($data[1]['uid'], $resultItems[1]['uid']);
$this->assertSame($data[2]['uid'], $resultItems[2]['uid']);
$this->assertInstanceOf(ResultItemInterface::class, $resultItems[0]);
$this->assertInstanceOf(ResultItemInterface::class, $resultItems[1]);
$this->assertInstanceOf(ResultItemInterface::class, $resultItems[2]);
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Copyright\SearchCore\Tests\Unit\Domain\Search;
namespace Codappix\SearchCore\Tests\Unit\Domain\Search;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
@ -23,7 +23,10 @@ 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\Model\SearchResult;
use Codappix\SearchCore\Domain\Search\SearchService;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
@ -35,10 +38,38 @@ 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();
@ -48,11 +79,15 @@ class SearchServiceTest extends AbstractUnitTestCase
$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,19 +96,19 @@ 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')
->with($this->callback(function ($searchRequest) {
return $searchRequest->getLimit() === 45;
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest);
@ -84,19 +119,19 @@ 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')
->with($this->callback(function ($searchRequest) {
return $searchRequest->getLimit() === 10;
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest);
@ -107,20 +142,23 @@ 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')
->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['property' => 'something'];
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest);
@ -131,14 +169,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')
@ -147,7 +187,8 @@ class SearchServiceTest extends AbstractUnitTestCase
'anotherProperty' => 'anything',
'property' => 'something',
];
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']);
@ -159,20 +200,20 @@ 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())
->method('search')
->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['anotherProperty' => 'anything'];
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']);
@ -184,23 +225,93 @@ 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')
->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['anotherProperty' => 'anything'];
}));
}))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']);
$this->subject->search($searchRequest);
}
/**
* @test
*/
public function originalSearchResultIsReturnedIfNoDataProcessorIsConfigured()
{
$this->configuration->expects($this->any())
->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->any())
->method('get')
->will($this->throwException(new InvalidArgumentException));
$searchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$this->connection->expects($this->once())
->method('search')
->willReturn($searchResultMock);
$this->dataProcessorService->expects($this->never())->method('executeDataProcessor');
$searchRequest = new SearchRequest('');
$this->assertSame($searchResultMock, $this->subject->search($searchRequest), 'Did not get created result without applied data processing');
}
/**
* @test
*/
public function configuredDataProcessorsAreExecutedOnSearchResult()
{
$this->configuration->expects($this->any())
->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->any())
->method('get')
->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
['SomeProcessorClass']
));
$searchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock();
$searchResult = new SearchResult($searchResultMock, [['field 1' => 'value 1']]);
$this->connection->expects($this->once())
->method('search')
->willReturn($searchResult);
$this->dataProcessorService->expects($this->once())
->method('executeDataProcessor')
->with('SomeProcessorClass', ['field 1' => 'value 1'])
->willReturn([
'field 1' => 'value 1',
'field 2' => 'value 2',
]);
$this->objectManager->expects($this->once())
->method('get')
->with(SearchResult::class, $searchResult, [
['field 1' => 'value 1', 'field 2' => 'value 2']
])
->willReturn($searchResultMock);
$searchRequest = new SearchRequest('');
$this->assertSame($searchResultMock, $this->subject->search($searchRequest), 'Did not get created result with applied data processing');
}
}