Merge branch 'develop' into support/76

Conflicts:
    Classes/DataProcessing/ProcessorInterface.php
    Classes/Domain/Index/AbstractIndexer.php
    Classes/Integration/Form/Finisher/DataHandlerFinisher.php
    Makefile
    Tests/Functional/Connection/Elasticsearch/FilterTest.php
    Tests/Functional/Fixtures/BasicSetup.ts
    Tests/Unit/Integration/Form/Finisher/DataHandlerFinisherTest.php
    composer.json
This commit is contained in:
Daniel Siepmann 2018-03-08 11:53:44 +01:00
commit ee3987a746
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
61 changed files with 1534 additions and 387 deletions

View file

@ -35,6 +35,7 @@ services:
install: make install install: make install
script: script:
- make cgl
- make unitTests - make unitTests
- make functionalTests - make functionalTests

View file

@ -132,7 +132,10 @@ class Elasticsearch implements Singleton, ConnectionInterface
} }
); );
} catch (\Elastica\Exception\NotFoundException $exception) { } catch (\Elastica\Exception\NotFoundException $exception) {
$this->logger->debug('Tried to delete document in index, which does not exist.', [$documentType, $identifier]); $this->logger->debug(
'Tried to delete document in index, which does not exist.',
[$documentType, $identifier]
);
} }
} }

View file

@ -45,25 +45,26 @@ class Facet implements FacetInterface
*/ */
protected $options = []; protected $options = [];
public function __construct($name, array $aggregation, ConfigurationContainerInterface $configuration) public function __construct(string $name, array $aggregation, ConfigurationContainerInterface $configuration)
{ {
$this->name = $name; $this->name = $name;
$this->buckets = $aggregation['buckets']; $this->buckets = $aggregation['buckets'];
$this->field = $configuration->getIfExists('searching.facets.' . $this->name . '.field') ?: '';
$config = $configuration->getIfExists('searching.facets.' . $this->name) ?: [];
foreach ($config as $configEntry) {
if (isset($configEntry['field'])) {
$this->field = $configEntry['field'];
break;
}
}
} }
/** public function getName() : string
* @return string
*/
public function getName()
{ {
return $this->name; return $this->name;
} }
/** public function getField() : string
* @return string
*/
public function getField()
{ {
return $this->field; return $this->field;
} }
@ -73,7 +74,7 @@ class Facet implements FacetInterface
* *
* @return array<FacetOptionInterface> * @return array<FacetOptionInterface>
*/ */
public function getOptions() public function getOptions() : array
{ {
$this->initOptions(); $this->initOptions();

View file

@ -29,6 +29,11 @@ class FacetOption implements FacetOptionInterface
*/ */
protected $name = ''; protected $name = '';
/**
* @var string
*/
protected $displayName = '';
/** /**
* @var int * @var int
*/ */
@ -40,21 +45,21 @@ class FacetOption implements FacetOptionInterface
public function __construct(array $bucket) public function __construct(array $bucket)
{ {
$this->name = $bucket['key']; $this->name = $bucket['key'];
$this->displayName = isset($bucket['key_as_string']) ? $bucket['key_as_string'] : $this->getName();
$this->count = $bucket['doc_count']; $this->count = $bucket['doc_count'];
} }
/** public function getName() : string
* @return string
*/
public function getName()
{ {
return $this->name; return $this->name;
} }
/** public function getDisplayName() : string
* @return int {
*/ return $this->displayName;
public function getCount() }
public function getCount() : int
{ {
return $this->count; return $this->count;
} }

View file

@ -24,10 +24,14 @@ use Codappix\SearchCore\Connection\FacetInterface;
use Codappix\SearchCore\Connection\ResultItemInterface; use Codappix\SearchCore\Connection\ResultItemInterface;
use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Connection\SearchResultInterface; use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Domain\Model\QueryResultInterfaceStub;
use Codappix\SearchCore\Domain\Model\ResultItem;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
class SearchResult implements SearchResultInterface class SearchResult implements SearchResultInterface
{ {
use QueryResultInterfaceStub;
/** /**
* @var SearchRequestInterface * @var SearchRequestInterface
*/ */
@ -104,7 +108,7 @@ class SearchResult implements SearchResultInterface
} }
foreach ($this->result->getResults() as $result) { 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; $this->position = 0;
} }
// Extbase QueryResultInterface - Implemented to support Pagination of Fluid.
public function getQuery() public function getQuery()
{ {
return $this->searchRequest; 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

@ -28,15 +28,17 @@ interface FacetOptionInterface
/** /**
* Returns the name of this option. Equivalent * Returns the name of this option. Equivalent
* to value used for filtering. * to value used for filtering.
*
* @return string
*/ */
public function getName(); public function getName() : string;
/**
* If a pre-rendered name is provided, this will be returned.
* Otherwise it's the same as getName().
*/
public function getDisplayName() : string;
/** /**
* Returns the number of found results for this option. * Returns the number of found results for this option.
*
* @return int
*/ */
public function getCount(); public function getCount() : int;
} }

View file

@ -28,15 +28,11 @@ interface FacetRequestInterface
/** /**
* The identifier of the facet, used as key in arrays and to get the facet * The identifier of the facet, used as key in arrays and to get the facet
* from search request, etc. * from search request, etc.
*
* @return string
*/ */
public function getIdentifier(); public function getIdentifier() : string;
/** /**
* The field to use for facet building. * The config to use for facet building.
*
* @return string
*/ */
public function getField(); public function getConfig() : array;
} }

View file

