diff --git a/.travis.yml b/.travis.yml index 79bf8f9..e265e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,15 @@ +sudo: true + +addons: + apt: + packages: + - oracle-java8-set-default +before_install: + - curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.0.deb && sudo dpkg -i --force-confnew elasticsearch-5.2.0.deb && sudo service elasticsearch start + language: php php: - - 5.6 - 7.0 - 7.1 @@ -16,18 +24,12 @@ env: - typo3DatabaseUsername="travis" - typo3DatabasePassword="" matrix: - - TYPO3_VERSION="~7.6" - TYPO3_VERSION="~8" - TYPO3_VERSION="dev-master" matrix: fast_finish: true exclude: - # TYPO3 no longer supports 5.6 - - env: TYPO3_VERSION="~8" - php: 5.6 - - env: TYPO3_VERSION="dev-master" - php: 5.6 allow_failures: - env: TYPO3_VERSION="~8" php: 7.0 @@ -40,11 +42,12 @@ matrix: services: - mysql - - elasticsearch install: make install -script: make functionalTests +script: + - make unitTests + - make functionalTests after_script: - make uploadCodeCoverage diff --git a/Classes/Command/IndexCommandController.php b/Classes/Command/IndexCommandController.php index c1ade27..b14d4b3 100644 --- a/Classes/Command/IndexCommandController.php +++ b/Classes/Command/IndexCommandController.php @@ -1,5 +1,5 @@ @@ -20,7 +20,8 @@ namespace Leonmrni\SearchCore\Command; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; @@ -34,12 +35,6 @@ class IndexCommandController extends CommandController */ protected $indexerFactory; - /** - * @var \Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface - * @inject - */ - protected $configuration; - /** * @param IndexerFactory $factory */ @@ -49,19 +44,18 @@ class IndexCommandController extends CommandController } /** - * Will index the given table or everything. + * Will index the given identifier. * - * @param string $table + * @param string $identifier */ - public function indexCommand($table) + public function indexCommand($identifier) { - // TODO: Allow to index multiple tables at once? // TODO: Also allow to index everything? - if (! in_array($table, GeneralUtility::trimExplode(',', $this->configuration->get('indexer.tca.allowedTables')))) { - $this->outputLine('Table is not allowed for indexing.'); - $this->quit(1); + try { + $this->indexerFactory->getIndexer($identifier)->indexAllDocuments(); + $this->outputLine($identifier . ' was indexed.'); + } catch (NoMatchingIndexerException $e) { + $this->outputLine('No indexer found for: ' . $identifier); } - $this->indexerFactory->getIndexer($table)->indexAllDocuments(); - $this->outputLine('Table was indexed.'); } } diff --git a/Classes/Configuration/ConfigurationContainer.php b/Classes/Configuration/ConfigurationContainer.php index db66a10..7481692 100644 --- a/Classes/Configuration/ConfigurationContainer.php +++ b/Classes/Configuration/ConfigurationContainer.php @@ -1,5 +1,5 @@ diff --git a/Classes/Configuration/ConfigurationContainerInterface.php b/Classes/Configuration/ConfigurationContainerInterface.php index 7bc45ef..9429535 100644 --- a/Classes/Configuration/ConfigurationContainerInterface.php +++ b/Classes/Configuration/ConfigurationContainerInterface.php @@ -1,5 +1,5 @@ @@ -34,6 +34,8 @@ interface ConfigurationContainerInterface extends Singleton * * @param string $path In dot notation. E.g. indexer.tca.allowedTables * @return mixed + * + * @throws InvalidArgumentException */ public function get($path); diff --git a/Classes/Configuration/InvalidArgumentException.php b/Classes/Configuration/InvalidArgumentException.php index fd9e048..1f5f8f0 100644 --- a/Classes/Configuration/InvalidArgumentException.php +++ b/Classes/Configuration/InvalidArgumentException.php @@ -1,5 +1,5 @@ diff --git a/Classes/Configuration/NoConfigurationException.php b/Classes/Configuration/NoConfigurationException.php index bc6c1ba..db4e511 100644 --- a/Classes/Configuration/NoConfigurationException.php +++ b/Classes/Configuration/NoConfigurationException.php @@ -1,5 +1,5 @@ diff --git a/Classes/Connection/ConnectionInterface.php b/Classes/Connection/ConnectionInterface.php index fdd09e3..a130f9e 100644 --- a/Classes/Connection/ConnectionInterface.php +++ b/Classes/Connection/ConnectionInterface.php @@ -1,5 +1,5 @@ diff --git a/Classes/Connection/Elasticsearch.php b/Classes/Connection/Elasticsearch.php index 560ca07..f6321bc 100644 --- a/Classes/Connection/Elasticsearch.php +++ b/Classes/Connection/Elasticsearch.php @@ -1,5 +1,5 @@ @@ -20,7 +20,10 @@ namespace Leonmrni\SearchCore\Connection; * 02110-1301, USA. */ +use Codappix\SearchCore\Connection\Elasticsearch\SearchResult; +use Codappix\SearchCore\Domain\Search\QueryFactory; use TYPO3\CMS\Core\SingletonInterface as Singleton; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** * Outer wrapper to elasticsearch. @@ -42,16 +45,31 @@ class Elasticsearch implements Singleton, ConnectionInterface */ protected $typeFactory; + /** + * @var Elasticsearch\MappingFactory + */ + protected $mappingFactory; + /** * @var Elasticsearch\DocumentFactory */ protected $documentFactory; + /** + * @var QueryFactory + */ + protected $queryFactory; + /** * @var \TYPO3\CMS\Core\Log\Logger */ protected $logger; + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + /** * Inject log manager to get concrete logger from it. * @@ -62,22 +80,36 @@ class Elasticsearch implements Singleton, ConnectionInterface $this->logger = $logManager->getLogger(__CLASS__); } + /** + * @param ObjectManagerInterface $objectManager + */ + public function injectObjectManager(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + /** * @param Elasticsearch\Connection $connection * @param Elasticsearch\IndexFactory $indexFactory * @param Elasticsearch\TypeFactory $typeFactory + * @param Elasticsearch\MappingFactory $mappingFactory * @param Elasticsearch\DocumentFactory $documentFactory + * @param QueryFactory $queryFactory */ public function __construct( Elasticsearch\Connection $connection, Elasticsearch\IndexFactory $indexFactory, Elasticsearch\TypeFactory $typeFactory, - Elasticsearch\DocumentFactory $documentFactory + Elasticsearch\MappingFactory $mappingFactory, + Elasticsearch\DocumentFactory $documentFactory, + QueryFactory $queryFactory ) { $this->connection = $connection; $this->indexFactory = $indexFactory; $this->typeFactory = $typeFactory; + $this->mappingFactory = $mappingFactory; $this->documentFactory = $documentFactory; + $this->queryFactory = $queryFactory; } public function addDocument($documentType, array $document) @@ -133,6 +165,13 @@ class Elasticsearch implements Singleton, ConnectionInterface protected function withType($documentType, callable $callback) { $type = $this->getType($documentType); + // TODO: Check whether it's to heavy to send it so often e.g. for every single document. + // Perhaps add command controller to submit mapping?! + // Also it's not possible to change mapping without deleting index first. + // Mattes told about a solution. + // So command looks like the best way so far, except we manage mattes solution. + // Still then this should be done once. So perhaps singleton which tracks state and does only once? + $this->mappingFactory->getMapping($type)->send(); $callback($type); $type->getIndex()->refresh(); } @@ -140,7 +179,7 @@ class Elasticsearch implements Singleton, ConnectionInterface /** * @param SearchRequestInterface $searchRequest * - * @return \Elastica\ResultSet + * @return SearchResultInterface */ public function search(SearchRequestInterface $searchRequest) { @@ -148,10 +187,9 @@ class Elasticsearch implements Singleton, ConnectionInterface $search = new \Elastica\Search($this->connection->getClient()); $search->addIndex('typo3content'); + $search->setQuery($this->queryFactory->create($searchRequest)); - // TODO: Return wrapped result to implement our interface. - // Also update php doc to reflect the change. - return $search->search('"' . $searchRequest->getSearchTerm() . '"'); + return $this->objectManager->get(SearchResult::class, $search->search()); } /** diff --git a/Classes/Connection/Elasticsearch/Connection.php b/Classes/Connection/Elasticsearch/Connection.php index 25c7361..cd0a1fd 100644 --- a/Classes/Connection/Elasticsearch/Connection.php +++ b/Classes/Connection/Elasticsearch/Connection.php @@ -1,5 +1,5 @@ @@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Connection\Elasticsearch; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use TYPO3\CMS\Core\SingletonInterface as Singleton; /** diff --git a/Classes/Connection/Elasticsearch/DocumentFactory.php b/Classes/Connection/Elasticsearch/DocumentFactory.php index 43462fe..99d29e5 100644 --- a/Classes/Connection/Elasticsearch/DocumentFactory.php +++ b/Classes/Connection/Elasticsearch/DocumentFactory.php @@ -1,5 +1,5 @@ diff --git a/Classes/Connection/Elasticsearch/Facet.php b/Classes/Connection/Elasticsearch/Facet.php new file mode 100644 index 0000000..7893f5f --- /dev/null +++ b/Classes/Connection/Elasticsearch/Facet.php @@ -0,0 +1,93 @@ + + * + * 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\Connection\FacetInterface; + +class Facet implements FacetInterface +{ + /** + * @var string + */ + protected $name = ''; + + /** + * @var string + */ + protected $field = ''; + + /** + * @var array + */ + protected $buckets = []; + + /** + * @var array + */ + protected $options = []; + + public function __construct($name, array $aggregation, ConfigurationContainerInterface $configuration) + { + $this->name = $name; + $this->buckets = $aggregation['buckets']; + $this->field = $configuration->getIfExists('searching.facets.' . $this->name . '.field') ?: ''; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getField() + { + return $this->field; + } + + /** + * Returns all possible options for this facet. + * + * @return array + */ + public function getOptions() + { + $this->initOptions(); + + return $this->options; + } + + protected function initOptions() + { + if ($this->options !== []) { + return; + } + + foreach ($this->buckets as $bucket) { + $this->options[] = new FacetOption($bucket); + } + } +} diff --git a/Classes/Connection/Elasticsearch/FacetOption.php b/Classes/Connection/Elasticsearch/FacetOption.php new file mode 100644 index 0000000..7359434 --- /dev/null +++ b/Classes/Connection/Elasticsearch/FacetOption.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. + */ + +use Codappix\SearchCore\Connection\FacetOptionInterface; + +class FacetOption implements FacetOptionInterface +{ + /** + * @var string + */ + protected $name = ''; + + /** + * @var int + */ + protected $count = 0; + + /** + * @param array $bucket + */ + public function __construct(array $bucket) + { + $this->name = $bucket['key']; + $this->count = $bucket['doc_count']; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return int + */ + public function getCount() + { + return $this->count; + } +} diff --git a/Classes/Connection/Elasticsearch/IndexFactory.php b/Classes/Connection/Elasticsearch/IndexFactory.php index 0a06ad4..019b091 100644 --- a/Classes/Connection/Elasticsearch/IndexFactory.php +++ b/Classes/Connection/Elasticsearch/IndexFactory.php @@ -1,5 +1,5 @@ diff --git a/Classes/Connection/Elasticsearch/MappingFactory.php b/Classes/Connection/Elasticsearch/MappingFactory.php new file mode 100644 index 0000000..d4ac037 --- /dev/null +++ b/Classes/Connection/Elasticsearch/MappingFactory.php @@ -0,0 +1,73 @@ + + * + * 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\Configuration\InvalidArgumentException; +use TYPO3\CMS\Core\SingletonInterface as Singleton; + +/** + * Factory to get mappings. + */ +class MappingFactory implements Singleton +{ + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + + /** + * @param ConfigurationContainerInterface $configuration + */ + public function __construct(ConfigurationContainerInterface $configuration) + { + $this->configuration = $configuration; + } + + /** + * Get an mapping based on type. + * + * @param \Elastica\Type $type + * + * @return \Elastica\Mapping + */ + public function getMapping(\Elastica\Type $type) + { + $mapping = new \Elastica\Type\Mapping(); + $mapping->setType($type); + $mapping->setProperties($this->getConfiguration($type->getName())); + + return $mapping; + } + + /** + * @param string $identifier + * @return array + */ + protected function getConfiguration($identifier) + { + try { + return $this->configuration->get('indexing.' . $identifier . '.mapping'); + } catch (InvalidArgumentException $e) { + return []; + } + } +} diff --git a/Classes/Connection/Elasticsearch/ResultItem.php b/Classes/Connection/Elasticsearch/ResultItem.php new file mode 100644 index 0000000..e56783c --- /dev/null +++ b/Classes/Connection/Elasticsearch/ResultItem.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 Codappix\SearchCore\Connection\ResultItemInterface; + +class ResultItem implements ResultItemInterface +{ + /** + * @var array + */ + protected $data = []; + + public function __construct(\Elastica\Result $result) + { + $this->data = $result->getData(); + } + + public function offsetExists($offset) + { + return isset($this->data[$offset]); + } + + public function offsetGet($offset) + { + return $this->data[$offset]; + } + + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077); + } + + public function offsetUnset($offset) + { + throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077); + } +} diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 3f90d60..f8ffdd8 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -1,5 +1,5 @@ @@ -20,12 +20,127 @@ namespace Leonmrni\SearchCore\Connection\Elasticsearch; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Connection\FacetInterface; +use Codappix\SearchCore\Connection\ResultItemInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; -/** - * - */ -class SearchResult extends \Elastica\SearchResult implements SearchResultInterface +class SearchResult implements SearchResultInterface { + /** + * @var \Elastica\ResultSet + */ + protected $result; + /** + * @var array + */ + protected $facets = []; + + /** + * @var array + */ + protected $results = []; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + public function __construct(\Elastica\ResultSet $result, ObjectManagerInterface $objectManager) + { + $this->result = $result; + $this->objectManager = $objectManager; + } + + /** + * @return array + */ + public function getResults() + { + $this->initResults(); + + return $this->results; + } + + /** + * Return all facets, if any. + * + * @return array + */ + public function getFacets() + { + $this->initFacets(); + + return $this->facets; + } + + /** + * Returns the total sum of matching results. + * + * @return int + */ + public function getTotalCount() + { + return $this->result->getTotalHits(); + } + + // Countable - Interface + /** + * Returns the total sum of results contained in this result. + * + * @return int + */ + public function count() + { + return $this->result->count(); + } + + // Iterator - Interface + public function current() + { + return $this->result->current(); + } + + public function next() + { + return $this->result->next(); + } + + public function key() + { + return $this->result->key(); + } + + public function valid() + { + return $this->result->valid(); + } + + public function rewind() + { + $this->result->rewind(); + } + + protected function initResults() + { + if ($this->results !== []) { + return; + } + + foreach ($this->result->getResults() as $result) { + $this->results[] = new ResultItem($result); + } + } + + protected function initFacets() + { + if ($this->facets !== [] || !$this->result->hasAggregations()) { + return; + } + + foreach ($this->result->getAggregations() as $aggregationName => $aggregation) { + $this->facets[] = $this->objectManager->get(Facet::class, $aggregationName, $aggregation); + } + } } diff --git a/Classes/Connection/Elasticsearch/TypeFactory.php b/Classes/Connection/Elasticsearch/TypeFactory.php index b529937..d5283f8 100644 --- a/Classes/Connection/Elasticsearch/TypeFactory.php +++ b/Classes/Connection/Elasticsearch/TypeFactory.php @@ -1,5 +1,5 @@ diff --git a/Classes/Connection/FacetInterface.php b/Classes/Connection/FacetInterface.php new file mode 100644 index 0000000..b1cc421 --- /dev/null +++ b/Classes/Connection/FacetInterface.php @@ -0,0 +1,39 @@ + + * + * 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. + */ + +/** + * A single facet, e.g. keyword based. + */ +interface FacetInterface +{ + /** + * @return string + */ + public function getName(); + + /** + * Returns all possible options for this facet. + * + * @return array + */ + public function getOptions(); +} diff --git a/Classes/Connection/FacetOptionInterface.php b/Classes/Connection/FacetOptionInterface.php new file mode 100644 index 0000000..51a1efd --- /dev/null +++ b/Classes/Connection/FacetOptionInterface.php @@ -0,0 +1,42 @@ + + * + * 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. + */ + +/** + * A single possible option of a facet. + */ +interface FacetOptionInterface +{ + /** + * Returns the name of this option. Equivalent + * to value used for filtering. + * + * @return string + */ + public function getName(); + + /** + * Returns the number of found results for this option. + * + * @return int + */ + public function getCount(); +} diff --git a/Classes/Connection/FacetRequestInterface.php b/Classes/Connection/FacetRequestInterface.php new file mode 100644 index 0000000..9352f96 --- /dev/null +++ b/Classes/Connection/FacetRequestInterface.php @@ -0,0 +1,42 @@ + + * + * 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. + */ + +/** + * Used to request facets / aggregates from connection. + */ +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(); + + /** + * The field to use for facet building. + * + * @return string + */ + public function getField(); +} diff --git a/Classes/Connection/ResultItemInterface.php b/Classes/Connection/ResultItemInterface.php new file mode 100644 index 0000000..56add2a --- /dev/null +++ b/Classes/Connection/ResultItemInterface.php @@ -0,0 +1,29 @@ + + * + * 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 ArrayAccess to enable retrieval of information in fluid. + */ +interface ResultItemInterface extends \ArrayAccess +{ + +} diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index ecf7a74..ee779f7 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -1,5 +1,5 @@ @@ -31,4 +31,14 @@ interface SearchRequestInterface * @return string */ public function getSearchTerm(); + + /** + * @return bool + */ + public function hasFilter(); + + /** + * @return array + */ + public function getFilter(); } diff --git a/Classes/Connection/SearchResultInterface.php b/Classes/Connection/SearchResultInterface.php index f00e97c..5c45bcd 100644 --- a/Classes/Connection/SearchResultInterface.php +++ b/Classes/Connection/SearchResultInterface.php @@ -1,5 +1,5 @@ @@ -21,9 +21,35 @@ namespace Leonmrni\SearchCore\Connection; */ /** - * + * A search result. */ -interface SearchResultInterface extends \Iterator, \Countable, \ArrayAccess +interface SearchResultInterface extends \Iterator, \Countable { + /** + * @return array + */ + public function getResults(); + /** + * Return all facets, if any. + * + * @return array + */ + public function getFacets(); + + /** + * Returns the total sum of matching results. + * + * @return int + */ + public function getTotalCount(); + + // Countable - Interface + + /** + * Returns the total sum of results contained in this result. + * + * @return int + */ + public function count(); } diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index b7624f7..068f9a1 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -1,5 +1,5 @@ @@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Controller; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Domain\Model\SearchRequest; -use Leonmrni\SearchCore\Domain\Search\SearchService; +use Codappix\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Domain\Search\SearchService; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; /** diff --git a/Classes/Domain/Index/AbstractIndexer.php b/Classes/Domain/Index/AbstractIndexer.php new file mode 100644 index 0000000..b780dc5 --- /dev/null +++ b/Classes/Domain/Index/AbstractIndexer.php @@ -0,0 +1,113 @@ + + * + * 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\ConnectionInterface; + +abstract class AbstractIndexer implements IndexerInterface +{ + /** + * @var ConnectionInterface + */ + protected $connection; + + /** + * @var \TYPO3\CMS\Core\Log\Logger + */ + protected $logger; + + /** + * Inject log manager to get concrete logger from it. + * + * @param \TYPO3\CMS\Core\Log\LogManager $logManager + */ + public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager) + { + $this->logger = $logManager->getLogger(__CLASS__); + } + + /** + * @param ConnectionInterface $connection + */ + public function __construct(ConnectionInterface $connection) + { + $this->connection = $connection; + } + + public function indexAllDocuments() + { + $this->logger->info('Start indexing'); + foreach ($this->getRecordGenerator() as $records) { + $this->logger->debug('Index records.', [$records]); + if ($records === null) { + break; + } + + $this->connection->addDocuments($this->getDocumentName(), $records); + } + $this->logger->info('Finish indexing'); + } + + public function indexDocument($identifier) + { + $this->logger->info('Start indexing single record.', [$identifier]); + try { + $this->connection->addDocument($this->getDocumentName(), $this->getRecord($identifier)); + } catch (NoRecordFoundException $e) { + $this->logger->info('Could not index document.', [$e->getMessage()]); + } + $this->logger->info('Finish indexing'); + } + + /** + * @return \Generator + */ + protected function getRecordGenerator() + { + $offset = 0; + // TODO: Make configurable. + $limit = 50; + + while (($records = $this->getRecords($offset, $limit)) !== []) { + yield $records; + $offset += $limit; + } + } + + /** + * @param int $offset + * @param int $limit + * @return array|null + */ + abstract protected function getRecords($offset, $limit); + + /** + * @param int $identifier + * @return array + * @throws NoRecordFoundException If record could not be found. + */ + abstract protected function getRecord($identifier); + + /** + * @return string + */ + abstract protected function getDocumentName(); +} diff --git a/Classes/Domain/Index/IndexerFactory.php b/Classes/Domain/Index/IndexerFactory.php index 4ecbfb9..6618d01 100644 --- a/Classes/Domain/Index/IndexerFactory.php +++ b/Classes/Domain/Index/IndexerFactory.php @@ -1,5 +1,5 @@ @@ -20,6 +20,11 @@ namespace Leonmrni\SearchCore\Domain\Index; * 02110-1301, USA. */ +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Configuration\InvalidArgumentException; +use Codappix\SearchCore\Domain\Index\IndexerInterface; +use Codappix\SearchCore\Domain\Index\TcaIndexer; +use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService; use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; @@ -34,27 +39,61 @@ class IndexerFactory implements Singleton protected $objectManager; /** - * @param ObjectManagerInterface $objectManager + * @var ConfigurationContainerInterface */ - public function __construct(ObjectManagerInterface $objectManager) - { + protected $configuration; + + /** + * @param ObjectManagerInterface $objectManager + * @param ConfigurationContainerInterface $configuration + */ + public function __construct( + ObjectManagerInterface $objectManager, + ConfigurationContainerInterface $configuration + ) { $this->objectManager = $objectManager; + $this->configuration = $configuration; } /** - * @param string $tableName + * @param string $identifier * * @return IndexerInterface + * @throws NoMatchingIndexer */ - public function getIndexer($tableName) + public function getIndexer($identifier) { - // This is the place to use configuration to return different indexer. - return $this->objectManager->get( - TcaIndexer::Class, - $this->objectManager->get( - TcaIndexer\TcaTableService::class, - $tableName - ) - ); + try { + return $this->buildIndexer($this->configuration->get('indexing.' . $identifier . '.indexer'), $identifier); + } catch (NoMatchingIndexerException $e) { + // Nothing to do, we throw exception below + } catch (InvalidArgumentException $e) { + // Nothing to do, we throw exception below + } + + throw new NoMatchingIndexerException('Could not find an indexer for ' . $identifier, 1497341442); + } + + /** + * @param string $indexerClass + * @param string $identifier + * + * @return IndexerInterface + * @throws NoMatchingIndexer + */ + protected function buildIndexer($indexerClass, $identifier) + { + if ($indexerClass === TcaIndexer::class) { + return $this->objectManager->get( + TcaIndexer::class, + $this->objectManager->get(TcaTableService::class, $identifier) + ); + } + + if (class_exists($indexerClass) && in_array(IndexerInterface::class, class_implements($indexerClass))) { + return $this->objectManager->get($indexerClass); + } + + throw new NoMatchingIndexerException('Could not find indexer: ' . $indexerClass, 1497341442); } } diff --git a/Classes/Domain/Index/IndexerInterface.php b/Classes/Domain/Index/IndexerInterface.php index d70b410..5fef64f 100644 --- a/Classes/Domain/Index/IndexerInterface.php +++ b/Classes/Domain/Index/IndexerInterface.php @@ -1,5 +1,5 @@ diff --git a/Classes/Domain/Index/IndexingException.php b/Classes/Domain/Index/IndexingException.php index a22660d..d8ca71e 100644 --- a/Classes/Domain/Index/IndexingException.php +++ b/Classes/Domain/Index/IndexingException.php @@ -1,5 +1,5 @@ diff --git a/Classes/Domain/Index/NoMatchingIndexerException.php b/Classes/Domain/Index/NoMatchingIndexerException.php new file mode 100644 index 0000000..3f6c094 --- /dev/null +++ b/Classes/Domain/Index/NoMatchingIndexerException.php @@ -0,0 +1,25 @@ + + * + * 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. + */ + +class NoMatchingIndexerException extends IndexingException +{ +} diff --git a/Classes/Domain/Index/NoRecordFoundException.php b/Classes/Domain/Index/NoRecordFoundException.php index 8a92408..469359d 100644 --- a/Classes/Domain/Index/NoRecordFoundException.php +++ b/Classes/Domain/Index/NoRecordFoundException.php @@ -1,5 +1,5 @@ diff --git a/Classes/Domain/Index/TcaIndexer.php b/Classes/Domain/Index/TcaIndexer.php index 3abdcb3..c0a77a1 100644 --- a/Classes/Domain/Index/TcaIndexer.php +++ b/Classes/Domain/Index/TcaIndexer.php @@ -1,5 +1,5 @@ @@ -20,39 +20,18 @@ namespace Leonmrni\SearchCore\Domain\Index; * 02110-1301, USA. */ -use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; -use Leonmrni\SearchCore\Connection\ConnectionInterface; +use Codappix\SearchCore\Connection\ConnectionInterface; /** * Will index the given table using configuration from TCA. */ -class TcaIndexer implements IndexerInterface +class TcaIndexer extends AbstractIndexer { - /** - * @var ConnectionInterface - */ - protected $connection; - /** * @var TcaIndexer\TcaTableService */ protected $tcaTableService; - /** - * @var \TYPO3\CMS\Core\Log\Logger - */ - protected $logger; - - /** - * Inject log manager to get concrete logger from it. - * - * @param \TYPO3\CMS\Core\Log\LogManager $logManager - */ - public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager) - { - $this->logger = $logManager->getLogger(__CLASS__); - } - /** * @param TcaIndexer\TcaTableService $tcaTableService * @param ConnectionInterface $connection @@ -65,46 +44,6 @@ class TcaIndexer implements IndexerInterface $this->connection = $connection; } - public function indexAllDocuments() - { - $this->logger->info('Start indexing'); - foreach ($this->getRecordGenerator() as $records) { - $this->logger->debug('Index records.', [$records]); - if ($records === null) { - break; - } - - $this->connection->addDocuments($this->tcaTableService->getTableName(), $records); - } - $this->logger->info('Finish indexing'); - } - - public function indexDocument($identifier) - { - $this->logger->info('Start indexing single record.', [$identifier]); - try { - $this->connection->addDocument($this->tcaTableService->getTableName(), $this->getRecord($identifier)); - } catch (NoRecordFoundException $e) { - $this->logger->info('Could not index document.', [$e->getMessage()]); - } - $this->logger->info('Finish indexing'); - } - - /** - * @return \Generator - */ - protected function getRecordGenerator() - { - $offset = 0; - // TODO: Make configurable. - $limit = 50; - - while (($records = $this->getRecords($offset, $limit)) !== []) { - yield $records; - $offset += $limit; - } - } - /** * @param int $offset * @param int $limit @@ -157,6 +96,14 @@ class TcaIndexer implements IndexerInterface return $record; } + /** + * @return string + */ + protected function getDocumentName() + { + return $this->tcaTableService->getTableName(); + } + protected function getDatabaseConnection() { return GeneralUtility::makeInstance(ConnectionPool::class) diff --git a/Classes/Domain/Index/TcaIndexer/InvalidArgumentException.php b/Classes/Domain/Index/TcaIndexer/InvalidArgumentException.php index 0f3dc9a..bc2036f 100644 --- a/Classes/Domain/Index/TcaIndexer/InvalidArgumentException.php +++ b/Classes/Domain/Index/TcaIndexer/InvalidArgumentException.php @@ -1,5 +1,5 @@ diff --git a/Classes/Domain/Index/TcaIndexer/RelationResolver.php b/Classes/Domain/Index/TcaIndexer/RelationResolver.php index 69eb4a2..b09e483 100644 --- a/Classes/Domain/Index/TcaIndexer/RelationResolver.php +++ b/Classes/Domain/Index/TcaIndexer/RelationResolver.php @@ -1,5 +1,5 @@ diff --git a/Classes/Domain/Index/TcaIndexer/TcaTableService.php b/Classes/Domain/Index/TcaIndexer/TcaTableService.php index 936d425..21e6374 100644 --- a/Classes/Domain/Index/TcaIndexer/TcaTableService.php +++ b/Classes/Domain/Index/TcaIndexer/TcaTableService.php @@ -1,5 +1,5 @@ @@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Domain\Index\IndexingException; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Domain\Index\IndexingException; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -151,7 +151,7 @@ class TcaTableService . ' AND pages.no_search = 0' ; - $userDefinedWhere = $this->configuration->getIfExists('indexer.tca.' . $this->tableName . '.additionalWhereClause'); + $userDefinedWhere = $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.additionalWhereClause'); if (is_string($userDefinedWhere)) { $whereClause .= ' AND ' . $userDefinedWhere; } @@ -269,7 +269,7 @@ class TcaTableService */ protected function isBlackListedRootLineConfigured() { - return (bool) $this->configuration->getIfExists('indexer.tca.rootLineBlacklist'); + return (bool) $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist'); } /** @@ -279,6 +279,6 @@ class TcaTableService */ protected function getBlackListedRootLine() { - return GeneralUtility::intExplode(',', $this->configuration->getIfExists('indexer.tca.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 new file mode 100644 index 0000000..b1dbc4c --- /dev/null +++ b/Classes/Domain/Model/FacetRequest.php @@ -0,0 +1,66 @@ + + * + * 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\FacetRequestInterface; + +class FacetRequest implements FacetRequestInterface +{ + /** + * @var string + */ + protected $identifier = ''; + + /** + * @var string + */ + protected $field = ''; + + /** + * 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) + { + $this->identifier = $identifier; + $this->field = $field; + } + + /** + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @return string + */ + public function getField() + { + return $this->field; + } +} diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index 1de2f71..163f65a 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -1,5 +1,5 @@ @@ -20,7 +20,8 @@ namespace Leonmrni\SearchCore\Domain\Model; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Connection\SearchRequestInterface; +use Codappix\SearchCore\Connection\FacetRequestInterface; +use Codappix\SearchCore\Connection\SearchRequestInterface; /** * Represents a search request used to process an actual search. @@ -32,14 +33,24 @@ class SearchRequest implements SearchRequestInterface * * @var string */ - protected $query; + protected $query = ''; + + /** + * @var array + */ + protected $filter = []; + + /** + * @var array + */ + protected $facets = []; /** * @param string $query */ public function __construct($query) { - $this->query = $query; + $this->query = (string) $query; } /** @@ -57,4 +68,48 @@ class SearchRequest implements SearchRequestInterface { return $this->query; } + + /** + * @param array $filter + */ + public function setFilter(array $filter) + { + $this->filter = array_filter(array_map('strval', $filter)); + } + + /** + * @return bool + */ + public function hasFilter() + { + return count($this->filter) > 0; + } + + /** + * @return array + */ + public function getFilter() + { + return $this->filter; + } + + /** + * Add a facet to gather in this search request. + * + * @param FacetRequestInterface $facet + */ + public function addFacet(FacetRequestInterface $facet) + { + $this->facets[$facet->getIdentifier()] = $facet; + } + + /** + * Returns all configured facets to fetch in this search request. + * + * @return array + */ + public function getFacets() + { + return $this->facets; + } } diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php new file mode 100644 index 0000000..7809fb6 --- /dev/null +++ b/Classes/Domain/Search/QueryFactory.php @@ -0,0 +1,138 @@ + + * + * 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\ConnectionInterface; +use Codappix\SearchCore\Connection\Elasticsearch\Query; +use Codappix\SearchCore\Connection\SearchRequestInterface; +use TYPO3\CMS\Extbase\Utility\ArrayUtility; + +class QueryFactory +{ + /** + * @var \TYPO3\CMS\Core\Log\Logger + */ + protected $logger; + + /** + * @var array + */ + protected $query = []; + + /** + * @param \TYPO3\CMS\Core\Log\LogManager $logManager + */ + public function __construct(\TYPO3\CMS\Core\Log\LogManager $logManager) + { + $this->logger = $logManager->getLogger(__CLASS__); + } + + /** + * @param SearchRequestInterface $searchRequest + * + * @return \Elastica\Query + */ + public function create(SearchRequestInterface $searchRequest) + { + return $this->createElasticaQuery($searchRequest); + } + + /** + * @param SearchRequestInterface $searchRequest + * + * TODO: This is not in scope Elasticsearch, therefore should not return elastica. + * @return \Elastica\Query + */ + protected function createElasticaQuery(SearchRequestInterface $searchRequest) + { + $this->addSearch($searchRequest); + $this->addFilter($searchRequest); + $this->addFacets($searchRequest); + + $this->logger->debug('Generated elasticsearch query.', [$this->query]); + return new \Elastica\Query($this->query); + } + + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addSearch(SearchRequestInterface $searchRequest) + { + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'query' => [ + 'bool' => [ + 'must' => [ + [ + 'match' => [ + '_all' => $searchRequest->getSearchTerm() + ], + ], + ], + ], + ], + ]); + } + + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addFilter(SearchRequestInterface $searchRequest) + { + if (! $searchRequest->hasFilter()) { + return; + } + + $terms = []; + foreach ($searchRequest->getFilter() as $name => $value) { + $terms[] = [ + 'term' => [ + $name => $value, + ], + ]; + } + + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'query' => [ + 'bool' => [ + 'filter' => $terms, + ], + ], + ]); + } + + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addFacets(SearchRequestInterface $searchRequest) + { + foreach ($searchRequest->getFacets() as $facet) { + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'aggs' => [ + $facet->getIdentifier() => [ + 'terms' => [ + 'field' => $facet->getField(), + ], + ], + ], + ]); + } + } +} diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index afbaf7e..158d9d1 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -1,5 +1,5 @@ @@ -20,10 +20,13 @@ namespace Leonmrni\SearchCore\Domain\Search; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Connection\ConnectionInterface; -use Leonmrni\SearchCore\Connection\SearchRequestInterface; -use Leonmrni\SearchCore\Connection\SearchResultInterface; -use Leonmrni\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Configuration\InvalidArgumentException; +use Codappix\SearchCore\Connection\ConnectionInterface; +use Codappix\SearchCore\Connection\SearchRequestInterface; +use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Domain\Model\FacetRequest; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** * Service to process a search request. @@ -36,11 +39,28 @@ class SearchService protected $connection; /** - * @param ConnectionInterface $connection + * @var ConfigurationContainerInterface */ - public function __construct(ConnectionInterface $connection) - { + protected $configuration; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @param ConnectionInterface $connection + * @param ConfigurationContainerInterface $configuration + * @param ObjectManagerInterface $objectManager + */ + public function __construct( + ConnectionInterface $connection, + ConfigurationContainerInterface $configuration, + ObjectManagerInterface $objectManager + ) { $this->connection = $connection; + $this->configuration = $configuration; + $this->objectManager = $objectManager; } /** @@ -49,6 +69,34 @@ class SearchService */ public function search(SearchRequestInterface $searchRequest) { + $this->addConfiguredFacets($searchRequest); + return $this->connection->search($searchRequest); } + + /** + * Add facets from configuration to request. + * + * @param SearchRequestInterface $searchRequest + */ + protected function addConfiguredFacets(SearchRequestInterface $searchRequest) + { + $facetsConfig = $this->configuration->getIfExists('searching.facets'); + if ($facetsConfig === null) { + return; + } + + 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'] + )); + } + } } diff --git a/Classes/Domain/Service/DataHandler.php b/Classes/Domain/Service/DataHandler.php index 213b9cc..6ac8069 100644 --- a/Classes/Domain/Service/DataHandler.php +++ b/Classes/Domain/Service/DataHandler.php @@ -1,5 +1,5 @@ @@ -20,7 +20,10 @@ namespace Leonmrni\SearchCore\Domain\Service; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException; +use Codappix\SearchCore\Domain\Index\TcaIndexer; use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -40,14 +43,13 @@ class DataHandler implements Singleton /** * TODO: Only inject on first use?! * - * @var \Leonmrni\SearchCore\Connection\ConnectionInterface + * @var \Codappix\SearchCore\Connection\ConnectionInterface * @inject */ protected $connection; /** - * @var \Leonmrni\SearchCore\Domain\Index\IndexerFactory - * @inject + * @var IndexerFactory */ protected $indexerFactory; @@ -73,20 +75,12 @@ class DataHandler implements Singleton /** * @param ConfigurationContainerInterface $configuration + * @param IndexerFactory $indexerFactory */ - public function __construct(ConfigurationContainerInterface $configuration) + public function __construct(ConfigurationContainerInterface $configuration, IndexerFactory $indexerFactory) { $this->configuration = $configuration; - } - - /** - * Get all tables that are allowed for indexing. - * - * @return array - */ - public function getAllowedTablesForIndexing() - { - return GeneralUtility::trimExplode(',', $this->configuration->get('indexer.tca.allowedTables')); + $this->indexerFactory = $indexerFactory; } /** @@ -96,7 +90,7 @@ class DataHandler implements Singleton public function add($table, array $record) { $this->logger->debug('Record received for add.', [$table, $record]); - $this->indexerFactory->getIndexer($table)->indexDocument($record['uid']); + $this->getIndexer($table)->indexDocument($record['uid']); } /** @@ -105,7 +99,7 @@ class DataHandler implements Singleton public function update($table, array $record) { $this->logger->debug('Record received for update.', [$table, $record]); - $this->indexerFactory->getIndexer($table)->indexDocument($record['uid']); + $this->getIndexer($table)->indexDocument($record['uid']); } /** @@ -117,4 +111,31 @@ class DataHandler implements Singleton $this->logger->debug('Record received for delete.', [$table, $identifier]); $this->connection->deleteDocument($table, $identifier); } + + /** + * @param string $table + * @return IndexerInterface + * + * @throws NoMatchingIndexerException + */ + protected function getIndexer($table) + { + return $this->indexerFactory->getIndexer($table); + } + + /** + * @param string $table + * @return bool + */ + public function canHandle($table) + { + try { + $this->getIndexer($table); + return true; + } catch (NoMatchingIndexerException $e) { + return false; + } + + return false; + } } diff --git a/Classes/Hook/DataHandler.php b/Classes/Hook/DataHandler.php index 1bcb4a5..d0eb1ba 100644 --- a/Classes/Hook/DataHandler.php +++ b/Classes/Hook/DataHandler.php @@ -1,5 +1,5 @@ @@ -20,8 +20,9 @@ namespace Leonmrni\SearchCore\Hook; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\NoConfigurationException; -use Leonmrni\SearchCore\Domain\Service\DataHandler as OwnDataHandler; +use Codappix\SearchCore\Configuration\NoConfigurationException; +use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException; +use Codappix\SearchCore\Domain\Service\DataHandler as OwnDataHandler; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\DataHandling\DataHandler as CoreDataHandler; use TYPO3\CMS\Core\Log\LogManager; @@ -140,7 +141,7 @@ class DataHandler implements Singleton $this->logger->debug('Datahandler could not be setup.'); return false; } - if (! $this->shouldProcessTable($table)) { + if (! $this->dataHandler->canHandle($table)) { $this->logger->debug('Table is not allowed.', [$table]); return false; } @@ -148,15 +149,6 @@ class DataHandler implements Singleton return true; } - /** - * @param string $table - * @return bool - */ - protected function shouldProcessTable($table) - { - return in_array($table, $this->dataHandler->getAllowedTablesForIndexing()); - } - /** * Wrapper to allow unit testing. * diff --git a/Configuration/TypoScript/constants.txt b/Configuration/TypoScript/constants.txt old mode 100755 new mode 100644 index 50f57c3..f97c039 --- a/Configuration/TypoScript/constants.txt +++ b/Configuration/TypoScript/constants.txt @@ -8,20 +8,16 @@ plugin { } } - indexer { - tca { - # Pages are not supported yet, see - # https://github.com/DanielSiepmann/search_core/issues/24 but - # should also be added, together with additionalWhereClause - # based on doktypes - allowedTables = tt_content - - tt_content { - additionalWhereClause ( - pages.doktype NOT IN (3, 199) - AND tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu', 'shortcut', 'search', 'login') - ) - } + indexing { + # Pages are not supported yet, see + # https://github.com/DanielSiepmann/search_core/issues/24 but + # should also be added, together with additionalWhereClause + # based on doktypes + tt_content { + additionalWhereClause ( + pages.doktype NOT IN (3, 199) + AND tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu', 'shortcut', 'search', 'login') + ) } } } diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt old mode 100755 new mode 100644 index afa15be..0efcf4d --- a/Configuration/TypoScript/setup.txt +++ b/Configuration/TypoScript/setup.txt @@ -8,13 +8,10 @@ plugin { } } - indexer { - tca { - allowedTables = {$plugin.tx_searchcore.settings.indexer.tca.allowedTables} - - tt_content { - additionalWhereClause = {$plugin.tx_searchcore.settings.indexer.tca.tt_content.additionalWhereClause} - } + indexing { + tt_content { + indexer = Codappix\SearchCore\Domain\Index\TcaIndexer + additionalWhereClause = {$plugin.tx_searchcore.settings.indexing.tt_content.additionalWhereClause} } } } diff --git a/Documentation/source/concepts.rst b/Documentation/source/concepts.rst index 4d48d35..0ce3a0e 100644 --- a/Documentation/source/concepts.rst +++ b/Documentation/source/concepts.rst @@ -24,7 +24,7 @@ Currently only :ref:`Elasticsearch` is provided. Indexing -------- -The indexing is done by one of the available indexer. It should be possible to define the indexer to -use for certein document types. Also it should be possible to write custom indexer to use. +The indexing is done by one of the available indexer. For each identifier it's possible to define +the indexer to use. Also it's possible to write custom indexer to use. Currently only the :ref:`TcaIndexer` is provided. diff --git a/Documentation/source/conf.py b/Documentation/source/conf.py index 07129e7..a708209 100644 --- a/Documentation/source/conf.py +++ b/Documentation/source/conf.py @@ -120,7 +120,7 @@ html_theme = 'alabaster' # documentation. html_theme_options = { 'description': 'TYPO3 Extension to integrate search services.', - 'github_user': 'DanielSiepmann', + 'github_user': 'Codappix', 'github_repo': 'search_core', 'github_button': True, 'github_banner': True, @@ -306,7 +306,7 @@ intersphinx_mapping = { 't3tcaref': ('https://docs.typo3.org/typo3cms/TCAReference/', None), } extlinks = { - 'project': ('https://github.com/DanielSiepmann/search_core/projects/%s', 'Github project: '), - 'pr': ('https://github.com/DanielSiepmann/search_core/pull/%s', 'Github pull request: '), - 'issue': ('https://github.com/DanielSiepmann/search_core/issues/%s', 'Github issue: '), + 'project': ('https://github.com/Codappix/search_core/projects/%s', 'Github project: '), + 'pr': ('https://github.com/Codappix/search_core/pull/%s', 'Github pull request: '), + 'issue': ('https://github.com/Codappix/search_core/issues/%s', 'Github issue: '), } diff --git a/Documentation/source/configuration.rst b/Documentation/source/configuration.rst index fb51d11..3d8db75 100644 --- a/Documentation/source/configuration.rst +++ b/Documentation/source/configuration.rst @@ -38,7 +38,7 @@ Options The following section contains the different options, e.g. for :ref:`connections` and :ref:`indexer`: ``plugin.tx_searchcore.settings.connection`` or -``plugin.tx_searchcore.settings.index``. +``plugin.tx_searchcore.settings.indexing``. .. _configuration_options_connection: @@ -96,8 +96,8 @@ The following settings are available. For each setting its documented which conn .. _configuration_options_index: -index -^^^^^ +Indexing +^^^^^^^^ Holds settings regarding the indexing, e.g. of TYPO3 records, to search services. @@ -106,8 +106,9 @@ Configured as:: plugin { tx_searchcore { settings { - indexer { - indexerName { + indexing { + identifier { + indexer = FullyQualifiedClassname // the settings } } @@ -115,26 +116,10 @@ Configured as:: } } -Where ``indexerName`` is one of the available :ref:`indexer`. +Where ``identifier`` is up to you, but should match table names to make :ref:`TcaIndexer` work. The following settings are available. For each setting its documented which indexer consumes it. -.. _allowedTables: - -``allowedTables`` -""""""""""""""""" - - Used by: :ref:`TcaIndexer`. - - Defines which TYPO3 tables are allowed to be indexed. Only white listed tables will be processed - through Command Line Interface and Hooks. - - Contains a comma separated list of table names. Spaces are trimmed. - - Example:: - - plugin.tx_searchcore.settings.indexer.tca.allowedTables = tt_content, fe_users - .. _rootLineBlacklist: ``rootLineBlacklist`` @@ -151,7 +136,7 @@ The following settings are available. For each setting its documented which inde Example:: - plugin.tx_searchcore.settings.index.tca.rootLineBlacklist = 3, 10, 100 + plugin.tx_searchcore.settings.indexing..rootLineBlacklist = 3, 10, 100 Also it's possible to define some behaviour for the different document types. In context of TYPO3 tables are used as document types 1:1. It's possible to configure different tables. The following @@ -170,9 +155,57 @@ options are available: Example:: - plugin.tx_searchcore.settings.index.tca.tt_content.additionalWhereClause = tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu') + plugin.tx_searchcore.settings.indexing..additionalWhereClause = tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu') .. attention:: Make sure to prefix all fields with the corresponding table name. The selection from database will contain joins and can lead to SQL errors if a field exists in multiple tables. + +.. _mapping: + +``mapping`` +""""""""""" + + Used by: Elasticsearch connection while indexing. + + Define mapping for Elasticsearch, have a look at the official docs: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/mapping.html + You are able to define the mapping for each property / columns. + + Example:: + + plugin.tx_searchcore.settings.indexing.tt_content.mapping { + CType { + type = keyword + } + } + + The above example will define the ``CType`` field of ``tt_content`` as ``type: keyword``. This + makes building a facet possible. + + +.. _configuration_options_search: + +Searching +^^^^^^^^^ + +.. _facets: + +``facets`` +""""""""""" + + Used by: Elasticsearch connection while building search query. + + Define aggregations for Elasticsearch, have a look at the official docs: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/search-aggregations-bucket-terms-aggregation.html + Currently only the term facet is provided. + + Example:: + + plugin.tx_searchcore.settings.searching.facets { + contentTypes { + field = CType + } + } + + The above example will provide a facet with options for all found ``CType`` results together + with a count. diff --git a/Documentation/source/connections.rst b/Documentation/source/connections.rst index 678a92d..b51f280 100644 --- a/Documentation/source/connections.rst +++ b/Documentation/source/connections.rst @@ -22,5 +22,9 @@ The connection is configurable through the following options: * :ref:`port` +* :ref:`mapping` + +* :ref:`facets` + .. _elastic Elasticsearch: https://www.elastic.co/products/elasticsearch .. _elastica: http://elastica.io/ diff --git a/Documentation/source/development.rst b/Documentation/source/development.rst index 8026473..e73425c 100644 --- a/Documentation/source/development.rst +++ b/Documentation/source/development.rst @@ -32,9 +32,10 @@ Then setup your system:: git clone git@github.com:DanielSiepmann/search_core.git \ && cd search_core \ - && export typo3DatabaseName="searchcoretest62" \ - && export TYPO3_VERSION="~6.2" \ + && export typo3DatabaseName="searchcoretest76" \ + && export TYPO3_VERSION="~7.6" \ && make install \ + && make unitTests \ && make functionalTests If all tests are okay, start your work. @@ -42,8 +43,8 @@ If all tests are okay, start your work. If you are working with multiple TYPO3 versions make sure to export `typo3DatabaseName` and `TYPO3_VERSION` in your environment like:: - export typo3DatabaseName="searchcoretest76" - export TYPO3_VERSION="~7.6" + export typo3DatabaseName="searchcoretest62" + export TYPO3_VERSION="~6.2" Also run the install command for each version before running any tests. Only this will make sure you are testing against the actual TYPO3 Version and database scheme. diff --git a/Documentation/source/features.rst b/Documentation/source/features.rst new file mode 100644 index 0000000..669e632 --- /dev/null +++ b/Documentation/source/features.rst @@ -0,0 +1,38 @@ +.. _features: + +Features +======== + +The following features are currently provided: + +.. _features_indexing: + +Indexing +-------- + +Indexing data to Elasticsearch is provided. The extension delivers an indexer for TCA with zero +configuration needs. Still it's possible to configure the indexer. + +Also custom classes can be used as indexers. + +.. _features_search: + +Searching +--------- + +Currently all fields are searched for a single search input. + +Also multiple filter are supported. Filtering results by fields for string contents. + +Even facets / aggregates are now possible. Therefore a mapping has to be defined in TypoScript for +indexing, and the facets itself while searching. + +.. _features_planned: + +Planned +--------- + +The following features are currently planned and will be integrated: + +#. Pagination + Add a pagination to search results, to allow users to walk through all results. diff --git a/Documentation/source/index.rst b/Documentation/source/index.rst index dfda13b..5427fc9 100644 --- a/Documentation/source/index.rst +++ b/Documentation/source/index.rst @@ -7,6 +7,7 @@ Table of Contents :maxdepth: 1 :glob: + features installation configuration usage diff --git a/Documentation/source/installation.rst b/Documentation/source/installation.rst index 614b6e6..ddfb065 100644 --- a/Documentation/source/installation.rst +++ b/Documentation/source/installation.rst @@ -6,7 +6,7 @@ Installation The extension can be installed through composer:: - composer require "leonmrni/search_core dev-feature/integrate-elasticsearch" + composer require "leonmrni/search_core dev-master as 1.0.x-dev" or by `downloading`_ and placing it inside the :file:`typo3conf/ext`-Folder of your installation. In that case you need to install all dependencies yourself. Dependencies are: @@ -16,8 +16,7 @@ In that case you need to install all dependencies yourself. Dependencies are: :lines: 19-21 :dedent: 8 - Afterwards you need to enable the extension through the extension manager and include the static -typoscript setup. +TypoScript setup. -.. _downloading: https://github.com/DanielSiepmann/search_core/archive/feature/integrate-elasticsearch.zip +.. _downloading: https://github.com/DanielSiepmann/search_core/archive/master.zip diff --git a/Documentation/source/readme.rst b/Documentation/source/readme.rst index ec5e0e0..ba77ae8 100644 --- a/Documentation/source/readme.rst +++ b/Documentation/source/readme.rst @@ -18,14 +18,14 @@ reindexing. Current state ------------- -This is still a very early alpha version. More information can be taken from Github at +This is still a very early beta version. More information can be taken from Github at `current issues`_ and `current projects`_. We are also focusing on Code Quality and Testing through `travis ci`_, `scrutinizer`_ and `codacy`_. -.. _current issues: https://github.com/DanielSiepmann/search_core/issues -.. _current projects: https://github.com/DanielSiepmann/search_core/projects -.. _travis ci: https://travis-ci.org/DanielSiepmann/search_core -.. _scrutinizer: https://scrutinizer-ci.com/g/DanielSiepmann/search_core/inspections -.. _codacy: https://www.codacy.com/app/daniel-siepmann/search_core/dashboard +.. _current issues: https://github.com/Codappix/search_core/issues +.. _current projects: https://github.com/Codappix/search_core/projects +.. _travis ci: https://travis-ci.org/Codappix/search_core +.. _scrutinizer: https://scrutinizer-ci.com/g/Codappix/search_core/inspections +.. _codacy: https://www.codacy.com/app/Codappix/search_core/dashboard diff --git a/Documentation/source/usage.rst b/Documentation/source/usage.rst index 1fc0e51..1ca50d2 100644 --- a/Documentation/source/usage.rst +++ b/Documentation/source/usage.rst @@ -11,12 +11,12 @@ Manual indexing You can trigger indexing from CLI:: - ./typo3/cli_dispatch.phpsh extbase index:index --table 'tt_content' + ./typo3/cli_dispatch.phpsh extbase index:index --identifier 'tt_content' This will index the table ``tt_content`` using the :ref:`TcaIndexer`. -Only one table per call is available, to index multiple tables just make multiple calls. -The tables have to be white listed through :ref:`allowedTables` option. +Only one index per call is available, to run multiple indexers, just make multiple calls. +The indexers have to be defined in TypoScript via :ref:`configuration_options_index`. .. _usage_auto_indexing: @@ -24,7 +24,7 @@ Auto indexing ------------- Indexing is done through hooks every time an record is changed. -The tables have to be white listed through :ref:`allowedTables` option. +The tables have to be configured via :ref:`configuration_options_index`. .. note:: @@ -37,3 +37,48 @@ Searching / Frontend Plugin To provide a search interface you can insert the frontend Plugin as normal content element of type plugin. The plugin is named *Search Core*. + +Please provide your own template, the extension will not deliver a useful template for now. + +The extbase mapping is used, this way you can create a form: + +.. code-block:: html + + + + + + +.. _usage_searching_filter: + +Filter +"""""" + +Thanks to extbase mapping, filter are added to the form: + +.. code-block:: html + + + + +.. _usage_searching_facets: + +Facets +"""""" + +To add a facet as criteria for searching, use :ref:`usage_searching_filter`. + +To display facet results use: + +.. code-block:: html + + + + + + + diff --git a/Makefile b/Makefile index 566709a..e8177b9 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,11 @@ functionalTests: --stop-on-error \ -c Tests/Functional/FunctionalTests.xml +unitTests: + TYPO3_PATH_WEB=$(TYPO3_WEB_DIR) \ + .Build/bin/phpunit --colors --debug -v \ + -c Tests/Unit/UnitTests.xml + uploadCodeCoverage: uploadCodeCoverageToScrutinizer uploadCodeCoverageToCodacy uploadCodeCoverageToScrutinizer: diff --git a/Tests/Functional/AbstractFunctionalTestCase.php b/Tests/Functional/AbstractFunctionalTestCase.php index c231784..7f808c7 100644 --- a/Tests/Functional/AbstractFunctionalTestCase.php +++ b/Tests/Functional/AbstractFunctionalTestCase.php @@ -1,5 +1,5 @@ diff --git a/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php b/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php index cdd8ddf..92f9db4 100644 --- a/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php +++ b/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php @@ -1,5 +1,5 @@ @@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase as BaseFunctionalTestCase; +use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase as BaseFunctionalTestCase; /** * All functional tests should extend this base class. @@ -43,11 +43,19 @@ abstract class AbstractFunctionalTestCase extends BaseFunctionalTestCase 'host' => getenv('ES_HOST') ?: \Elastica\Connection::DEFAULT_HOST, 'port' => getenv('ES_PORT') ?: \Elastica\Connection::DEFAULT_PORT, ]); + + // Start with clean system for test. + $this->cleanUp(); } public function tearDown() { - // Delete everything so next test starts clean. + // Make system clean again. + $this->cleanUp(); + } + + protected function cleanUp() + { $this->client->getIndex('_all')->delete(); $this->client->getIndex('_all')->clearCache(); } diff --git a/Tests/Functional/Connection/Elasticsearch/FilterTest.php b/Tests/Functional/Connection/Elasticsearch/FilterTest.php new file mode 100644 index 0000000..aa5a5dc --- /dev/null +++ b/Tests/Functional/Connection/Elasticsearch/FilterTest.php @@ -0,0 +1,94 @@ + + * + * 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 FilterTest extends AbstractFunctionalTestCase +{ + protected function getDataSets() + { + return array_merge( + parent::getDataSets(), + ['Tests/Functional/Fixtures/Searching/Filter.xml'] + ); + } + + /** + * @test + */ + public function itsPossibleToFilterResultsByASingleField() + { + \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('Search Word'); + + $result = $searchService->search($searchRequest); + $this->assertSame(2, count($result), 'Did not receive both indexed elements without filter.'); + + $searchRequest->setFilter(['CType' => 'HTML']); + $result = $searchService->search($searchRequest); + $this->assertSame('5', $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('Search Word'); + $result = $searchService->search($searchRequest); + + $this->assertSame(1, count($result->getFacets()), 'Did not receive the single defined facet.'); + + $facet = $result->getFacets()[0]; + $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[0]; + $this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.'); + $this->assertSame(1, $option->getCount(), 'Option did not have expected count.'); + $option = $options[1]; + $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/IndexTcaTableTest.php b/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php index 5e0c1a7..9678d83 100644 --- a/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php +++ b/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php @@ -1,5 +1,5 @@ @@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Index\IndexerFactory; use TYPO3\CMS\Extbase\Object\ObjectManager; /** @@ -61,7 +61,7 @@ class IndexTcaTableTest extends AbstractFunctionalTestCase /** * @test - * @expectedException \Leonmrni\SearchCore\Domain\Index\IndexingException + * @expectedException \Codappix\SearchCore\Domain\Index\IndexingException */ public function indexingNonConfiguredTableWillThrowException() { diff --git a/Tests/Functional/Fixtures/BasicSetup.ts b/Tests/Functional/Fixtures/BasicSetup.ts index 510e2a1..e24aee8 100644 --- a/Tests/Functional/Fixtures/BasicSetup.ts +++ b/Tests/Functional/Fixtures/BasicSetup.ts @@ -8,9 +8,23 @@ plugin { } } - indexer { - tca { - allowedTables = tt_content + indexing { + tt_content { + indexer = Codappix\SearchCore\Domain\Index\TcaIndexer + + mapping { + CType { + type = keyword + } + } + } + } + + searching { + facets { + contentTypes { + field = CType + } } } } diff --git a/Tests/Functional/Fixtures/Indexing/TcaIndexer/RespectRootLineBlacklist.ts b/Tests/Functional/Fixtures/Indexing/TcaIndexer/RespectRootLineBlacklist.ts index 44013e7..c0c1cd6 100644 --- a/Tests/Functional/Fixtures/Indexing/TcaIndexer/RespectRootLineBlacklist.ts +++ b/Tests/Functional/Fixtures/Indexing/TcaIndexer/RespectRootLineBlacklist.ts @@ -1,8 +1,8 @@ plugin { tx_searchcore { settings { - indexer { - tca { + indexing { + tt_content { rootLineBlacklist = 3 } } diff --git a/Tests/Functional/Fixtures/Indexing/UserWhereClause.ts b/Tests/Functional/Fixtures/Indexing/UserWhereClause.ts index d79bb14..7478ef1 100644 --- a/Tests/Functional/Fixtures/Indexing/UserWhereClause.ts +++ b/Tests/Functional/Fixtures/Indexing/UserWhereClause.ts @@ -1,11 +1,9 @@ plugin { tx_searchcore { settings { - indexer { - tca { - tt_content { - additionalWhereClause = tt_content.CType NOT IN ('div') - } + indexing { + tt_content { + additionalWhereClause = tt_content.CType NOT IN ('div') } } } diff --git a/Tests/Functional/Fixtures/Searching/Filter.xml b/Tests/Functional/Fixtures/Searching/Filter.xml new file mode 100644 index 0000000..103a4da --- /dev/null +++ b/Tests/Functional/Fixtures/Searching/Filter.xml @@ -0,0 +1,42 @@ + + + + 5 + 1 + 1480686370 + 1480686370 + 0 + 72 + html +
indexed content element with html ctype
+ Search Word + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +
+ + + 6 + 1 + 1480686370 + 1480686370 + 0 + 72 + header +
indexed content element with header ctype
+ Search Word + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +
+
diff --git a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php index a26021d..40b64ea 100644 --- a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php +++ b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php @@ -1,5 +1,5 @@ @@ -20,10 +20,11 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService; -use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook; -use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService; +use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook; +use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase; use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; @@ -42,7 +43,10 @@ abstract class AbstractDataHandlerTest extends AbstractFunctionalTestCase $objectManager = GeneralUtility::makeInstance(ObjectManager::class); $this->subject = $this->getMockBuilder(DataHandlerService::class) - ->setConstructorArgs([$objectManager->get(ConfigurationContainerInterface::class)]) + ->setConstructorArgs([ + $objectManager->get(ConfigurationContainerInterface::class), + $objectManager->get(IndexerFactory::class) + ]) ->setMethods(['add', 'update', 'delete']) ->getMock(); diff --git a/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php b/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php index 44f5186..b1676e3 100644 --- a/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php +++ b/Tests/Functional/Hooks/DataHandler/IgnoresUnkownOperationTest.php @@ -1,5 +1,5 @@ @@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService; -use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook; +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; diff --git a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php index 6073633..c33701d 100644 --- a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php +++ b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesTest.php @@ -1,5 +1,5 @@ @@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService; -use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook; +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; diff --git a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesWithMultipleTablesConfiguredTest.php b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesWithMultipleTablesConfiguredTest.php index 0bdf563..ebbee99 100644 --- a/Tests/Functional/Hooks/DataHandler/NonAllowedTablesWithMultipleTablesConfiguredTest.php +++ b/Tests/Functional/Hooks/DataHandler/NonAllowedTablesWithMultipleTablesConfiguredTest.php @@ -1,5 +1,5 @@ diff --git a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php index e9e834e..715fe29 100644 --- a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php +++ b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php @@ -1,5 +1,5 @@ @@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService; -use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook; +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; diff --git a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesWithMultipleTablesConfiguredTest.php b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesWithMultipleTablesConfiguredTest.php index f743bdb..c0b0aa3 100644 --- a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesWithMultipleTablesConfiguredTest.php +++ b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesWithMultipleTablesConfiguredTest.php @@ -1,5 +1,5 @@ diff --git a/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php b/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php index 360cacd..5577ac0 100644 --- a/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php +++ b/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php @@ -1,5 +1,5 @@ @@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Tests\Indexing\TcaIndexer; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Domain\Index\TcaIndexer\TcaTableService; -use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase; +use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService; +use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; diff --git a/Tests/Functional/Indexing/TcaIndexerTest.php b/Tests/Functional/Indexing/TcaIndexerTest.php index 056af39..2b3f817 100644 --- a/Tests/Functional/Indexing/TcaIndexerTest.php +++ b/Tests/Functional/Indexing/TcaIndexerTest.php @@ -1,5 +1,5 @@ @@ -20,12 +20,12 @@ namespace Leonmrni\SearchCore\Tests\Indexing; * 02110-1301, USA. */ -use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; -use Leonmrni\SearchCore\Connection\Elasticsearch; -use Leonmrni\SearchCore\Domain\Index\TcaIndexer; -use Leonmrni\SearchCore\Domain\Index\TcaIndexer\RelationResolver; -use Leonmrni\SearchCore\Domain\Index\TcaIndexer\TcaTableService; -use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase; +use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Connection\Elasticsearch; +use Codappix\SearchCore\Domain\Index\TcaIndexer; +use Codappix\SearchCore\Domain\Index\TcaIndexer\RelationResolver; +use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService; +use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase; use TYPO3\CMS\Extbase\Object\ObjectManager; class TcaIndexerTest extends AbstractFunctionalTestCase diff --git a/Tests/Unit/AbstractUnitTestCase.php b/Tests/Unit/AbstractUnitTestCase.php new file mode 100644 index 0000000..c11e74e --- /dev/null +++ b/Tests/Unit/AbstractUnitTestCase.php @@ -0,0 +1,46 @@ + + * + * 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\Tests\UnitTestCase as CoreTestCase; + +abstract class AbstractUnitTestCase extends CoreTestCase +{ + /** + * @return \TYPO3\CMS\Core\Log\LogManager + */ + protected function getMockedLogger() + { + $logger = $this->getMockBuilder(\TYPO3\CMS\Core\Log\LogManager::class) + ->disableOriginalConstructor() + ->setMethods(['getLogger']) + ->getMock(); + $logger->expects($this->once()) + ->method('getLogger') + ->will($this->returnValue( + $this->getMockBuilder(\TYPO3\CMS\Core\Log\Logger::class) + ->disableOriginalConstructor() + ->getMock() + )); + + return $logger; + } +} diff --git a/Tests/Unit/Command/IndexCommandControllerTest.php b/Tests/Unit/Command/IndexCommandControllerTest.php new file mode 100644 index 0000000..4c81672 --- /dev/null +++ b/Tests/Unit/Command/IndexCommandControllerTest.php @@ -0,0 +1,94 @@ + + * + * 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\Command\IndexCommandController; +use Codappix\SearchCore\Domain\Index\IndexerFactory; +use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException; +use Codappix\SearchCore\Domain\Index\TcaIndexer; +use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; +use TYPO3\CMS\Extbase\Mvc\Controller\CommandController; +use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException; + +class IndexCommandControllerTest extends AbstractUnitTestCase +{ + /** + * @var IndexCommandController + */ + protected $subject; + + /** + * @var IndexerFactory + */ + protected $indexerFactory; + + public function setUp() + { + parent::setUp(); + + $this->indexerFactory = $this->getMockBuilder(IndexerFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->subject = $this->getMockBuilder(IndexCommandController::class) + ->disableOriginalConstructor() + ->setMethods(['quit', 'outputLine']) + ->getMock(); + $this->subject->injectIndexerFactory($this->indexerFactory); + } + + /** + * @test + */ + public function indexerStopsForNonAllowedTable() + { + $this->subject->expects($this->once()) + ->method('outputLine') + ->with('No indexer found for: nonAllowedTable'); + $this->indexerFactory->expects($this->once()) + ->method('getIndexer') + ->with('nonAllowedTable') + ->will($this->throwException(new NoMatchingIndexerException)); + + $this->subject->indexCommand('nonAllowedTable'); + } + + /** + * @test + */ + public function indexerExecutesForAllowedTable() + { + $indexerMock = $this->getMockBuilder(TcaIndexer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->subject->expects($this->never()) + ->method('quit'); + $this->subject->expects($this->once()) + ->method('outputLine') + ->with('allowedTable was indexed.'); + $this->indexerFactory->expects($this->once()) + ->method('getIndexer') + ->with('allowedTable') + ->will($this->returnValue($indexerMock)); + + $this->subject->indexCommand('allowedTable'); + } +} diff --git a/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php b/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php new file mode 100644 index 0000000..9e88d4b --- /dev/null +++ b/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php @@ -0,0 +1,85 @@ + + * + * 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\Index\TcaIndexer\TcaTableService; +use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; + +class TcaTableServiceTest extends AbstractUnitTestCase +{ + /** + * @var TcaTableService + */ + protected $subject; + + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + + public function setUp() + { + parent::setUp(); + + $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); + + $this->subject = $this->getMockBuilder(TcaTableService::class) + ->disableOriginalConstructor() + ->setMethodsExcept(['getWhereClause', 'injectLogger', 'getTableName']) + ->getMock(); + $this->inject($this->subject, 'configuration', $this->configuration); + $this->inject($this->subject, 'logger', $this->getMockedLogger()); + $this->inject($this->subject, 'tableName', 'table'); + } + + /** + * @test + */ + public function doUsePlainQueryIfNoAdditionalWhereClauseIsDefined() + { + $this->configuration->expects($this->exactly(2)) + ->method('getIfExists') + ->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist']) + ->will($this->onConsecutiveCalls(null, false)); + + $this->assertSame( + '1=1 AND pages.no_search = 0', + $this->subject->getWhereClause() + ); + } + + /** + * @test + */ + public function configuredAdditionalWhereClauseIsAdded() + { + $this->configuration->expects($this->exactly(2)) + ->method('getIfExists') + ->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist']) + ->will($this->onConsecutiveCalls('table.field = "someValue"', false)); + + $this->assertSame( + '1=1 AND pages.no_search = 0 AND table.field = "someValue"', + $this->subject->getWhereClause() + ); + } +} diff --git a/Tests/Unit/Domain/Search/QueryFactoryTest.php b/Tests/Unit/Domain/Search/QueryFactoryTest.php new file mode 100644 index 0000000..32c8fbd --- /dev/null +++ b/Tests/Unit/Domain/Search/QueryFactoryTest.php @@ -0,0 +1,148 @@ + + * + * 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\FacetRequest; +use Codappix\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Domain\Search\QueryFactory; +use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; + +class QueryFactoryTest extends AbstractUnitTestCase +{ + /** + * @var QueryFactory + */ + protected $subject; + + public function setUp() + { + parent::setUp(); + + $this->subject = new QueryFactory($this->getMockedLogger()); + } + + /** + * @test + */ + public function creatonOfQueryWorksInGeneral() + { + $searchRequest = new SearchRequest('SearchWord'); + + $query = $this->subject->create($searchRequest); + $this->assertInstanceOf( + \Elastica\Query::class, + $query, + 'Factory did not create the expected instance.' + ); + } + + /** + * @test + */ + public function filterIsAddedToQuery() + { + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->setFilter(['field' => 'content']); + + $query = $this->subject->create($searchRequest); + $this->assertSame( + [ + ['term' => ['field' => 'content']] + ], + $query->toArray()['query']['bool']['filter'], + 'Filter was not added to query.' + ); + } + + /** + * @test + */ + public function emptyFilterIsNotAddedToQuery() + { + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->setFilter([ + 'field' => '', + 'field1' => 0, + 'field2' => false, + ]); + + $this->assertFalse( + $searchRequest->hasFilter(), + 'Search request contains filter even if it should not.' + ); + + $query = $this->subject->create($searchRequest); + $this->assertSame( + null, + $query->toArray()['query']['bool']['filter'], + 'Filter was added to query, even if no filter exists.' + ); + } + + /** + * @test + */ + public function userInputIsAlwaysString() + { + $searchRequest = new SearchRequest(10); + $searchRequest->setFilter(['field' => 20]); + + $query = $this->subject->create($searchRequest); + $this->assertSame( + '10', + $query->toArray()['query']['bool']['must'][0]['match']['_all'], + 'Search word was not escaped as expected.' + ); + $this->assertSame( + '20', + $query->toArray()['query']['bool']['filter'][0]['term']['field'], + 'Search word was not escaped as expected.' + ); + } + + /** + * @test + */ + public function facetsAreAddedToQuery() + { + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->addFacet(new FacetRequest('Identifier', 'FieldName')); + $searchRequest->addFacet(new FacetRequest('Identifier 2', 'FieldName 2')); + + $query = $this->subject->create($searchRequest); + $this->assertSame( + [ + 'Identifier' => [ + 'terms' => [ + 'field' => 'FieldName', + ], + ], + 'Identifier 2' => [ + 'terms' => [ + 'field' => 'FieldName 2', + ], + ], + ], + $query->toArray()['aggs'], + 'Facets were not added to query.' + ); + } +} diff --git a/Tests/Unit/UnitTests.xml b/Tests/Unit/UnitTests.xml new file mode 100644 index 0000000..6456405 --- /dev/null +++ b/Tests/Unit/UnitTests.xml @@ -0,0 +1,28 @@ + + + + + . + + + + + + ../../Classes + + + diff --git a/composer.json b/composer.json index 2aba101..e0da7a1 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,17 @@ { - "name": "leonmrni/search_core", + "name": "codappix/search_core", "type": "typo3-cms-extension", - "description": "Leonmrni Search Core.", - "homepage": "http://www.leonmrni.com", + "description": "Codappix Search Core.", + "homepage": "https://github.com/Codappix/search_core", "license": ["GPL-2.0+"], "autoload": { "psr-4": { - "Leonmrni\\SearchCore\\": "Classes" + "Codappix\\SearchCore\\": "Classes" } }, "autoload-dev": { "psr-4": { - "Leonmrni\\SearchCore\\Tests\\": "Tests/", + "Codappix\\SearchCore\\Tests\\": "Tests/", "TYPO3\\CMS\\Core\\Tests\\": ".Build/vendor/typo3/cms/typo3/sysext/core/Tests/" } }, diff --git a/ext_emconf.php b/ext_emconf.php index 3aab0c7..118e2df 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -4,6 +4,7 @@ $EM_CONF[$_EXTKEY] = [ 'title' => 'Search Core', 'description' => 'Search core for implementing various search types.', 'category' => 'be', + 'clearCacheOnLoad' => 1, 'constraints' => [ 'depends' => [ 'typo3' => '7.6.0-7.6.99', @@ -13,12 +14,11 @@ $EM_CONF[$_EXTKEY] = [ ], 'autoload' => [ 'psr-4' => [ - 'Leonmrni\\SearchCore\\' => 'Classes', + 'Codappix\\SearchCore\\' => 'Classes', ], ], - 'state' => 'alpha', - 'clearCacheOnLoad' => 1, + 'state' => 'beta', + 'version' => '1.0.0', 'author' => 'Daniel Siepmann', 'author_email' => 'coding@daniel-siepmann.de', - 'version' => '1.0.0', ]; diff --git a/ext_localconf.php b/ext_localconf.php index 7495d73..1c1f9cd 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -11,15 +11,15 @@ call_user_func( 'SC_OPTIONS' => [ 'extbase' => [ 'commandControllers' => [ - Leonmrni\SearchCore\Command\IndexCommandController::class, + Codappix\SearchCore\Command\IndexCommandController::class, ], ], 't3lib/class.t3lib_tcemain.php' => [ 'processCmdmapClass' => [ - $extensionKey => '&' . \Leonmrni\SearchCore\Hook\DataHandler::class, + $extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class, ], 'processDatamapClass' => [ - $extensionKey => '&' . \Leonmrni\SearchCore\Hook\DataHandler::class, + $extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class, ], ], ], @@ -27,7 +27,7 @@ call_user_func( ); TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - 'Leonmrni.' . $extensionKey, + 'Codappix.' . $extensionKey, 'search', [ 'Search' => 'search' @@ -39,8 +39,8 @@ call_user_func( \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Object\Container\Container') ->registerImplementation( - 'Leonmrni\SearchCore\Connection\ConnectionInterface', - 'Leonmrni\SearchCore\Connection\Elasticsearch' + 'Codappix\SearchCore\Connection\ConnectionInterface', + 'Codappix\SearchCore\Connection\Elasticsearch' ); }, $_EXTKEY diff --git a/ext_tables.php b/ext_tables.php index d6d07ed..1a54787 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -7,7 +7,7 @@ ); TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'Leonmrni.' . $_EXTKEY, + 'Codappix.' . $_EXTKEY, 'search', 'Search Core' );