diff --git a/.travis.yml b/.travis.yml index 811b8f6..afd9e62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,7 @@ services: install: make install script: + - make cgl - make unitTests - make functionalTests diff --git a/Classes/Connection/Elasticsearch.php b/Classes/Connection/Elasticsearch.php index 8a3cb2b..b86bebf 100644 --- a/Classes/Connection/Elasticsearch.php +++ b/Classes/Connection/Elasticsearch.php @@ -132,7 +132,10 @@ class Elasticsearch implements Singleton, ConnectionInterface } ); } 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] + ); } } diff --git a/Classes/Connection/Elasticsearch/Facet.php b/Classes/Connection/Elasticsearch/Facet.php index e24a659..1142d88 100644 --- a/Classes/Connection/Elasticsearch/Facet.php +++ b/Classes/Connection/Elasticsearch/Facet.php @@ -45,25 +45,26 @@ class Facet implements FacetInterface */ protected $options = []; - public function __construct($name, array $aggregation, ConfigurationContainerInterface $configuration) + public function __construct(string $name, array $aggregation, ConfigurationContainerInterface $configuration) { $this->name = $name; $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; + } + } } - /** - * @return string - */ - public function getName() + public function getName() : string { return $this->name; } - /** - * @return string - */ - public function getField() + public function getField() : string { return $this->field; } @@ -73,7 +74,7 @@ class Facet implements FacetInterface * * @return array */ - public function getOptions() + public function getOptions() : array { $this->initOptions(); diff --git a/Classes/Connection/Elasticsearch/FacetOption.php b/Classes/Connection/Elasticsearch/FacetOption.php index 7359434..6b544bb 100644 --- a/Classes/Connection/Elasticsearch/FacetOption.php +++ b/Classes/Connection/Elasticsearch/FacetOption.php @@ -29,6 +29,11 @@ class FacetOption implements FacetOptionInterface */ protected $name = ''; + /** + * @var string + */ + protected $displayName = ''; + /** * @var int */ @@ -40,21 +45,21 @@ class FacetOption implements FacetOptionInterface public function __construct(array $bucket) { $this->name = $bucket['key']; + $this->displayName = isset($bucket['key_as_string']) ? $bucket['key_as_string'] : $this->getName(); $this->count = $bucket['doc_count']; } - /** - * @return string - */ - public function getName() + public function getName() : string { return $this->name; } - /** - * @return int - */ - public function getCount() + public function getDisplayName() : string + { + return $this->displayName; + } + + public function getCount() : int { return $this->count; } diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 3919722..867d823 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -24,10 +24,14 @@ use Codappix\SearchCore\Connection\FacetInterface; use Codappix\SearchCore\Connection\ResultItemInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Domain\Model\QueryResultInterfaceStub; +use Codappix\SearchCore\Domain\Model\ResultItem; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; class SearchResult implements SearchResultInterface { + use QueryResultInterfaceStub; + /** * @var SearchRequestInterface */ @@ -104,7 +108,7 @@ class SearchResult implements SearchResultInterface } foreach ($this->result->getResults() as $result) { - $this->results[] = new ResultItem($result); + $this->results[] = new ResultItem($result->getData()); } } @@ -153,41 +157,8 @@ class SearchResult implements SearchResultInterface $this->position = 0; } - // Extbase QueryResultInterface - Implemented to support Pagination of Fluid. - public function getQuery() { return $this->searchRequest; } - - public function getFirst() - { - throw new \BadMethodCallException('Method is not implemented yet.', 1502195121); - } - - public function toArray() - { - throw new \BadMethodCallException('Method is not implemented yet.', 1502195135); - } - - public function offsetExists($offset) - { - // Return false to allow Fluid to use appropriate getter methods. - return false; - } - - public function offsetGet($offset) - { - throw new \BadMethodCallException('Use getter to fetch properties.', 1502196933); - } - - public function offsetSet($offset, $value) - { - throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196934); - } - - public function offsetUnset($offset) - { - throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196936); - } } diff --git a/Classes/Connection/FacetOptionInterface.php b/Classes/Connection/FacetOptionInterface.php index 51a1efd..bf998db 100644 --- a/Classes/Connection/FacetOptionInterface.php +++ b/Classes/Connection/FacetOptionInterface.php @@ -28,15 +28,17 @@ interface FacetOptionInterface /** * Returns the name of this option. Equivalent * 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. - * - * @return int */ - public function getCount(); + public function getCount() : int; } diff --git a/Classes/Connection/FacetRequestInterface.php b/Classes/Connection/FacetRequestInterface.php index 9352f96..cdc16b1 100644 --- a/Classes/Connection/FacetRequestInterface.php +++ b/Classes/Connection/FacetRequestInterface.php @@ -28,15 +28,11 @@ interface FacetRequestInterface /** * The identifier of the facet, used as key in arrays and to get the facet * from search request, etc. - * - * @return string */ - public function getIdentifier(); + public function getIdentifier() : string; /** - * The field to use for facet building. - * - * @return string + * The config to use for facet building. */ - public function getField(); + public function getConfig() : array; } diff --git a/Classes/Connection/ResultItemInterface.php b/Classes/Connection/ResultItemInterface.php index 56add2a..d7fe1a9 100644 --- a/Classes/Connection/ResultItemInterface.php +++ b/Classes/Connection/ResultItemInterface.php @@ -25,5 +25,12 @@ namespace Codappix\SearchCore\Connection; */ interface ResultItemInterface extends \ArrayAccess { - + /** + * Returns every information as array. + * + * Provide key/column/field => data. + * + * Used e.g. for dataprocessing. + */ + public function getPlainData() : array; } diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index 7c7956e..e24adc3 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -20,6 +20,7 @@ namespace Codappix\SearchCore\Connection; * 02110-1301, USA. */ +use Codappix\SearchCore\Domain\Search\SearchService; use TYPO3\CMS\Extbase\Persistence\QueryInterface; interface SearchRequestInterface extends QueryInterface @@ -40,4 +41,10 @@ interface SearchRequestInterface extends QueryInterface * @return array */ public function getFilter(); + + /** + * Workaround for paginate widget support which will + * use the request to build another search. + */ + public function setSearchService(SearchService $searchService); } diff --git a/Classes/DataProcessing/ContentObjectDataProcessorAdapterProcessor.php b/Classes/DataProcessing/ContentObjectDataProcessorAdapterProcessor.php new file mode 100644 index 0000000..322486b --- /dev/null +++ b/Classes/DataProcessing/ContentObjectDataProcessorAdapterProcessor.php @@ -0,0 +1,59 @@ + + * + * 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 + ); + } +} diff --git a/Classes/DataProcessing/CopyToProcessor.php b/Classes/DataProcessing/CopyToProcessor.php index 2df7438..0853cfb 100644 --- a/Classes/DataProcessing/CopyToProcessor.php +++ b/Classes/DataProcessing/CopyToProcessor.php @@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing; */ class CopyToProcessor implements ProcessorInterface { - public function processRecord(array $record, array $configuration) : array + public function processData(array $record, array $configuration) : array { $all = []; diff --git a/Classes/DataProcessing/GeoPointProcessor.php b/Classes/DataProcessing/GeoPointProcessor.php index f9256b3..c5b6d18 100644 --- a/Classes/DataProcessing/GeoPointProcessor.php +++ b/Classes/DataProcessing/GeoPointProcessor.php @@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing; */ class GeoPointProcessor implements ProcessorInterface { - public function processRecord(array $record, array $configuration) : array + public function processData(array $record, array $configuration) : array { if (! $this->canApply($record, $configuration)) { return $record; diff --git a/Classes/DataProcessing/ProcessorInterface.php b/Classes/DataProcessing/ProcessorInterface.php index ef051f4..f7513f2 100644 --- a/Classes/DataProcessing/ProcessorInterface.php +++ b/Classes/DataProcessing/ProcessorInterface.php @@ -21,14 +21,13 @@ namespace Codappix\SearchCore\DataProcessing; */ /** - * All DataProcessing Processors should implement this interface, otherwise - * they will not be executed. + * All DataProcessing Processors should implement this interface. */ interface ProcessorInterface { /** - * Processes the given record. + * Processes the given data. * Also retrieves the configuration for this processor instance. */ - public function processRecord(array $record, array $configuration) : array; + public function processData(array $record, array $configuration) : array; } diff --git a/Classes/DataProcessing/RemoveProcessor.php b/Classes/DataProcessing/RemoveProcessor.php index 6e74237..b8d6283 100644 --- a/Classes/DataProcessing/RemoveProcessor.php +++ b/Classes/DataProcessing/RemoveProcessor.php @@ -27,7 +27,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; */ class RemoveProcessor implements ProcessorInterface { - public function processRecord(array $record, array $configuration) : array + public function processData(array $record, array $configuration) : array { if (!isset($configuration['fields'])) { return $record; diff --git a/Classes/DataProcessing/Service.php b/Classes/DataProcessing/Service.php new file mode 100644 index 0000000..3e3b053 --- /dev/null +++ b/Classes/DataProcessing/Service.php @@ -0,0 +1,56 @@ + + * + * 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); + } +} diff --git a/Classes/Domain/Index/AbstractIndexer.php b/Classes/Domain/Index/AbstractIndexer.php index 4ccb80e..431fb4d 100644 --- a/Classes/Domain/Index/AbstractIndexer.php +++ b/Classes/Domain/Index/AbstractIndexer.php @@ -43,6 +43,12 @@ abstract class AbstractIndexer implements IndexerInterface */ protected $identifier = ''; + /** + * @var \Codappix\SearchCore\DataProcessing\Service + * @inject + */ + protected $dataProcessorService; + /** * @var \TYPO3\CMS\Core\Log\Logger */ @@ -135,18 +141,7 @@ abstract class AbstractIndexer implements IndexerInterface { try { foreach ($this->configuration->get('indexing.' . $this->identifier . '.dataProcessing') as $configuration) { - $className = ''; - if (is_string($configuration)) { - $className = $configuration; - $configuration = []; - } else { - $className = $configuration['_typoScriptNodeValue']; - } - - $dataProcessor = GeneralUtility::makeInstance($className); - if ($dataProcessor instanceof ProcessorInterface) { - $record = $dataProcessor->processRecord($record, $configuration); - } + $record = $this->dataProcessorService->executeDataProcessor($configuration, $record); } } catch (InvalidArgumentException $e) { // Nothing to do. diff --git a/Classes/Domain/Index/TcaIndexer/TcaTableService.php b/Classes/Domain/Index/TcaIndexer/TcaTableService.php index 4b1f5b9..beab49b 100644 --- a/Classes/Domain/Index/TcaIndexer/TcaTableService.php +++ b/Classes/Domain/Index/TcaIndexer/TcaTableService.php @@ -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)) { $whereClause .= ' AND ' . $userDefinedWhere; } @@ -348,6 +350,9 @@ class TcaTableService */ protected function getBlackListedRootLine() : array { - return GeneralUtility::intExplode(',', $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist')); + return GeneralUtility::intExplode( + ',', + $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist') + ); } } diff --git a/Classes/Domain/Model/FacetRequest.php b/Classes/Domain/Model/FacetRequest.php index b1dbc4c..0d82ad3 100644 --- a/Classes/Domain/Model/FacetRequest.php +++ b/Classes/Domain/Model/FacetRequest.php @@ -30,37 +30,27 @@ class FacetRequest implements FacetRequestInterface 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 * 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->field = $field; + $this->config = $config; } - /** - * @return string - */ - public function getIdentifier() + public function getIdentifier() : string { return $this->identifier; } - /** - * @return string - */ - public function getField() + public function getConfig() : array { - return $this->field; + return $this->config; } } diff --git a/Classes/Domain/Model/QueryResultInterfaceStub.php b/Classes/Domain/Model/QueryResultInterfaceStub.php new file mode 100644 index 0000000..960fd40 --- /dev/null +++ b/Classes/Domain/Model/QueryResultInterfaceStub.php @@ -0,0 +1,61 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * As we have to stay compatible with QueryResultInterface + * of extbase but can and need not to provide all methods, + * this stub will provde the non implemented methods to + * keep real implementations clean. + */ +trait QueryResultInterfaceStub +{ + public function getFirst() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195121); + } + + public function toArray() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195135); + } + + public function offsetExists($offset) + { + // Return false to allow Fluid to use appropriate getter methods. + return false; + } + + public function offsetGet($offset) + { + throw new \BadMethodCallException('Use getter to fetch properties.', 1502196933); + } + + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196934); + } + + public function offsetUnset($offset) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196936); + } +} diff --git a/Classes/Connection/Elasticsearch/ResultItem.php b/Classes/Domain/Model/ResultItem.php similarity index 87% rename from Classes/Connection/Elasticsearch/ResultItem.php rename to Classes/Domain/Model/ResultItem.php index e56783c..d0e370a 100644 --- a/Classes/Connection/Elasticsearch/ResultItem.php +++ b/Classes/Domain/Model/ResultItem.php @@ -1,5 +1,5 @@ @@ -29,9 +29,14 @@ class ResultItem implements ResultItemInterface */ protected $data = []; - public function __construct(\Elastica\Result $result) + public function __construct(array $result) { - $this->data = $result->getData(); + $this->data = $result; + } + + public function getPlainData() : array + { + return $this->data; } public function offsetExists($offset) diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index d751ce8..69237bb 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -23,6 +23,7 @@ namespace Codappix\SearchCore\Domain\Model; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\FacetRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; +use Codappix\SearchCore\Domain\Search\SearchService; /** * Represents a search request used to process an actual search. @@ -63,6 +64,11 @@ class SearchRequest implements SearchRequestInterface */ protected $connection = null; + /** + * @var SearchService + */ + protected $searchService = null; + /** * @param string $query */ @@ -143,18 +149,29 @@ class SearchRequest implements SearchRequestInterface $this->connection = $connection; } + public function setSearchService(SearchService $searchService) + { + $this->searchService = $searchService; + } + // Extbase QueryInterface // Current implementation covers only paginate widget support. public function execute($returnRawQueryResult = false) { - if ($this->connection instanceof ConnectionInterface) { - return $this->connection->search($this); + if (! ($this->connection instanceof ConnectionInterface)) { + throw new \InvalidArgumentException( + 'Connection was not set before, therefore execute can not work. Use `setConnection` before.', + 1502197732 + ); + } + if (! ($this->searchService instanceof SearchService)) { + throw new \InvalidArgumentException( + 'SearchService was not set before, therefore execute can not work. Use `setSearchService` before.', + 1520325175 + ); } - throw new \InvalidArgumentException( - 'Connection was not set before, therefore execute can not work. Use `setConnection` before.', - 1502197732 - ); + return $this->searchService->processResult($this->connection->search($this)); } public function setLimit($limit) diff --git a/Classes/Domain/Model/SearchResult.php b/Classes/Domain/Model/SearchResult.php new file mode 100644 index 0000000..d91820d --- /dev/null +++ b/Classes/Domain/Model/SearchResult.php @@ -0,0 +1,129 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Codappix\SearchCore\Connection\ResultItemInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Domain\Model\QueryResultInterfaceStub; + +/** + * Generic model for mapping a concrete search result from a connection. + */ +class SearchResult implements SearchResultInterface +{ + use QueryResultInterfaceStub; + + /** + * @var SearchResultInterface + */ + protected $originalSearchResult; + + /** + * @var array + */ + protected $resultItems = []; + + /** + * @var array + */ + protected $results = []; + + /** + * For Iterator interface. + * + * @var int + */ + protected $position = 0; + + public function __construct(SearchResultInterface $originalSearchResult, array $resultItems) + { + $this->originalSearchResult = $originalSearchResult; + $this->resultItems = $resultItems; + } + + /** + * @return array + */ + public function getResults() + { + $this->initResults(); + + return $this->results; + } + + protected function initResults() + { + if ($this->results !== []) { + return; + } + + foreach ($this->resultItems as $item) { + $this->results[] = new ResultItem($item); + } + } + + public function getFacets() + { + return $this->originalSearchResult->getFacets(); + } + + public function getCurrentCount() + { + return $this->originalSearchResult->getCurrentCount(); + } + + public function count() + { + return $this->originalSearchResult->count(); + } + + public function current() + { + return $this->getResults()[$this->position]; + } + + public function next() + { + ++$this->position; + + return $this->current(); + } + + public function key() + { + return $this->position; + } + + public function valid() + { + return isset($this->getResults()[$this->position]); + } + + public function rewind() + { + $this->position = 0; + } + + public function getQuery() + { + return $this->originalSearchResult->getQuery(); + } +} diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index e193e64..23e93a1 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -120,6 +120,10 @@ class QueryFactory return; } + if (trim($searchRequest->getSearchTerm()) === '') { + return; + } + $boostQueryParts = []; foreach ($fields as $fieldName => $boostValue) { @@ -162,7 +166,11 @@ class QueryFactory { try { $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) { // Nothing configured @@ -170,7 +178,10 @@ class QueryFactory try { $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); if ($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]; } @@ -240,11 +263,7 @@ class QueryFactory foreach ($searchRequest->getFacets() as $facet) { $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ 'aggs' => [ - $facet->getIdentifier() => [ - 'terms' => [ - 'field' => $facet->getField(), - ], - ], + $facet->getIdentifier() => $facet->getConfig(), ], ]); } diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index 120ab3b..03fc11b 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -25,7 +25,9 @@ use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Model\FacetRequest; +use Codappix\SearchCore\Domain\Model\SearchResult; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; @@ -49,19 +51,27 @@ class SearchService */ protected $objectManager; + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + /** * @param ConnectionInterface $connection * @param ConfigurationContainerInterface $configuration * @param ObjectManagerInterface $objectManager + * @param DataProcessorService $dataProcessorService */ public function __construct( ConnectionInterface $connection, ConfigurationContainerInterface $configuration, - ObjectManagerInterface $objectManager + ObjectManagerInterface $objectManager, + DataProcessorService $dataProcessorService ) { $this->connection = $connection; $this->configuration = $configuration; $this->objectManager = $objectManager; + $this->dataProcessorService = $dataProcessorService; } /** @@ -70,12 +80,15 @@ class SearchService */ public function search(SearchRequestInterface $searchRequest) { - $searchRequest->setConnection($this->connection); $this->addSize($searchRequest); $this->addConfiguredFacets($searchRequest); $this->addConfiguredFilters($searchRequest); - return $this->connection->search($searchRequest); + // Add connection to request to enable paginate widget support + $searchRequest->setConnection($this->connection); + $searchRequest->setSearchService($this); + + return $this->processResult($this->connection->search($searchRequest)); } /** @@ -103,15 +116,10 @@ class SearchService } 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( FacetRequest::class, $identifier, - $facetConfig['field'] + $facetConfig )); } } @@ -138,4 +146,30 @@ class SearchService // Nothing todo, no filter configured. } } + + /** + * Processes the result, e.g. applies configured data processing to result. + */ + public function processResult(SearchResultInterface $searchResult) : SearchResultInterface + { + try { + $newSearchResultItems = []; + foreach ($this->configuration->get('searching.dataProcessing') as $configuration) { + foreach ($searchResult as $resultItem) { + $newSearchResultItems[] = $this->dataProcessorService->executeDataProcessor( + $configuration, + $resultItem->getPlainData() + ); + } + } + + return $this->objectManager->get( + SearchResult::class, + $searchResult, + $newSearchResultItems + ); + } catch (InvalidArgumentException $e) { + return $searchResult; + } + } } diff --git a/Classes/Domain/Service/DataHandler.php b/Classes/Domain/Service/DataHandler.php index 4b0386e..edb6386 100644 --- a/Classes/Domain/Service/DataHandler.php +++ b/Classes/Domain/Service/DataHandler.php @@ -82,16 +82,6 @@ class DataHandler implements Singleton $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 */ diff --git a/Classes/Hook/DataHandler.php b/Classes/Hook/DataHandler.php index 3c4902a..22f1175 100644 --- a/Classes/Hook/DataHandler.php +++ b/Classes/Hook/DataHandler.php @@ -91,42 +91,36 @@ class DataHandler implements Singleton return true; } - /** - * Called by CoreDataHandler on database operations, e.g. if new records were created or records were updated. - * - * @param string $status - * @param string $table - * @param string|int $uid - * @param array $fieldArray - * @param CoreDataHandler $dataHandler - * - * @return bool False if hook was not processed. - */ - public function processDatamap_afterDatabaseOperations($status, $table, $uid, array $fieldArray, CoreDataHandler $dataHandler) + public function processDatamap_afterAllOperations(CoreDataHandler $dataHandler) + { + foreach ($dataHandler->datamap as $table => $record) { + $uid = key($record); + $fieldData = current($record); + + if (isset($fieldArray['uid'])) { + $uid = $fieldArray['uid']; + } elseif (isset($dataHandler->substNEWwithIDs[$uid])) { + $uid = $dataHandler->substNEWwithIDs[$uid]; + } + + $this->processRecord($table, $uid); + } + } + + protected function processRecord(string $table, int $uid) : bool { 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; } - if ($status === 'new') { - $fieldArray['uid'] = $dataHandler->substNEWwithIDs[$uid]; - $this->dataHandler->add($table, $fieldArray); + $record = $this->getRecord($table, $uid); + if ($record !== null) { + $this->dataHandler->update($table, $record); return true; } - if ($status === 'update') { - $record = $this->getRecord($table, $uid); - if ($record !== null) { - $this->dataHandler->update($table, $record); - } - return true; - } - - $this->logger->debug( - 'Database update not processed, cause status is unhandled.', - [$status, $table, $uid, $fieldArray] - ); + $this->logger->debug('Indexing of record not processed, as he was not found in Database.', [$table, $uid]); return false; } diff --git a/Documentation/source/changelog.rst b/Documentation/source/changelog.rst new file mode 100644 index 0000000..0d7f020 --- /dev/null +++ b/Documentation/source/changelog.rst @@ -0,0 +1,8 @@ +Changelog +========= + +.. toctree:: + :maxdepth: 1 + :glob: + + changelog/* diff --git a/Documentation/source/changelog/20180406-120-facet-configuration.rst b/Documentation/source/changelog/20180406-120-facet-configuration.rst new file mode 100644 index 0000000..53021d9 --- /dev/null +++ b/Documentation/source/changelog/20180406-120-facet-configuration.rst @@ -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`. diff --git a/Documentation/source/concepts.rst b/Documentation/source/concepts.rst index 0fe6c5d..f4c5bbd 100644 --- a/Documentation/source/concepts.rst +++ b/Documentation/source/concepts.rst @@ -36,5 +36,7 @@ DataProcessing Before data is transfered to search service, it can be processed by "DataProcessors" like already known by :ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing` of :ref:`t3tsref:cobj-fluidtemplate`. +The same is true for retrieved search results. They can be processed again by "DataProcessors" to +prepare data for display in Templates or further usage. Configuration is done through TypoScript, see :ref:`dataProcessing`. diff --git a/Documentation/source/configuration.rst b/Documentation/source/configuration.rst index eca5ba4..09289ac 100644 --- a/Documentation/source/configuration.rst +++ b/Documentation/source/configuration.rst @@ -5,9 +5,12 @@ 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 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 diff --git a/Documentation/source/configuration/dataProcessing/ContentObjectDataProcessorAdapterProcessor.rst b/Documentation/source/configuration/dataProcessing/ContentObjectDataProcessorAdapterProcessor.rst new file mode 100644 index 0000000..7d8f0b2 --- /dev/null +++ b/Documentation/source/configuration/dataProcessing/ContentObjectDataProcessorAdapterProcessor.rst @@ -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. diff --git a/Documentation/source/configuration/dataProcessing/availableAndPlanned.rst b/Documentation/source/configuration/dataProcessing/availableAndPlanned.rst new file mode 100644 index 0000000..9f31736 --- /dev/null +++ b/Documentation/source/configuration/dataProcessing/availableAndPlanned.rst @@ -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``. diff --git a/Documentation/source/configuration/indexing.rst b/Documentation/source/configuration/indexing.rst index b605008..7d6c3d4 100644 --- a/Documentation/source/configuration/indexing.rst +++ b/Documentation/source/configuration/indexing.rst @@ -146,7 +146,7 @@ Example:: dataProcessing -------------- -Used by: All connections while indexing. +Used by: All connections while indexing, due to implementation inside ``AbstractIndexer``. Configure modifications on each document before sending it to the configured connection. Same as provided by TYPO3 for :ref:`t3tsref:cobj-fluidtemplate` through @@ -170,38 +170,7 @@ Example:: The above example will copy all existing fields to the field ``search_spellcheck``. Afterwards all fields, including ``search_spellcheck`` will be copied to ``search_all``. -E.g. used to index all information into a field for :ref:`spellchecking` or searching with -different :ref:`mapping`. -The following Processor are available: +.. include:: /configuration/dataProcessing/availableAndPlanned.rst -.. toctree:: - :maxdepth: 1 - :glob: - - dataProcessing/CopyToProcessor - dataProcessing/RemoveProcessor - dataProcessing/GeoPointProcessor - -The following Processor are planned: - - ``Codappix\SearchCore\DataProcessing\ReplaceProcessor`` - Will execute a search and replace on configured fields. - - ``Codappix\SearchCore\DataProcessing\RootLevelProcessor`` - Will attach the root level to the record. - - ``Codappix\SearchCore\DataProcessing\ChannelProcessor`` - Will add a configurable channel to the record, e.g. if you have different areas in your - website like "products" and "infos". - - ``Codappix\SearchCore\DataProcessing\RelationResolverProcessor`` - Resolves all relations using the TCA. - -Of course you are able to provide further processors. Just implement -``Codappix\SearchCore\DataProcessing\ProcessorInterface`` and use the FQCN (=Fully qualified -class name) as done in the examples above. - -By implementing also the same interface as necessary for TYPO3 -:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`, you are able to reuse the same code -also for Fluid to prepare the same record fetched from DB for your fluid. +Also data processors are available for search results too, see :ref:`searching_dataProcessing`. diff --git a/Documentation/source/configuration/searching.rst b/Documentation/source/configuration/searching.rst index ae73fad..857aa74 100644 --- a/Documentation/source/configuration/searching.rst +++ b/Documentation/source/configuration/searching.rst @@ -119,20 +119,48 @@ E.g. you submit a filter in form of: .. code-block:: html - - - + + Due to TYPO3 7.x fluid limitations, we build this input ourself. + No longer necessary in 8 and above + + + 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:: mapping { filter { - distance { - field = geo_distance + month { + type = range + field = released + raw { + format = yyyy-MM + } + fields { - distance = distance - location = location + gte = from + 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 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. +Everything that is configured inside ``raw`` is passed, as is, to search service, e.g. +elasticsearch. + .. _fields: fields @@ -216,3 +247,37 @@ Example:: } Only ``filter`` is allowed as value. Will submit an empty query to switch to filter mode. + +.. _searching_dataProcessing: + +dataProcessing +-------------- + +Used by: All connections while indexing, due to implementation inside ``SearchService``. + +Configure modifications on each document before returning search result. Same as provided by TYPO3 +for :ref:`t3tsref:cobj-fluidtemplate` through +:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`. + +All processors are applied in configured order. Allowing to work with already processed data. + +Example:: + + plugin.tx_searchcore.settings.searching.dataProcessing { + 1 = Codappix\SearchCore\DataProcessing\CopyToProcessor + 1 { + to = search_spellcheck + } + + 2 = Codappix\SearchCore\DataProcessing\CopyToProcessor + 2 { + to = search_all + } + } + +The above example will copy all existing fields to the field ``search_spellcheck``. Afterwards +all fields, including ``search_spellcheck`` will be copied to ``search_all``. + +.. include:: /configuration/dataProcessing/availableAndPlanned.rst + +Also data processors are available while indexing too, see :ref:`dataProcessing`. diff --git a/Documentation/source/features.rst b/Documentation/source/features.rst index 8baf453..1a4abd6 100644 --- a/Documentation/source/features.rst +++ b/Documentation/source/features.rst @@ -29,6 +29,16 @@ Also multiple filter are supported. Filtering results by fields for string conte Facets / aggregates are also possible. Therefore a mapping has to be defined in TypoScript for indexing, and the facets itself while searching. +.. _features_dataProcessing: + +DataProcessing +============== + +DataProcessing, as known from ``FLUIDTEMPLATE`` is available while indexing and for search results. +Each item can be processed by multiple processor to prepare data for indexing and output. + +See :ref:`concepts_indexing_dataprocessing` in :ref:`concepts` section. + .. _features_planned: Planned diff --git a/Documentation/source/index.rst b/Documentation/source/index.rst index 5427fc9..2edfae3 100644 --- a/Documentation/source/index.rst +++ b/Documentation/source/index.rst @@ -15,3 +15,4 @@ Table of Contents connections indexer development + changelog diff --git a/Documentation/source/installation.rst b/Documentation/source/installation.rst index ddfb065..5bb0c56 100644 --- a/Documentation/source/installation.rst +++ b/Documentation/source/installation.rst @@ -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 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 diff --git a/Makefile b/Makefile index 72a1b9b..554c567 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ install: clean COMPOSER_PROCESS_TIMEOUT=1000 composer require -vv --dev --prefer-source typo3/cms="$(TYPO3_VERSION)" git checkout composer.json +cgl: + ./.Build/bin/phpcs + functionalTests: typo3DatabaseName=$(typo3DatabaseName) \ typo3DatabaseUsername=$(typo3DatabaseUsername) \ diff --git a/Tests/Functional/Connection/Elasticsearch/FacetTest.php b/Tests/Functional/Connection/Elasticsearch/FacetTest.php new file mode 100644 index 0000000..1c38c07 --- /dev/null +++ b/Tests/Functional/Connection/Elasticsearch/FacetTest.php @@ -0,0 +1,78 @@ + + * + * 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.'); + } +} diff --git a/Tests/Functional/Connection/Elasticsearch/FilterTest.php b/Tests/Functional/Connection/Elasticsearch/FilterTest.php index 2430833..75d400c 100644 --- a/Tests/Functional/Connection/Elasticsearch/FilterTest.php +++ b/Tests/Functional/Connection/Elasticsearch/FilterTest.php @@ -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(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.'); - } } diff --git a/Tests/Functional/DataProcessing/ContentObjectDataProcessorAdapterProcessorTest.php b/Tests/Functional/DataProcessing/ContentObjectDataProcessorAdapterProcessorTest.php new file mode 100644 index 0000000..871b205 --- /dev/null +++ b/Tests/Functional/DataProcessing/ContentObjectDataProcessorAdapterProcessorTest.php @@ -0,0 +1,55 @@ + + * + * 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.' + ); + } +} diff --git a/Tests/Functional/Fixtures/BasicSetup.ts b/Tests/Functional/Fixtures/BasicSetup.ts index 3551670..4129362 100644 --- a/Tests/Functional/Fixtures/BasicSetup.ts +++ b/Tests/Functional/Fixtures/BasicSetup.ts @@ -37,13 +37,6 @@ plugin { } searching { - fields = search_title - facets { - contentTypes { - field = CType - } - } - fields { query = _all } diff --git a/Tests/Functional/Fixtures/Searching/Facet.ts b/Tests/Functional/Fixtures/Searching/Facet.ts new file mode 100644 index 0000000..10eeae6 --- /dev/null +++ b/Tests/Functional/Fixtures/Searching/Facet.ts @@ -0,0 +1,17 @@ +plugin { + tx_searchcore { + settings { + searching { + facets { + contentTypes { + terms { + field = CType + } + } + } + } + } + } +} + +module.tx_searchcore < plugin.tx_searchcore diff --git a/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php b/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php deleted file mode 100644 index b1676e3..0000000 --- a/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * 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".' - ); - } -} diff --git a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php index c33701d..509f053 100644 --- a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php +++ b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php @@ -64,7 +64,7 @@ class NonAllowedTablesTest extends AbstractDataHandlerTest /** * @test */ - public function updateWillNotBeTriggeredForSysCategory() + public function updateWillNotBeTriggeredForExistingSysCategory() { $this->subject->expects($this->exactly(0))->method('update'); @@ -83,9 +83,9 @@ class NonAllowedTablesTest extends AbstractDataHandlerTest /** * @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->stripslashes_values = 0; diff --git a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php index 6276616..ee3eb05 100644 --- a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php +++ b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php @@ -66,7 +66,7 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest /** * @test */ - public function updateWillBeTriggeredForTtContent() + public function updateWillBeTriggeredForExistingTtContent() { $this->subject->expects($this->exactly(1))->method('update') ->with( @@ -94,9 +94,9 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest /** * @test */ - public function addWillBeTriggeredForTtContent() + public function updateWillBeTriggeredForNewTtContent() { - $this->subject->expects($this->exactly(1))->method('add') + $this->subject->expects($this->exactly(1))->method('update') ->with( $this->equalTo('tt_content'), $this->callback(function ($record) { diff --git a/Tests/Unit/Configuration/ConfigurationUtilityTest.php b/Tests/Unit/Configuration/ConfigurationUtilityTest.php index b2dc5f8..4db367c 100644 --- a/Tests/Unit/Configuration/ConfigurationUtilityTest.php +++ b/Tests/Unit/Configuration/ConfigurationUtilityTest.php @@ -31,8 +31,11 @@ class ConfigurationUtilityTest extends AbstractUnitTestCase * @test * @dataProvider possibleRequestAndConfigurationForFluidtemplate */ - public function recursiveEntriesAreProcessedAsFluidtemplate(SearchRequestInterface $searchRequest, array $array, array $expected) - { + public function recursiveEntriesAreProcessedAsFluidtemplate( + SearchRequestInterface $searchRequest, + array $array, + array $expected + ) { $subject = new ConfigurationUtility(); $this->assertSame( diff --git a/Tests/Unit/Connection/Elasticsearch/FacetOptionTest.php b/Tests/Unit/Connection/Elasticsearch/FacetOptionTest.php new file mode 100644 index 0000000..696762c --- /dev/null +++ b/Tests/Unit/Connection/Elasticsearch/FacetOptionTest.php @@ -0,0 +1,64 @@ + + * + * 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.' + ); + } +} diff --git a/Tests/Unit/DataProcessing/CopyToProcessorTest.php b/Tests/Unit/DataProcessing/CopyToProcessorTest.php index 2f9e498..28545a3 100644 --- a/Tests/Unit/DataProcessing/CopyToProcessorTest.php +++ b/Tests/Unit/DataProcessing/CopyToProcessorTest.php @@ -27,15 +27,15 @@ class CopyToProcessorTest extends AbstractUnitTestCase { /** * @test - * @dataProvider getPossibleRecordConfigurationCombinations + * @dataProvider getPossibleDataConfigurationCombinations */ - public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedRecord) + public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedData) { $subject = new CopyToProcessor(); - $processedRecord = $subject->processRecord($record, $configuration); + $processedData = $subject->processData($record, $configuration); $this->assertSame( - $expectedRecord, - $processedRecord, + $expectedData, + $processedData, 'The processor did not return the expected processed record.' ); } @@ -43,7 +43,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase /** * @return array */ - public function getPossibleRecordConfigurationCombinations() + public function getPossibleDataConfigurationCombinations() { return [ 'Copy all fields to new field' => [ @@ -54,7 +54,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase 'configuration' => [ 'to' => 'new_field', ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', 'field 2' => 'Some more content like ipsum', 'new_field' => 'Some content like lorem' . PHP_EOL . 'Some more content like ipsum', @@ -71,7 +71,7 @@ class CopyToProcessorTest extends AbstractUnitTestCase 'configuration' => [ 'to' => 'new_field', ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', 'field with sub2' => [ 'Tag 1', diff --git a/Tests/Unit/DataProcessing/GeoPointProcessorTest.php b/Tests/Unit/DataProcessing/GeoPointProcessorTest.php index 99b8eb3..02db659 100644 --- a/Tests/Unit/DataProcessing/GeoPointProcessorTest.php +++ b/Tests/Unit/DataProcessing/GeoPointProcessorTest.php @@ -27,15 +27,15 @@ class GeoPointProcessorTest extends AbstractUnitTestCase { /** * @test - * @dataProvider getPossibleRecordConfigurationCombinations + * @dataProvider getPossibleDataConfigurationCombinations */ - public function geoPointsAreAddedAsConfigured(array $record, array $configuration, array $expectedRecord) + public function geoPointsAreAddedAsConfigured(array $record, array $configuration, array $expectedData) { $subject = new GeoPointProcessor(); - $processedRecord = $subject->processRecord($record, $configuration); + $processedData = $subject->processData($record, $configuration); $this->assertSame( - $expectedRecord, - $processedRecord, + $expectedData, + $processedData, 'The processor did not return the expected processed record.' ); } @@ -43,7 +43,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase /** * @return array */ - public function getPossibleRecordConfigurationCombinations() + public function getPossibleDataConfigurationCombinations() { return [ 'Create new field with existing lat and lng' => [ @@ -56,7 +56,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase 'lat' => 'lat', 'lon' => 'lng', ], - 'expectedRecord' => [ + 'expectedData' => [ 'lat' => 23.232, 'lng' => 45.43, 'location' => [ @@ -73,7 +73,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase 'configuration' => [ 'to' => 'location', ], - 'expectedRecord' => [ + 'expectedData' => [ 'lat' => 23.232, 'lng' => 45.43, ], @@ -88,7 +88,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase 'lat' => 'lat', 'lon' => 'lng', ], - 'expectedRecord' => [ + 'expectedData' => [ 'lat' => '', 'lng' => '', ], @@ -103,7 +103,7 @@ class GeoPointProcessorTest extends AbstractUnitTestCase 'lat' => 'lat', 'lon' => 'lng', ], - 'expectedRecord' => [ + 'expectedData' => [ 'lat' => 'av', 'lng' => 'dsf', ], diff --git a/Tests/Unit/DataProcessing/RemoveProcessorTest.php b/Tests/Unit/DataProcessing/RemoveProcessorTest.php index dc55f73..cd23d16 100644 --- a/Tests/Unit/DataProcessing/RemoveProcessorTest.php +++ b/Tests/Unit/DataProcessing/RemoveProcessorTest.php @@ -27,15 +27,15 @@ class RemoveProcessorTest extends AbstractUnitTestCase { /** * @test - * @dataProvider getPossibleRecordConfigurationCombinations + * @dataProvider getPossibleDataConfigurationCombinations */ - public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedRecord) + public function fieldsAreCopiedAsConfigured(array $record, array $configuration, array $expectedData) { $subject = new RemoveProcessor(); - $processedRecord = $subject->processRecord($record, $configuration); + $processedData = $subject->processData($record, $configuration); $this->assertSame( - $expectedRecord, - $processedRecord, + $expectedData, + $processedData, 'The processor did not return the expected processed record.' ); } @@ -43,7 +43,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase /** * @return array */ - public function getPossibleRecordConfigurationCombinations() + public function getPossibleDataConfigurationCombinations() { return [ 'Nothing configured' => [ @@ -56,7 +56,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase ], 'configuration' => [ ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', 'field with sub2' => [ 'Tag 1', @@ -76,7 +76,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase 'fields' => 'field with sub2', '_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor', ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', ], ], @@ -92,7 +92,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase 'fields' => 'non existing', '_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor', ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', 'field with sub2' => [ 'Tag 1', @@ -113,7 +113,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase 'fields' => 'field 3, field with sub2', '_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor', ], - 'expectedRecord' => [ + 'expectedData' => [ 'field 1' => 'Some content like lorem', ], ], @@ -125,7 +125,7 @@ class RemoveProcessorTest extends AbstractUnitTestCase 'fields' => 'field 1', '_typoScriptNodeValue' => 'Codappix\SearchCore\DataProcessing\RemoveProcessor', ], - 'expectedRecord' => [ + 'expectedData' => [ ], ], ]; diff --git a/Tests/Unit/Domain/Index/AbstractIndexerTest.php b/Tests/Unit/Domain/Index/AbstractIndexerTest.php index 3bbc97d..411cc24 100644 --- a/Tests/Unit/Domain/Index/AbstractIndexerTest.php +++ b/Tests/Unit/Domain/Index/AbstractIndexerTest.php @@ -24,6 +24,7 @@ use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\DataProcessing\CopyToProcessor; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Index\AbstractIndexer; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; @@ -44,17 +45,26 @@ class AbstractIndexerTest extends AbstractUnitTestCase */ protected $connection; + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + public function setUp() { parent::setUp(); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock(); + $this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class) + ->disableOriginalConstructor() + ->getMock(); $this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [ $this->connection, $this->configuration ]); + $this->inject($this->subject, 'dataProcessorService', $this->dataProcessorService); $this->subject->injectLogger($this->getMockedLogger()); $this->subject->setIdentifier('testTable'); $this->subject->expects($this->any()) @@ -73,7 +83,30 @@ class AbstractIndexerTest extends AbstractUnitTestCase $expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test'; $expectedRecord['search_abstract'] = ''; - $this->configuration->expects($this->exactly(2)) + $this->dataProcessorService->expects($this->any()) + ->method('executeDataProcessor') + ->withConsecutive( + [ + [ + '_typoScriptNodeValue' => CopyToProcessor::class, + 'to' => 'new_test_field', + ], + $record, + ], + [ + [ + '_typoScriptNodeValue' => CopyToProcessor::class, + 'to' => 'new_test_field2', + ], + array_merge($record, ['new_test_field' => 'test']), + ] + ) + ->will($this->onConsecutiveCalls( + array_merge($record, ['new_test_field' => 'test']), + $expectedRecord + )); + + $this->configuration->expects($this->any()) ->method('get') ->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields']) ->will($this->onConsecutiveCalls([ diff --git a/Tests/Unit/Domain/Model/ResultItemTest.php b/Tests/Unit/Domain/Model/ResultItemTest.php new file mode 100644 index 0000000..53477c7 --- /dev/null +++ b/Tests/Unit/Domain/Model/ResultItemTest.php @@ -0,0 +1,110 @@ + + * + * 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']); + } +} diff --git a/Tests/Unit/Domain/Model/SearchRequestTest.php b/Tests/Unit/Domain/Model/SearchRequestTest.php index 127baf2..28f90e3 100644 --- a/Tests/Unit/Domain/Model/SearchRequestTest.php +++ b/Tests/Unit/Domain/Model/SearchRequestTest.php @@ -20,7 +20,10 @@ namespace Codappix\SearchCore\Tests\Unit\Domain\Model; * 02110-1301, USA. */ +use Codappix\SearchCore\Connection\ConnectionInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; use Codappix\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Domain\Search\SearchService; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; class SearchRequestTest extends AbstractUnitTestCase @@ -31,12 +34,12 @@ class SearchRequestTest extends AbstractUnitTestCase */ public function emptyFilterWillNotBeSet(array $filter) { - $searchRequest = new SearchRequest(); - $searchRequest->setFilter($filter); + $subject = new SearchRequest(); + $subject->setFilter($filter); $this->assertSame( [], - $searchRequest->getFilter(), + $subject->getFilter(), 'Empty filter were set, even if they should not.' ); } @@ -68,13 +71,67 @@ class SearchRequestTest extends AbstractUnitTestCase public function filterIsSet() { $filter = ['someField' => 'someValue']; - $searchRequest = new SearchRequest(); - $searchRequest->setFilter($filter); + $subject = new SearchRequest(); + $subject->setFilter($filter); $this->assertSame( $filter, - $searchRequest->getFilter(), + $subject->getFilter(), 'Filter was not set.' ); } + + /** + * @test + */ + public function exceptionIsThrownIfSearchServiceWasNotSet() + { + $subject = new SearchRequest(); + $subject->setConnection($this->getMockBuilder(ConnectionInterface::class)->getMock()); + $this->expectException(\InvalidArgumentException::class); + $subject->execute(); + } + + /** + * @test + */ + public function exceptionIsThrownIfConnectionWasNotSet() + { + $subject = new SearchRequest(); + $subject->setSearchService( + $this->getMockBuilder(SearchService::class) + ->disableOriginalConstructor() + ->getMock() + ); + $this->expectException(\InvalidArgumentException::class); + $subject->execute(); + } + + /** + * @test + */ + public function executionMakesUseOfProvidedConnectionAndSearchService() + { + $searchServiceMock = $this->getMockBuilder(SearchService::class) + ->disableOriginalConstructor() + ->getMock(); + $connectionMock = $this->getMockBuilder(ConnectionInterface::class) + ->getMock(); + $searchResultMock = $this->getMockBuilder(SearchResultInterface::class) + ->getMock(); + + $subject = new SearchRequest(); + $subject->setSearchService($searchServiceMock); + $subject->setConnection($connectionMock); + + $connectionMock->expects($this->once()) + ->method('search') + ->with($subject) + ->willReturn($searchResultMock); + $searchServiceMock->expects($this->once()) + ->method('processResult') + ->with($searchResultMock); + + $subject->execute(); + } } diff --git a/Tests/Unit/Domain/Model/SearchResultTest.php b/Tests/Unit/Domain/Model/SearchResultTest.php new file mode 100644 index 0000000..eebb3c1 --- /dev/null +++ b/Tests/Unit/Domain/Model/SearchResultTest.php @@ -0,0 +1,100 @@ + + * + * 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]); + } +} diff --git a/Tests/Unit/Domain/Search/QueryFactoryTest.php b/Tests/Unit/Domain/Search/QueryFactoryTest.php index cf74ce3..9fa9c62 100644 --- a/Tests/Unit/Domain/Search/QueryFactoryTest.php +++ b/Tests/Unit/Domain/Search/QueryFactoryTest.php @@ -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 */ @@ -118,8 +170,8 @@ class QueryFactoryTest extends AbstractUnitTestCase { $this->configureConfigurationMockWithDefault(); $searchRequest = new SearchRequest('SearchWord'); - $searchRequest->addFacet(new FacetRequest('Identifier', 'FieldName')); - $searchRequest->addFacet(new FacetRequest('Identifier 2', 'FieldName 2')); + $searchRequest->addFacet(new FacetRequest('Identifier', ['terms' => ['field' => 'FieldName']])); + $searchRequest->addFacet(new FacetRequest('Identifier 2', ['terms' => ['field' => 'FieldName 2']])); $query = $this->subject->create($searchRequest); $this->assertSame( diff --git a/Tests/Unit/Domain/Search/SearchServiceTest.php b/Tests/Unit/Domain/Search/SearchServiceTest.php index c63e29e..3d6791e 100644 --- a/Tests/Unit/Domain/Search/SearchServiceTest.php +++ b/Tests/Unit/Domain/Search/SearchServiceTest.php @@ -1,5 +1,5 @@ @@ -23,7 +23,10 @@ namespace Copyright\SearchCore\Tests\Unit\Domain\Search; use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\DataProcessing\Service as DataProcessorService; use Codappix\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Domain\Model\SearchResult; use Codappix\SearchCore\Domain\Search\SearchService; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; @@ -35,10 +38,38 @@ class SearchServiceTest extends AbstractUnitTestCase */ protected $subject; + /** + * @var SearchResultInterface + */ + protected $result; + + /** + * @var ConnectionInterface + */ + protected $connection; + + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @var DataProcessorService + */ + protected $dataProcessorService; + public function setUp() { parent::setUp(); + $this->result = $this->getMockBuilder(SearchResultInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -48,11 +79,15 @@ class SearchServiceTest extends AbstractUnitTestCase $this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class) ->disableOriginalConstructor() ->getMock(); + $this->dataProcessorService = $this->getMockBuilder(DataProcessorService::class) + ->setConstructorArgs([$this->objectManager]) + ->getMock(); $this->subject = new SearchService( $this->connection, $this->configuration, - $this->objectManager + $this->objectManager, + $this->dataProcessorService ); } @@ -61,19 +96,19 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function sizeIsAddedFromConfiguration() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(45, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { return $searchRequest->getLimit() === 45; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $this->subject->search($searchRequest); @@ -84,19 +119,19 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function defaultSizeIsAddedIfNothingIsConfigured() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { return $searchRequest->getLimit() === 10; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $this->subject->search($searchRequest); @@ -107,20 +142,23 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function configuredFilterAreAddedToRequestWithoutAnyFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['property' => 'something']); + ->will($this->onConsecutiveCalls( + ['property' => 'something'], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { return $searchRequest->getFilter() === ['property' => 'something']; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $this->subject->search($searchRequest); @@ -131,14 +169,16 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function configuredFilterAreAddedToRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['property' => 'something']); + ->will($this->onConsecutiveCalls( + ['property' => 'something'], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search') @@ -147,7 +187,8 @@ class SearchServiceTest extends AbstractUnitTestCase 'anotherProperty' => 'anything', 'property' => 'something', ]; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $searchRequest->setFilter(['anotherProperty' => 'anything']); @@ -159,20 +200,20 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function nonConfiguredFilterIsNotChangingRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { return $searchRequest->getFilter() === ['anotherProperty' => 'anything']; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $searchRequest->setFilter(['anotherProperty' => 'anything']); @@ -184,23 +225,101 @@ class SearchServiceTest extends AbstractUnitTestCase */ public function emptyConfiguredFilterIsNotChangingRequestWithExistingFilter() { - $this->configuration->expects($this->exactly(2)) + $this->configuration->expects($this->any()) ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); - $this->configuration->expects($this->exactly(1)) + $this->configuration->expects($this->any()) ->method('get') - ->with('searching.filter') - ->willReturn(['anotherProperty' => '']); + ->will($this->onConsecutiveCalls( + ['anotherProperty' => ''], + $this->throwException(new InvalidArgumentException) + )); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { return $searchRequest->getFilter() === ['anotherProperty' => 'anything']; - })); + })) + ->willReturn($this->getMockBuilder(SearchResultInterface::class)->getMock()); $searchRequest = new SearchRequest('SearchWord'); $searchRequest->setFilter(['anotherProperty' => 'anything']); $this->subject->search($searchRequest); } + + /** + * @test + */ + public function originalSearchResultIsReturnedIfNoDataProcessorIsConfigured() + { + $this->configuration->expects($this->any()) + ->method('getIfExists') + ->withConsecutive(['searching.size'], ['searching.facets']) + ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->any()) + ->method('get') + ->will($this->throwException(new InvalidArgumentException)); + + $searchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock(); + + $this->connection->expects($this->once()) + ->method('search') + ->willReturn($searchResultMock); + + $this->dataProcessorService->expects($this->never())->method('executeDataProcessor'); + + $searchRequest = new SearchRequest(''); + $this->assertSame( + $searchResultMock, + $this->subject->search($searchRequest), + 'Did not get created result without applied data processing' + ); + } + + /** + * @test + */ + public function configuredDataProcessorsAreExecutedOnSearchResult() + { + $this->configuration->expects($this->any()) + ->method('getIfExists') + ->withConsecutive(['searching.size'], ['searching.facets']) + ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->any()) + ->method('get') + ->will($this->onConsecutiveCalls( + $this->throwException(new InvalidArgumentException), + ['SomeProcessorClass'] + )); + + $searchResultMock = $this->getMockBuilder(SearchResultInterface::class)->getMock(); + $searchResult = new SearchResult($searchResultMock, [['field 1' => 'value 1']]); + + $this->connection->expects($this->once()) + ->method('search') + ->willReturn($searchResult); + + $this->dataProcessorService->expects($this->once()) + ->method('executeDataProcessor') + ->with('SomeProcessorClass', ['field 1' => 'value 1']) + ->willReturn([ + 'field 1' => 'value 1', + 'field 2' => 'value 2', + ]); + + $this->objectManager->expects($this->once()) + ->method('get') + ->with(SearchResult::class, $searchResult, [ + ['field 1' => 'value 1', 'field 2' => 'value 2'] + ]) + ->willReturn($searchResultMock); + + $searchRequest = new SearchRequest(''); + $this->assertSame( + $searchResultMock, + $this->subject->search($searchRequest), + 'Did not get created result with applied data processing' + ); + } } diff --git a/composer.json b/composer.json index 96dc524..b95a0fb 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "TYPO3\\CMS\\Core\\Tests\\": ".Build/vendor/typo3/cms/typo3/sysext/core/Tests/" } }, - "require" : { + "require": { "php": ">=7.0", "typo3/cms": "~7.6", "ruflin/elastica": "~3.2" diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 0000000..5ddadb4 --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,4 @@ +disable { + # cat=basic/enable; type=boolean; label=Disable Elasticsearch, which is enabled by default + elasticsearch = true +} diff --git a/ext_localconf.php b/ext_localconf.php index 7743804..5b16e70 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -37,11 +37,18 @@ call_user_func( ] ); - \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Object\Container\Container') - ->registerImplementation( - 'Codappix\SearchCore\Connection\ConnectionInterface', - 'Codappix\SearchCore\Connection\Elasticsearch' - ); + // 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( + \Codappix\SearchCore\Connection\ConnectionInterface::class, + \Codappix\SearchCore\Connection\Elasticsearch::class + ); + } }, $_EXTKEY ); diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..e722968 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,23 @@ + + + The coding standard for search_core. + + Classes/ + Tests/ + + + + + + + + + + + + + + + Classes/Hook/DataHandler.php + +