@ -25,5 +25,12 @@ namespace Codappix\SearchCore\Connection;
*/ */
interface ResultItemInterface extends \ArrayAccess 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. * 02110-1301, USA.
*/ */
use Codappix\SearchCore\Domain\Search\SearchService;
use TYPO3\CMS\Extbase\Persistence\QueryInterface; use TYPO3\CMS\Extbase\Persistence\QueryInterface;
interface SearchRequestInterface extends QueryInterface interface SearchRequestInterface extends QueryInterface
@ -40,4 +41,10 @@ interface SearchRequestInterface extends QueryInterface
* @return array * @return array
*/ */
public function getFilter(); 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,59 @@
<?php
namespace Codappix\SearchCore\DataProcessing;
/*
* Copyright (C) 2017 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\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\TypoScriptService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Executes an existing TYPO3 DataProcessor on the given data.
*/
class ContentObjectDataProcessorAdapterProcessor implements ProcessorInterface
{
/**
* @var TypoScriptService
*/
protected $typoScriptService;
public function __construct(TypoScriptService $typoScriptService)
{
$this->typoScriptService = $typoScriptService;
}
public function processData(array $data, array $configuration) : array
{
$dataProcessor = GeneralUtility::makeInstance($configuration['_dataProcessor']);
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->data = $data;
if (isset($configuration['_table'])) {
$contentObjectRenderer->start($data, $configuration['_table']);
}
return $dataProcessor->process(
$contentObjectRenderer,
[],
$this->typoScriptService->convertPlainArrayToTypoScriptArray($configuration),
$data
);
}
}

View file

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

View file

@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing;
*/ */
class GeoPointProcessor implements ProcessorInterface 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)) { if (! $this->canApply($record, $configuration)) {
return $record; return $record;

View file

@ -21,14 +21,13 @@ namespace Codappix\SearchCore\DataProcessing;
*/ */
/** /**
* All DataProcessing Processors should implement this interface, otherwise * All DataProcessing Processors should implement this interface.
* they will not be executed.
*/ */
interface ProcessorInterface interface ProcessorInterface
{ {
/** /**
* Processes the given record. * Processes the given data.
* Also retrieves the configuration for this processor instance. * 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 class RemoveProcessor implements ProcessorInterface
{ {
public function processRecord(array $record, array $configuration) : array public function processData(array $record, array $configuration) : array
{ {
if (!isset($configuration['fields'])) { if (!isset($configuration['fields'])) {
return $record; 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,6 +43,12 @@ abstract class AbstractIndexer implements IndexerInterface
*/ */
protected $identifier = ''; protected $identifier = '';
/**
* @var \Codappix\SearchCore\DataProcessing\Service
* @inject
*/
protected $dataProcessorService;
/** /**
* @var \TYPO3\CMS\Core\Log\Logger * @var \TYPO3\CMS\Core\Log\Logger
*/ */
@ -135,18 +141,7 @@ abstract class AbstractIndexer implements IndexerInterface
{ {
try { try {
foreach ($this->configuration->get('indexing.' . $this->identifier . '.dataProcessing') as $configuration) { foreach ($this->configuration->get('indexing.' . $this->identifier . '.dataProcessing') as $configuration) {
$className = ''; $record = $this->dataProcessorService->executeDataProcessor($configuration, $record);
if (is_string($configuration)) {
$className = $configuration;
$configuration = [];
} else {
$className = $configuration['_typoScriptNodeValue'];
}
$dataProcessor = GeneralUtility::makeInstance($className);
if ($dataProcessor instanceof ProcessorInterface) {
$record = $dataProcessor->processRecord($record, $configuration);
}
} }
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
// Nothing to do. // Nothing to do.

View file

@ -167,7 +167,9 @@ class TcaTableService
; ;
} }
$userDefinedWhere = $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.additionalWhereClause'); $userDefinedWhere = $this->configuration->getIfExists(
'indexing.' . $this->getTableName() . '.additionalWhereClause'
);
if (is_string($userDefinedWhere)) { if (is_string($userDefinedWhere)) {
$whereClause .= ' AND ' . $userDefinedWhere; $whereClause .= ' AND ' . $userDefinedWhere;
} }
@ -348,6 +350,9 @@ class TcaTableService
*/ */
protected function getBlackListedRootLine() : array protected function getBlackListedRootLine() : array
{ {
return GeneralUtility::intExplode(',', $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist')); return GeneralUtility::intExplode(
',',
$this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist')
);
} }
} }

View file

@ -30,37 +30,27 @@ class FacetRequest implements FacetRequestInterface
protected $identifier = ''; protected $identifier = '';
/** /**
* @var string * @var array
*/ */
protected $field = ''; protected $config = [];
/** /**
* TODO: Add validation / exception?
* As the facets come from configuration this might be a good idea to help * As the facets come from configuration this might be a good idea to help
* integrators find issues. * integrators find issues.
*
* @param string $identifier
* @param string $field
*/ */
public function __construct($identifier, $field) public function __construct(string $identifier, array $config)
{ {
$this->identifier = $identifier; $this->identifier = $identifier;
$this->field = $field; $this->config = $config;
} }
/** public function getIdentifier() : string
* @return string
*/
public function getIdentifier()
{ {
return $this->identifier; return $this->identifier;
} }
/** public function getConfig() : array
* @return string
*/
public function getField()
{ {
return $this->field; return $this->config;
} }
} }

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 <?php
namespace Codappix\SearchCore\Connection\Elasticsearch; namespace Codappix\SearchCore\Domain\Model;
/* /*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de> * Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
@ -29,9 +29,14 @@ class ResultItem implements ResultItemInterface
*/ */
protected $data = []; 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) public function offsetExists($offset)

View file

@ -23,6 +23,7 @@ namespace Codappix\SearchCore\Domain\Model;
use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\FacetRequestInterface; use Codappix\SearchCore\Connection\FacetRequestInterface;
use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Domain\Search\SearchService;
/** /**
* Represents a search request used to process an actual search. * Represents a search request used to process an actual search.
@ -63,6 +64,11 @@ class SearchRequest implements SearchRequestInterface
*/ */
protected $connection = null; protected $connection = null;
/**
* @var SearchService
*/
protected $searchService = null;
/** /**
* @param string $query * @param string $query
*/ */
@ -143,19 +149,30 @@ class SearchRequest implements SearchRequestInterface
$this->connection = $connection; $this->connection = $connection;
} }
public function setSearchService(SearchService $searchService)
{
$this->searchService = $searchService;
}
// Extbase QueryInterface // Extbase QueryInterface
// Current implementation covers only paginate widget support. // Current implementation covers only paginate widget support.
public function execute($returnRawQueryResult = false) public function execute($returnRawQueryResult = false)
{ {
if ($this->connection instanceof ConnectionInterface) { if (! ($this->connection instanceof ConnectionInterface)) {
return $this->connection->search($this);
}
throw new \InvalidArgumentException( throw new \InvalidArgumentException(
'Connection was not set before, therefore execute can not work. Use `setConnection` before.', 'Connection was not set before, therefore execute can not work. Use `setConnection` before.',
1502197732 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) 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

@ -120,6 +120,10 @@ class QueryFactory
return; return;
} }
if (trim($searchRequest->getSearchTerm()) === '') {
return;
}
$boostQueryParts = []; $boostQueryParts = [];
foreach ($fields as $fieldName => $boostValue) { foreach ($fields as $fieldName => $boostValue) {
@ -162,7 +166,11 @@ class QueryFactory
{ {
try { try {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
'stored_fields' => GeneralUtility::trimExplode(',', $this->configuration->get('searching.fields.stored_fields'), true), 'stored_fields' => GeneralUtility::trimExplode(
',',
$this->configuration->get('searching.fields.stored_fields'),
true
),
]); ]);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
// Nothing configured // Nothing configured
@ -170,7 +178,10 @@ class QueryFactory
try { try {
$scriptFields = $this->configuration->get('searching.fields.script_fields'); $scriptFields = $this->configuration->get('searching.fields.script_fields');
$scriptFields = $this->configurationUtility->replaceArrayValuesWithRequestContent($searchRequest, $scriptFields); $scriptFields = $this->configurationUtility->replaceArrayValuesWithRequestContent(
$searchRequest,
$scriptFields
);
$scriptFields = $this->configurationUtility->filterByCondition($scriptFields); $scriptFields = $this->configurationUtility->filterByCondition($scriptFields);
if ($scriptFields !== []) { if ($scriptFields !== []) {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, ['script_fields' => $scriptFields]); $query = ArrayUtility::arrayMergeRecursiveOverrule($query, ['script_fields' => $scriptFields]);
@ -232,6 +243,18 @@ class QueryFactory
} }
} }
if (isset($config['raw'])) {
$filter = array_merge($config['raw'], $filter);
}
if ($config['type'] === 'range') {
return [
'range' => [
$config['field'] => $filter,
],
];
}
return [$config['field'] => $filter]; return [$config['field'] => $filter];
} }
@ -240,11 +263,7 @@ class QueryFactory
foreach ($searchRequest->getFacets() as $facet) { foreach ($searchRequest->getFacets() as $facet) {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
'aggs' => [ 'aggs' => [
$facet->getIdentifier() => [ $facet->getIdentifier() => $facet->getConfig(),
'terms' => [
'field' => $facet->getField(),
],
],
], ],
]); ]);
} }

View file

@ -25,7 +25,9 @@ use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Connection\SearchResultInterface; use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\DataProcessing\Service as DataProcessorService;
use Codappix\SearchCore\Domain\Model\FacetRequest; use Codappix\SearchCore\Domain\Model\FacetRequest;
use Codappix\SearchCore\Domain\Model\SearchResult;
use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
@ -49,19 +51,27 @@ class SearchService
*/ */
protected $objectManager; protected $objectManager;
/**
* @var DataProcessorService
*/
protected $dataProcessorService;
/** /**
* @param ConnectionInterface $connection * @param ConnectionInterface $connection
* @param ConfigurationContainerInterface $configuration * @param ConfigurationContainerInterface $configuration
* @param ObjectManagerInterface $objectManager * @param ObjectManagerInterface $objectManager
* @param DataProcessorService $dataProcessorService
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
ConfigurationContainerInterface $configuration, ConfigurationContainerInterface $configuration,
ObjectManagerInterface $objectManager ObjectManagerInterface $objectManager,
DataProcessorService $dataProcessorService
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->objectManager = $objectManager; $this->objectManager = $objectManager;
$this->dataProcessorService = $dataProcessorService;
} }
/** /**
@ -70,12 +80,15 @@ class SearchService
*/ */
public function search(SearchRequestInterface $searchRequest) public function search(SearchRequestInterface $searchRequest)
{ {
$searchRequest->setConnection($this->connection);
$this->addSize($searchRequest); $this->addSize($searchRequest);
$this->addConfiguredFacets($searchRequest); $this->addConfiguredFacets($searchRequest);
$this->addConfiguredFilters($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));
} }
/** /**
@ -103,15 +116,10 @@ class SearchService
} }
foreach ($facetsConfig as $identifier => $facetConfig) { foreach ($facetsConfig as $identifier => $facetConfig) {
if (!isset($facetConfig['field']) || trim($facetConfig['field']) === '') {
// TODO: Finish throw
throw new \Exception('message', 1499171142);
}
$searchRequest->addFacet($this->objectManager->get( $searchRequest->addFacet($this->objectManager->get(
FacetRequest::class, FacetRequest::class,
$identifier, $identifier,
$facetConfig['field'] $facetConfig
)); ));
} }
} }
@ -138,4 +146,30 @@ class SearchService
// Nothing todo, no filter configured. // 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

@ -82,16 +82,6 @@ class DataHandler implements Singleton
$this->indexerFactory = $indexerFactory; $this->indexerFactory = $indexerFactory;
} }
/**
* @param string $table
* @param array $record
*/
public function add($table, array $record)
{
$this->logger->debug('Record received for add.', [$table, $record]);
$this->getIndexer($table)->indexDocument($record['uid']);
}
/** /**
* @param string $table * @param string $table
*/ */

View file

@ -91,42 +91,36 @@ class DataHandler implements Singleton
return true; return true;
} }
/** public function processDatamap_afterAllOperations(CoreDataHandler $dataHandler)
* Called by CoreDataHandler on database operations, e.g. if new records were created or records were updated. {
* foreach ($dataHandler->datamap as $table => $record) {
* @param string $status $uid = key($record);
* @param string $table $fieldData = current($record);
* @param string|int $uid
* @param array $fieldArray if (isset($fieldArray['uid'])) {
* @param CoreDataHandler $dataHandler $uid = $fieldArray['uid'];
* } elseif (isset($dataHandler->substNEWwithIDs[$uid])) {
* @return bool False if hook was not processed. $uid = $dataHandler->substNEWwithIDs[$uid];
*/ }
public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fieldArray, CoreDataHandler $dataHandler)
$this->processRecord($table, $uid);
}
}
protected function processRecord(string $table, int $uid) : bool
{ {
if (! $this->shouldProcessHookForTable($table)) { if (! $this->shouldProcessHookForTable($table)) {
$this->logger->debug('Database update not processed.', [$table, $uid]); $this->logger->debug('Indexing of record not processed.', [$table, $uid]);
return false; return false;
} }
if ($status === 'new') {
$fieldArray['uid'] = $dataHandler->substNEWwithIDs[$uid];
$this->dataHandler->add($table, $fieldArray);
return true;
}
if ($status === 'update') {
$record = $this->getRecord($table, $uid); $record = $this->getRecord($table, $uid);
if ($record !== null) { if ($record !== null) {
$this->dataHandler->update($table, $record); $this->dataHandler->update($table, $record);
}
return true; return true;
} }
$this->logger->debug( $this->logger->debug('Indexing of record not processed, as he was not found in Database.', [$table, $uid]);
'Database update not processed, cause status is unhandled.',
[$status, $table, $uid, $fieldArray]
);
return false; return false;
} }

View file

@ -0,0 +1,8 @@
Changelog
=========
.. toctree::
:maxdepth: 1
:glob:
changelog/*

View file

@ -0,0 +1,40 @@
Breacking Change 120 "Pass facets configuration to elasticsearch"
=================================================================
In order to allow arbitrary facet configuration, we do not process the facet configuration anymore.
Instead integrators are able to configure facets for search service "as is". We just pipe the
configuration through.
Therefore the following, which worked before, does not work anymore:
.. code-block:: typoscript
:linenos:
:emphasize-lines: 4
plugin.tx_searchcore.settings.search {
facets {
category {
field = categories
}
}
}
Instead you have to provide the full configuration yourself:
.. code-block:: typoscript
:linenos:
:emphasize-lines: 4,6
plugin.tx_searchcore.settings.search {
facets {
category {
terms {
field = categories
}
}
}
}
You need to add line 4 and 6, the additional level ``terms`` for elasticsearch.
See :issue:`120`.

View file

@ -36,5 +36,7 @@ DataProcessing
Before data is transfered to search service, it can be processed by "DataProcessors" like already 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`. 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`. Configuration is done through TypoScript, see :ref:`dataProcessing`.

View file

@ -5,9 +5,12 @@
Configuration Configuration
============= =============
Installation wide configuration is handled inside of the extension manager. Just check out the
options there, they all have labels.
The extension offers the following configuration options through TypoScript. If you overwrite them The extension offers the following configuration options through TypoScript. If you overwrite them
through `setup` make sure to keep them in the `module` area as they will be accessed from backend through `setup` make sure to keep them in the `module` area as they will be accessed from backend
mode of TYPO3. Do so by placing the following line at the end:: mode of TYPO3 for indexing. Do so by placing the following line at the end::
module.tx_searchcore < plugin.tx_searchcore module.tx_searchcore < plugin.tx_searchcore

View file

@ -0,0 +1,32 @@
``Codappix\SearchCore\DataProcessing\ContentObjectDataProcessorAdapterProcessor``
=================================================================================
Will execute an existing TYPO3 data processor.
Possible Options:
``_dataProcessor``
Necessary, defined which data processor to apply. Provide the same as you would to call the
processor.
``_table``
Defines the "current" table as used by some processors, e.g.
``TYPO3\CMS\Frontend\DataProcessing\FilesProcessor``.
All further options are passed to the configured data processor. Therefore they are documented at
each data processor.
Example::
plugin.tx_searchcore.settings.searching.dataProcessing {
1 = Codappix\SearchCore\DataProcessing\ContentObjectDataProcessorAdapterProcessor
1 {
_table = pages
_dataProcessor = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
references.fieldName = media
as = images
}
}
The above example will create a new field ``images`` with resolved FAL relations from ``media``
field.

View file

@ -0,0 +1,36 @@
The following Processor are available:
.. toctree::
:maxdepth: 1
:glob:
/configuration/dataProcessing/ContentObjectDataProcessorAdapterProcessor
/configuration/dataProcessing/CopyToProcessor
/configuration/dataProcessing/GeoPointProcessor
/configuration/dataProcessing/RemoveProcessor
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 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 Configure modifications on each document before sending it to the configured connection. Same as
provided by TYPO3 for :ref:`t3tsref:cobj-fluidtemplate` through provided by TYPO3 for :ref:`t3tsref:cobj-fluidtemplate` through
@ -170,38 +170,7 @@ Example::
The above example will copy all existing fields to the field ``search_spellcheck``. Afterwards 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``. 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:: Also data processors are available for search results too, see :ref:`searching_dataProcessing`.
: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.

View file

@ -119,20 +119,48 @@ E.g. you submit a filter in form of:
.. code-block:: html .. code-block:: html
<f:form.textfield property="filter.distance.location.lat" value="51.168098" /> <f:comment>
<f:form.textfield property="filter.distance.location.lon" value="6.381384" /> Due to TYPO3 7.x fluid limitations, we build this input ourself.
<f:form.textfield property="filter.distance.distance" value="100km" /> No longer necessary in 8 and above
</f:comment>
<select name="tx_searchcore_search[searchRequest][filter][month][from]" class="_control" >
<option value="">Month</option>
<f:for each="{searchResult.facets.month.options}" as="month">
<f:if condition="{month.count}">
<option
value="{month.displayName -> f:format.date(format: 'Y-m')}"
{f:if(condition: '{searchRequest.filter.month.from} == {month.displayName -> f:format.date(format: \'Y-m\')}', then: 'selected="true"')}
>{month.displayName -> f:format.date(format: '%B %Y')}</option>
</f:if>
</f:for>
</select>
<select name="tx_searchcore_search[searchRequest][filter][month][to]" class="_control" >
<option value="">Month</option>
<f:for each="{searchResult.facets.month.options}" as="month">
<f:if condition="{month.count}">
<option
value="{month.displayName -> f:format.date(format: 'Y-m')}"
{f:if(condition: '{searchRequest.filter.month.from} == {month.displayName -> f:format.date(format: \'Y-m\')}', then: 'selected="true"')}
>{month.displayName -> f:format.date(format: '%B %Y')}</option>
</f:if>
</f:for>
</select>
This will create a ``distance`` filter with subproperties. To make this filter actually work, you This will create a ``distance`` filter with subproperties. To make this filter actually work, you
can add the following TypoScript, which will be added to the filter:: can add the following TypoScript, which will be added to the filter::
mapping { mapping {
filter { filter {
distance { month {
field = geo_distance type = range
field = released
raw {
format = yyyy-MM
}
fields { fields {
distance = distance gte = from
location = location lte = to
} }
} }
} }
@ -143,9 +171,12 @@ in elasticsearch. In above example they do match, but you can also use different
On the left hand side is the elasticsearch field name, on the right side the one submitted as a On the left hand side is the elasticsearch field name, on the right side the one submitted as a
filter. filter.
The ``field``, in above example ``geo_distance``, will be used as the elasticsearch field for The ``field``, in above example ``released``, will be used as the elasticsearch field for
filtering. This way you can use arbitrary filter names and map them to existing elasticsearch fields. filtering. This way you can use arbitrary filter names and map them to existing elasticsearch fields.
Everything that is configured inside ``raw`` is passed, as is, to search service, e.g.
elasticsearch.
.. _fields: .. _fields:
fields fields
@ -216,3 +247,37 @@ Example::
} }
Only ``filter`` is allowed as value. Will submit an empty query to switch to filter mode. 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 Facets / aggregates are also possible. Therefore a mapping has to be defined in TypoScript for
indexing, and the facets itself while searching. 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: .. _features_planned:
Planned Planned

View file

@ -15,3 +15,4 @@ Table of Contents
connections connections
indexer indexer
development development
changelog

View file

@ -19,4 +19,8 @@ In that case you need to install all dependencies yourself. Dependencies are:
Afterwards you need to enable the extension through the extension manager and include the static Afterwards you need to enable the extension through the extension manager and include the static
TypoScript setup. TypoScript setup.
If you **don't** want to use the included elasticsearch integration, you have to disable it in the
extension manager configuration of the extension by checking the checkbox.
It's currently enabled by default but will be moved into its own extension in the future.
.. _downloading: https://github.com/DanielSiepmann/search_core/archive/master.zip .. _downloading: https://github.com/DanielSiepmann/search_core/archive/master.zip

View file

@ -15,6 +15,9 @@ install: clean
COMPOSER_PROCESS_TIMEOUT=1000 composer require -vv --dev --prefer-source typo3/cms="$(TYPO3_VERSION)" COMPOSER_PROCESS_TIMEOUT=1000 composer require -vv --dev --prefer-source typo3/cms="$(TYPO3_VERSION)"
git checkout composer.json git checkout composer.json
cgl:
./.Build/bin/phpcs
functionalTests: functionalTests:
typo3DatabaseName=$(typo3DatabaseName) \ typo3DatabaseName=$(typo3DatabaseName) \
typo3DatabaseUsername=$(typo3DatabaseUsername) \ typo3DatabaseUsername=$(typo3DatabaseUsername) \

View file

@ -0,0 +1,78 @@
<?php
namespace Codappix\SearchCore\Tests\Functional\Connection\Elasticsearch;
/*
* Copyright (C) 2017 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\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\SearchService;
use TYPO3\CMS\Extbase\Object\ObjectManager;
class FacetTest extends AbstractFunctionalTestCase
{
protected function getTypoScriptFilesForFrontendRootPage()
{
return array_merge(
parent::getTypoScriptFilesForFrontendRootPage(),
['EXT:search_core/Tests/Functional/Fixtures/Searching/Facet.ts']
);
}
protected function getDataSets()
{
return array_merge(
parent::getDataSets(),
['Tests/Functional/Fixtures/Searching/Filter.xml']
);
}
/**
* @test
*/
public function itsPossibleToFetchFacetsForField()
{
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->indexAllDocuments()
;
$searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(SearchService::class);
$searchRequest = new SearchRequest();
$result = $searchService->search($searchRequest);
$this->assertSame(1, count($result->getFacets()), 'Did not receive the single defined facet.');
$facet = current($result->getFacets());
$this->assertSame('contentTypes', $facet->getName(), 'Name of facet was not as expected.');
$this->assertSame('CType', $facet->getField(), 'Field of facet was not expected.');
$options = $facet->getOptions();
$this->assertSame(2, count($options), 'Did not receive the expected number of possible options for facet.');
$option = $options['HTML'];
$this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
$option = $options['Header'];
$this->assertSame('Header', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
}
}

View file

@ -58,37 +58,4 @@ class FilterTest extends AbstractFunctionalTestCase
$this->assertSame(5, (int) $result->getResults()[0]['uid'], 'Did not get the expected result entry.'); $this->assertSame(5, (int) $result->getResults()[0]['uid'], 'Did not get the expected result entry.');
$this->assertSame(1, count($result), 'Did not receive the single filtered element.'); $this->assertSame(1, count($result), 'Did not receive the single filtered element.');
} }
/**
* @test
*/
public function itsPossibleToFetchFacetsForField()
{
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->indexAllDocuments()
;
$searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(SearchService::class);
$searchRequest = new SearchRequest();
$result = $searchService->search($searchRequest);
$this->assertSame(1, count($result->getFacets()), 'Did not receive the single defined facet.');
$facet = current($result->getFacets());
$this->assertSame('contentTypes', $facet->getName(), 'Name of facet was not as expected.');
$this->assertSame('CType', $facet->getField(), 'Field of facet was not expected.');
$options = $facet->getOptions();
$this->assertSame(2, count($options), 'Did not receive the expected number of possible options for facet.');
$option = $options['HTML'];
$this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
$option = $options['Header'];
$this->assertSame('Header', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
}
} }

View file

@ -0,0 +1,55 @@
<?php
namespace Codappix\SearchCore\Tests\Functional\DataProcessing;
/*
* Copyright (C) 2017 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\DataProcessing\ContentObjectDataProcessorAdapterProcessor;
use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3\CMS\Extbase\Service\TypoScriptService;
use TYPO3\CMS\Frontend\DataProcessing\SplitProcessor;
class ContentObjectDataProcessorAdapterProcessorTest extends AbstractFunctionalTestCase
{
/**
* @test
*/
public function contentObjectDataProcessorIsExecuted()
{
$record = ['content' => 'value1, value2'];
$configuration = [
'_dataProcessor' => SplitProcessor::class,
'delimiter' => ',',
'fieldName' => 'content',
'as' => 'new_content',
];
$expectedData = [
'content' => 'value1, value2',
'new_content' => ['value1', 'value2'],
];
$subject = new ContentObjectDataProcessorAdapterProcessor(new TypoScriptService);
$processedData = $subject->processData($record, $configuration);
$this->assertSame(
$expectedData,
$processedData,
'The processor did not return the expected processed record.'
);
}
}

View file

@ -37,13 +37,6 @@ plugin {
} }
searching { searching {
fields = search_title
facets {
contentTypes {
field = CType
}
}
fields { fields {
query = _all query = _all
} }

View file

@ -0,0 +1,17 @@
plugin {
tx_searchcore {
settings {
searching {
facets {
contentTypes {
terms {
field = CType
}
}
}
}
}
}
}
module.tx_searchcore < plugin.tx_searchcore

View file

@ -1,54 +0,0 @@
<?php
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* 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\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
class IgnoresUnkownOperationTest extends AbstractDataHandlerTest
{
/**
* @var DataHandlerService|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
*/
protected $subject;
/**
* @test
*/
public function dataHandlerCommandSomethingIsIgnored()
{
$subject = new DataHandlerHook($this->subject);
$this->assertFalse(
$subject->processDatamap_afterDatabaseOperations(
'something',
'tt_content',
1,
[],
new Typo3DataHandler
),
'Hook processed status "something".'
);
}
}

View file

@ -64,7 +64,7 @@ class NonAllowedTablesTest extends AbstractDataHandlerTest
/** /**
* @test * @test
*/ */
public function updateWillNotBeTriggeredForSysCategory() public function updateWillNotBeTriggeredForExistingSysCategory()
{ {
$this->subject->expects($this->exactly(0))->method('update'); $this->subject->expects($this->exactly(0))->method('update');
@ -83,9 +83,9 @@ class NonAllowedTablesTest extends AbstractDataHandlerTest
/** /**
* @test * @test
*/ */
public function addWillNotBeTriggeredForSysCategoy() public function updateWillNotBeTriggeredForNewSysCategoy()
{ {
$this->subject->expects($this->exactly(0))->method('add'); $this->subject->expects($this->exactly(0))->method('update');
$tce = GeneralUtility::makeInstance(Typo3DataHandler::class); $tce = GeneralUtility::makeInstance(Typo3DataHandler::class);
$tce->stripslashes_values = 0; $tce->stripslashes_values = 0;

View file

@ -66,7 +66,7 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest
/** /**
* @test * @test
*/ */
public function updateWillBeTriggeredForTtContent() public function updateWillBeTriggeredForExistingTtContent()
{ {
$this->subject->expects($this->exactly(1))->method('update') $this->subject->expects($this->exactly(1))->method('update')
->with( ->with(
@ -94,9 +94,9 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest
/** /**
* @test * @test
*/ */
public function addWillBeTriggeredForTtContent() public function updateWillBeTriggeredForNewTtContent()
{ {
$this->subject->expects($this->exactly(1))->method('add') $this->subject->expects($this->exactly(1))->method('update')
->with( ->with(
$this->equalTo('tt_content'), $this->equalTo('tt_content'),
$this->callback(function ($record) { $this->callback(function ($record) {

View file

@ -31,8 +31,11 @@ class ConfigurationUtilityTest extends AbstractUnitTestCase
* @test * @test
* @dataProvider possibleRequestAndConfigurationForFluidtemplate * @dataProvider possibleRequestAndConfigurationForFluidtemplate
*/ */
public function recursiveEntriesAreProcessedAsFluidtemplate(SearchRequestInterface $searchRequest, array $array, array $expected) public function recursiveEntriesAreProcessedAsFluidtemplate(
{ SearchRequestInterface $searchRequest,
array $array,
array $expected
) {
$subject = new ConfigurationUtility(); $subject = new ConfigurationUtility();
$this->assertSame( $this->assertSame(

View file

@ -0,0 +1,64 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Connection\Elasticsearch;
/*
* 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\Elasticsearch\FacetOption;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class FacetOptionTest extends AbstractUnitTestCase
{
/**
* @test
*/
public function displayNameIsReturnedAsExpected()
{
$bucket = [
'key' => 'Name',
'key_as_string' => 'DisplayName',
'doc_count' => 10,
];
$subject = new FacetOption($bucket);
$this->assertSame(
$bucket['key_as_string'],
$subject->getDisplayName(),
'Display name was not returned as expected.'
);
}
/**
* @test
*/
public function displayNameIsReturnedAsExpectedIfNotProvided()
{
$bucket = [
'key' => 'Name',
'doc_count' => 10,
];
$subject = new FacetOption($bucket);
$this->assertSame(
$bucket['key'],
$subject->getDisplayName(),
'Display name was not returned as expected.'
);
}
}

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\DataProcessing\CopyToProcessor; use Codappix\SearchCore\DataProcessing\CopyToProcessor;
use Codappix\SearchCore\DataProcessing\Service as DataProcessorService;
use Codappix\SearchCore\Domain\Index\AbstractIndexer; use Codappix\SearchCore\Domain\Index\AbstractIndexer;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
@ -44,17 +45,26 @@ class AbstractIndexerTest extends AbstractUnitTestCase
*/ */
protected $connection; protected $connection;
/**
* @var DataProcessorService
*/
protected $dataProcessorService;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock();
$this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock();
$this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class)
->disableOriginalConstructor()
->getMock();
$this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [ $this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [
$this->connection, $this->connection,
$this->configuration $this->configuration
]); ]);
$this->inject($this->subject, 'dataProcessorService', $this->dataProcessorService);
$this->subject->injectLogger($this->getMockedLogger()); $this->subject->injectLogger($this->getMockedLogger());
$this->subject->setIdentifier('testTable'); $this->subject->setIdentifier('testTable');
$this->subject->expects($this->any()) $this->subject->expects($this->any())
@ -73,7 +83,30 @@ class AbstractIndexerTest extends AbstractUnitTestCase
$expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test'; $expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test';
$expectedRecord['search_abstract'] = ''; $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') ->method('get')
->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields']) ->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields'])
->will($this->onConsecutiveCalls([ ->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. * 02110-1301, USA.
*/ */
use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Domain\Model\SearchRequest; use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\SearchService;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class SearchRequestTest extends AbstractUnitTestCase class SearchRequestTest extends AbstractUnitTestCase
@ -31,12 +34,12 @@ class SearchRequestTest extends AbstractUnitTestCase
*/ */
public function emptyFilterWillNotBeSet(array $filter) public function emptyFilterWillNotBeSet(array $filter)
{ {
$searchRequest = new SearchRequest(); $subject = new SearchRequest();
$searchRequest->setFilter($filter); $subject->setFilter($filter);
$this->assertSame( $this->assertSame(
[], [],
$searchRequest->getFilter(), $subject->getFilter(),
'Empty filter were set, even if they should not.' 'Empty filter were set, even if they should not.'
); );
} }
@ -68,13 +71,67 @@ class SearchRequestTest extends AbstractUnitTestCase
public function filterIsSet() public function filterIsSet()
{ {
$filter = ['someField' => 'someValue']; $filter = ['someField' => 'someValue'];
$searchRequest = new SearchRequest(); $subject = new SearchRequest();
$searchRequest->setFilter($filter); $subject->setFilter($filter);
$this->assertSame( $this->assertSame(
$filter, $filter,
$searchRequest->getFilter(), $subject->getFilter(),
'Filter was not set.' '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

@ -86,6 +86,58 @@ class QueryFactoryTest extends AbstractUnitTestCase
); );
} }
/**
* @test
*/
public function rangeFilterIsAddedToQuery()
{
$this->configureConfigurationMockWithDefault();
$this->configuration->expects($this->any())
->method('getIfExists')
->will($this->returnCallback(function ($configName) {
if ($configName === 'searching.mapping.filter.month') {
return [
'type' => 'range',
'field' => 'released',
'raw' => [
'format' => 'yyyy-MM',
],
'fields' => [
'gte' => 'from',
'lte' => 'to',
],
];
}
return [];
}));
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter([
'month' => [
'from' => '2016-03',
'to' => '2017-11',
],
]);
$query = $this->subject->create($searchRequest);
$this->assertSame(
[
[
'range' => [
'released' => [
'format' => 'yyyy-MM',
'gte' => '2016-03',
'lte' => '2017-11',
],
],
]
],
$query->toArray()['query']['bool']['filter'],
'Filter was not added to query.'
);
}
/** /**
* @test * @test
*/ */
@ -118,8 +170,8 @@ class QueryFactoryTest extends AbstractUnitTestCase
{ {
$this->configureConfigurationMockWithDefault(); $this->configureConfigurationMockWithDefault();
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->addFacet(new FacetRequest('Identifier', 'FieldName')); $searchRequest->addFacet(new FacetRequest('Identifier', ['terms' => ['field' => 'FieldName']]));
$searchRequest->addFacet(new FacetRequest('Identifier 2', 'FieldName 2')); $searchRequest->addFacet(new FacetRequest('Identifier 2', ['terms' => ['field' => 'FieldName 2']]));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertSame( $this->assertSame(

View file

@ -1,5 +1,5 @@
<?php <?php
namespace Copyright\SearchCore\Tests\Unit\Domain\Search; namespace Codappix\SearchCore\Tests\Unit\Domain\Search;
/* /*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de> * 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\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Connection\ConnectionInterface; 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\SearchRequest;
use Codappix\SearchCore\Domain\Model\SearchResult;
use Codappix\SearchCore\Domain\Search\SearchService; use Codappix\SearchCore\Domain\Search\SearchService;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
@ -35,10 +38,38 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
protected $subject; 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() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->result = $this->getMockBuilder(SearchResultInterface::class)
->disableOriginalConstructor()
->getMock();
$this->connection = $this->getMockBuilder(ConnectionInterface::class) $this->connection = $this->getMockBuilder(ConnectionInterface::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
@ -48,11 +79,15 @@ class SearchServiceTest extends AbstractUnitTestCase
$this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class) $this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class)
->setConstructorArgs([$this->objectManager])
->getMock();
$this->subject = new SearchService( $this->subject = new SearchService(
$this->connection, $this->connection,
$this->configuration, $this->configuration,
$this->objectManager $this->objectManager,
$this->dataProcessorService
); );
} }
@ -61,19 +96,19 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function sizeIsAddedFromConfiguration() public function sizeIsAddedFromConfiguration()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(45, null)); ->will($this->onConsecutiveCalls(45, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter')
->will($this->throwException(new InvalidArgumentException)); ->will($this->throwException(new InvalidArgumentException));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
->with($this->callback(function ($searchRequest) { ->with($this->callback(function ($searchRequest) {
return $searchRequest->getLimit() === 45; return $searchRequest->getLimit() === 45;
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest); $this->subject->search($searchRequest);
@ -84,19 +119,19 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function defaultSizeIsAddedIfNothingIsConfigured() public function defaultSizeIsAddedIfNothingIsConfigured()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null)); ->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter')
->will($this->throwException(new InvalidArgumentException)); ->will($this->throwException(new InvalidArgumentException));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
->with($this->callback(function ($searchRequest) { ->with($this->callback(function ($searchRequest) {
return $searchRequest->getLimit() === 10; return $searchRequest->getLimit() === 10;
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest); $this->subject->search($searchRequest);
@ -107,20 +142,23 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function configuredFilterAreAddedToRequestWithoutAnyFilter() public function configuredFilterAreAddedToRequestWithoutAnyFilter()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null)); ->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter') ->will($this->onConsecutiveCalls(
->willReturn(['property' => 'something']); ['property' => 'something'],
$this->throwException(new InvalidArgumentException)
));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
->with($this->callback(function ($searchRequest) { ->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['property' => 'something']; return $searchRequest->getFilter() === ['property' => 'something'];
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->subject->search($searchRequest); $this->subject->search($searchRequest);
@ -131,14 +169,16 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function configuredFilterAreAddedToRequestWithExistingFilter() public function configuredFilterAreAddedToRequestWithExistingFilter()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null)); ->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter') ->will($this->onConsecutiveCalls(
->willReturn(['property' => 'something']); ['property' => 'something'],
$this->throwException(new InvalidArgumentException)
));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
@ -147,7 +187,8 @@ class SearchServiceTest extends AbstractUnitTestCase
'anotherProperty' => 'anything', 'anotherProperty' => 'anything',
'property' => 'something', 'property' => 'something',
]; ];
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']); $searchRequest->setFilter(['anotherProperty' => 'anything']);
@ -159,20 +200,20 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function nonConfiguredFilterIsNotChangingRequestWithExistingFilter() public function nonConfiguredFilterIsNotChangingRequestWithExistingFilter()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null)); ->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter')
->will($this->throwException(new InvalidArgumentException)); ->will($this->throwException(new InvalidArgumentException));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
->with($this->callback(function ($searchRequest) { ->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['anotherProperty' => 'anything']; return $searchRequest->getFilter() === ['anotherProperty' => 'anything'];
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']); $searchRequest->setFilter(['anotherProperty' => 'anything']);
@ -184,23 +225,101 @@ class SearchServiceTest extends AbstractUnitTestCase
*/ */
public function emptyConfiguredFilterIsNotChangingRequestWithExistingFilter() public function emptyConfiguredFilterIsNotChangingRequestWithExistingFilter()
{ {
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->withConsecutive(['searching.size'], ['searching.facets']) ->withConsecutive(['searching.size'], ['searching.facets'])
->will($this->onConsecutiveCalls(null, null)); ->will($this->onConsecutiveCalls(null, null));
$this->configuration->expects($this->exactly(1)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->with('searching.filter') ->will($this->onConsecutiveCalls(
->willReturn(['anotherProperty' => '']); ['anotherProperty' => ''],
$this->throwException(new InvalidArgumentException)
));
$this->connection->expects($this->once()) $this->connection->expects($this->once())
->method('search') ->method('search')
->with($this->callback(function ($searchRequest) { ->with($this->callback(function ($searchRequest) {
return $searchRequest->getFilter() === ['anotherProperty' => 'anything']; return $searchRequest->getFilter() === ['anotherProperty' => 'anything'];
})); }))
->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock());
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['anotherProperty' => 'anything']); $searchRequest->setFilter(['anotherProperty' => 'anything']);
$this->subject->search($searchRequest); $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'
);
}
} }

View file

@ -15,7 +15,7 @@
"TYPO3\\CMS\\Core\\Tests\\": ".Build/vendor/typo3/cms/typo3/sysext/core/Tests/" "TYPO3\\CMS\\Core\\Tests\\": ".Build/vendor/typo3/cms/typo3/sysext/core/Tests/"
} }
}, },
"require" : { "require": {
"php": ">=7.0", "php": ">=7.0",
"typo3/cms": "~7.6", "typo3/cms": "~7.6",
"ruflin/elastica": "~3.2" "ruflin/elastica": "~3.2"

4
ext_conf_template.txt Normal file
View file

@ -0,0 +1,4 @@
disable {
# cat=basic/enable; type=boolean; label=Disable Elasticsearch, which is enabled by default
elasticsearch = true
}

View file

@ -37,11 +37,18 @@ call_user_func(
] ]
); );
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Object\Container\Container') // API does make use of object manager, therefore use GLOBALS
$extensionConfiguration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$extensionKey]);
if ($extensionConfiguration === false
|| !isset($extensionConfiguration['disable.']['elasticsearch'])
|| $extensionConfiguration['disable.']['elasticsearch'] !== '1'
) {
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\Container\Container::class)
->registerImplementation( ->registerImplementation(
'Codappix\SearchCore\Connection\ConnectionInterface', \Codappix\SearchCore\Connection\ConnectionInterface::class,
'Codappix\SearchCore\Connection\Elasticsearch' \Codappix\SearchCore\Connection\Elasticsearch::class
); );
}
}, },
$_EXTKEY $_EXTKEY
); );

23
phpcs.xml.dist Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<ruleset name="search_core">
<description>The coding standard for search_core.</description>
<file>Classes/</file>
<file>Tests/</file>
<!-- Set default settings -->
<arg value="sp"/>
<arg name="colors"/>
<arg name="encoding" value="utf-8" />
<arg name="extensions" value="php,php.dist,inc" />
<!-- Base rules -->
<rule ref="PSR2">
<!-- As it does not work with new array syntax. -->
<exclude name="Squiz.Arrays.ArrayBracketSpacing.SpaceBeforeBracket" />
</rule>
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
<!-- We have to follow the TYPO3 hook method names. -->
<exclude-pattern>Classes/Hook/DataHandler.php</exclude-pattern>
</rule>
</ruleset>