diff --git a/Classes/Connection/Elasticsearch.php b/Classes/Connection/Elasticsearch.php index f6321bc..4c66b6a 100644 --- a/Classes/Connection/Elasticsearch.php +++ b/Classes/Connection/Elasticsearch.php @@ -189,7 +189,7 @@ class Elasticsearch implements Singleton, ConnectionInterface $search->addIndex('typo3content'); $search->setQuery($this->queryFactory->create($searchRequest)); - return $this->objectManager->get(SearchResult::class, $search->search()); + return $this->objectManager->get(SearchResult::class, $searchRequest, $search->search()); } /** diff --git a/Classes/Connection/Elasticsearch/DocumentFactory.php b/Classes/Connection/Elasticsearch/DocumentFactory.php index 99d29e5..390c592 100644 --- a/Classes/Connection/Elasticsearch/DocumentFactory.php +++ b/Classes/Connection/Elasticsearch/DocumentFactory.php @@ -61,7 +61,10 @@ class DocumentFactory implements Singleton $identifier = $document['search_identifier']; unset($document['search_identifier']); - $this->logger->debug('Convert document to document', [$identifier, $document]); + $this->logger->debug( + sprintf('Convert %s %u to document.', $documentType, $identifier), + [$identifier, $document] + ); return new \Elastica\Document($identifier, $document); } diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 486b6eb..f45f02f 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -22,11 +22,17 @@ namespace Codappix\SearchCore\Connection\Elasticsearch; use Codappix\SearchCore\Connection\FacetInterface; use Codappix\SearchCore\Connection\ResultItemInterface; +use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; class SearchResult implements SearchResultInterface { + /** + * @var SearchRequestInterface + */ + protected $searchRequest; + /** * @var \Elastica\ResultSet */ @@ -42,13 +48,24 @@ class SearchResult implements SearchResultInterface */ protected $results = []; + /** + * For Iterator interface. + * + * @var int + */ + protected $position = 0; + /** * @var ObjectManagerInterface */ protected $objectManager; - public function __construct(\Elastica\ResultSet $result, ObjectManagerInterface $objectManager) - { + public function __construct( + SearchRequestInterface $searchRequest, + \Elastica\ResultSet $result, + ObjectManagerInterface $objectManager + ) { + $this->searchRequest = $searchRequest; $this->result = $result; $this->objectManager = $objectManager; } @@ -75,53 +92,11 @@ class SearchResult implements SearchResultInterface 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() + public function getCurrentCount() { 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 !== []) { @@ -143,4 +118,76 @@ class SearchResult implements SearchResultInterface $this->facets[$aggregationName] = $this->objectManager->get(Facet::class, $aggregationName, $aggregation); } } + + // Countable - Interface + public function count() + { + return $this->result->getTotalHits(); + } + + // Iterator - Interface + public function current() + { + return $this->getResults()[$this->position]; + } + + public function next() + { + ++$this->position; + + return $this->current(); + } + + public function key() + { + return $this->position; + } + + public function valid() + { + return isset($this->getResults()[$this->position]); + } + + public function rewind() + { + $this->position = 0; + } + + // Extbase QueryResultInterface - Implemented to support Pagination of Fluid. + + public function getQuery() + { + return $this->searchRequest; + } + + public function getFirst() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195121); + } + + public function toArray() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502195135); + } + + public function offsetExists($offset) + { + // Return false to allow Fluid to use appropriate getter methods. + return false; + } + + public function offsetGet($offset) + { + throw new \BadMethodCallException('Use getter to fetch properties.', 1502196933); + } + + public function offsetSet($offset, $value) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196934); + } + + public function offsetUnset($offset) + { + throw new \BadMethodCallException('You are not allowed to modify the result.', 1502196936); + } } diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index 7ec34ff..7c7956e 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -20,10 +20,9 @@ namespace Codappix\SearchCore\Connection; * 02110-1301, USA. */ -/** - * - */ -interface SearchRequestInterface +use TYPO3\CMS\Extbase\Persistence\QueryInterface; + +interface SearchRequestInterface extends QueryInterface { /** * Returns the actual string the user searched for. @@ -41,11 +40,4 @@ interface SearchRequestInterface * @return array */ public function getFilter(); - - /** - * Defines how many results should be fetched. - * - * @return int - */ - public function getSize(); } diff --git a/Classes/Connection/SearchResultInterface.php b/Classes/Connection/SearchResultInterface.php index 5c45bcd..d52a8ba 100644 --- a/Classes/Connection/SearchResultInterface.php +++ b/Classes/Connection/SearchResultInterface.php @@ -20,10 +20,12 @@ namespace Codappix\SearchCore\Connection; * 02110-1301, USA. */ +use TYPO3\CMS\Extbase\Persistence\QueryResultInterface; + /** * A search result. */ -interface SearchResultInterface extends \Iterator, \Countable +interface SearchResultInterface extends \Iterator, \Countable, QueryResultInterface { /** * @return array @@ -38,18 +40,9 @@ interface SearchResultInterface extends \Iterator, \Countable public function getFacets(); /** - * Returns the total sum of matching results. + * Returns the number of results in current result * * @return int */ - public function getTotalCount(); - - // Countable - Interface - - /** - * Returns the total sum of results contained in this result. - * - * @return int - */ - public function count(); + public function getCurrentCount(); } diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index 068f9a1..0fa7f73 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -44,6 +44,20 @@ class SearchController extends ActionController parent::__construct(); } + public function initializeSearchAction() + { + if (isset($this->settings['searching']['mode']) && $this->settings['searching']['mode'] === 'filter' + && $this->request->hasArgument('searchRequest') === false + ) { + $this->request->setArguments(array_merge( + $this->request->getArguments(), + [ + 'searchRequest' => $this->objectManager->get(SearchRequest::class), + ] + )); + } + } + /** * Process a search and deliver original request and result to view. * diff --git a/Classes/Domain/Index/TcaIndexer/TcaTableService.php b/Classes/Domain/Index/TcaIndexer/TcaTableService.php index b5f48ab..70bb52d 100644 --- a/Classes/Domain/Index/TcaIndexer/TcaTableService.php +++ b/Classes/Domain/Index/TcaIndexer/TcaTableService.php @@ -24,6 +24,8 @@ use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Domain\Index\IndexingException; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\RootlineUtility; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** * Encapsulate logik related to TCA configuration. @@ -47,15 +49,20 @@ class TcaTableService */ protected $configuration; + /** + * @var RelationResolver + */ + protected $relationResolver; + /** * @var \TYPO3\CMS\Core\Log\Logger */ protected $logger; /** - * @var RelationResolver + * @var ObjectManagerInterface */ - protected $relationResolver; + protected $objectManager; /** * Inject log manager to get concrete logger from it. @@ -67,6 +74,14 @@ class TcaTableService $this->logger = $logManager->getLogger(__CLASS__); } + /** + * @param ObjectManagerInterface $objectManager + */ + public function injectObjectManager(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + /** * @param string $tableName * @param ConfigurationContainerInterface $configuration @@ -243,26 +258,59 @@ class TcaTableService * Checks whether the given record was blacklisted by root line. * This can be configured by typoscript as whole root lines can be black listed. * - * NOTE: Does not support pages yet. We have to add a switch once we - * support them to use uid instead. + * Also further TYPO3 mechanics are taken into account. Does a valid root + * line exist, is page inside a recycler, is inherited start- endtime + * excluded, etc. * * @param array &$record * @return bool */ protected function isRecordBlacklistedByRootline(array &$record) { - // If no rootline exists, the record is on a unreachable page and therefore blacklisted. - $rootline = BackendUtility::BEgetRootLine($record['pid']); - if (!isset($rootline[0])) { + $pageUid = $record['pid']; + if ($this->tableName === 'pages') { + $pageUid = $record['uid']; + } + + try { + $rootline = $this->objectManager->get(RootlineUtility::class, $pageUid)->get(); + } catch (\RuntimeException $e) { + $this->logger->notice( + sprintf('Could not fetch rootline for page %u, because: %s', $pageUid, $e->getMessage()), + [$record, $e] + ); return true; } - // Check configured black list if present. - if ($this->isBlackListedRootLineConfigured()) { - foreach ($rootline as $pageInRootLine) { - if (in_array($pageInRootLine['uid'], $this->getBlackListedRootLine())) { - return true; - } + foreach ($rootline as $pageInRootLine) { + // Check configured black list if present. + if ($this->isBlackListedRootLineConfigured() + && in_array($pageInRootLine['uid'], $this->getBlackListedRootLine()) + ) { + $this->logger->info( + sprintf( + 'Record %u is black listed due to configured root line configuration of page %u.', + $record['uid'], + $pageInRootLine['uid'] + ), + [$record, $pageInRootLine] + ); + return true; + } + + if ($pageInRootLine['extendToSubpages'] && ( + ($pageInRootLine['endtime'] > 0 && $pageInRootLine['endtime'] <= time()) + || ($pageInRootLine['starttime'] > 0 && $pageInRootLine['starttime'] >= time()) + )) { + $this->logger->info( + sprintf( + 'Record %u is black listed due to configured timing of parent page %u.', + $record['uid'], + $pageInRootLine['uid'] + ), + [$record, $pageInRootLine] + ); + return true; } } diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index 7850b98..39e477c 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -20,6 +20,7 @@ namespace Codappix\SearchCore\Domain\Model; * 02110-1301, USA. */ +use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\FacetRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; @@ -35,11 +36,6 @@ class SearchRequest implements SearchRequestInterface */ protected $query = ''; - /** - * @var int - */ - protected $size = 10; - /** * @var array */ @@ -50,10 +46,27 @@ class SearchRequest implements SearchRequestInterface */ protected $facets = []; + /** + * @var int + */ + protected $offset = 0; + + /** + * @var int + */ + protected $limit = 10; + + /** + * Used for QueryInterface implementation to allow execute method to work. + * + * @var ConnectionInterface + */ + protected $connection = null; + /** * @param string $query */ - public function __construct($query) + public function __construct($query = '') { $this->query = (string) $query; } @@ -119,18 +132,162 @@ class SearchRequest implements SearchRequestInterface } /** - * @return int + * Define connection to use for this request. + * Necessary to allow implementation of execute for interface. + * + * @param ConnectionInterface $connection */ - public function getSize() + public function setConnection(ConnectionInterface $connection) { - return $this->size; + $this->connection = $connection; } - /** - * @param int $size - */ - public function setSize($size) + // Extbase QueryInterface + // Current implementation covers only paginate widget support. + public function execute($returnRawQueryResult = false) { - $this->size = (int) $size; + if ($this->connection instanceof ConnectionInterface) { + return $this->connection->search($this); + } + + throw new \InvalidArgumentException( + 'Connection was not set before, therefore execute can not work. Use `setConnection` before.', + 1502197732 + ); + } + + public function setLimit($limit) + { + $this->limit = (int) $limit; + } + + public function setOffset($offset) + { + $this->offset = (int) $offset; + } + + public function getLimit() + { + return $this->limit; + } + + public function getOffset() + { + return $this->offset; + } + + public function getSource() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196146); + } + + public function setOrderings(array $orderings) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196163); + } + + public function matching($constraint) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196197); + } + + public function logicalAnd($constraint1) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196166); + } + + public function logicalOr($constraint1) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196198); + } + + public function logicalNot(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196166); + } + + public function equals($propertyName, $operand, $caseSensitive = true) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196199); + } + + public function like($propertyName, $operand, $caseSensitive = true) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196167); + } + + public function contains($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196200); + } + + public function in($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196167); + } + + public function lessThan($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196201); + } + + public function lessThanOrEqual($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196168); + } + + public function greaterThan($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196202); + } + + public function greaterThanOrEqual($propertyName, $operand) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196168); + } + + public function getType() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196203); + } + + public function setQuerySettings(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface $querySettings) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196168); + } + + public function getQuerySettings() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196205); + } + + public function count() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196169); + } + + public function getOrderings() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196206); + } + + public function getConstraint() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196171); + } + + public function isEmpty($propertyName) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196207); + } + + public function setSource(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source) + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196172); + } + + public function getStatement() + { + throw new \BadMethodCallException('Method is not implemented yet.', 1502196208); } } diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index ce702f9..9455f80 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -39,11 +39,6 @@ class QueryFactory */ protected $configuration; - /** - * @var array - */ - protected $query = []; - /** * @param \TYPO3\CMS\Core\Log\LogManager $logManager * @param ConfigurationContainerInterface $configuration @@ -77,46 +72,53 @@ class QueryFactory */ protected function createElasticaQuery(SearchRequestInterface $searchRequest) { - $this->addSize($searchRequest); - $this->addSearch($searchRequest); - $this->addBoosts($searchRequest); - $this->addFilter($searchRequest); - $this->addFacets($searchRequest); + $query = []; + $this->addSize($searchRequest, $query); + $this->addSearch($searchRequest, $query); + $this->addBoosts($searchRequest, $query); + $this->addFilter($searchRequest, $query); + $this->addFacets($searchRequest, $query); // Use last, as it might change structure of query. // Better approach would be something like DQL to generate query and build result in the end. - $this->addFactorBoost(); + $this->addFactorBoost($query); - $this->logger->debug('Generated elasticsearch query.', [$this->query]); - return new \Elastica\Query($this->query); + $this->logger->debug('Generated elasticsearch query.', [$query]); + return new \Elastica\Query($query); } /** * @param SearchRequestInterface $searchRequest + * @param array $query */ - protected function addSize(SearchRequestInterface $searchRequest) + protected function addSize(SearchRequestInterface $searchRequest, array &$query) { - $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ - 'from' => 0, - 'size' => $searchRequest->getSize(), + $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ + 'from' => $searchRequest->getOffset(), + 'size' => $searchRequest->getLimit(), ]); } /** * @param SearchRequestInterface $searchRequest + * @param array $query */ - protected function addSearch(SearchRequestInterface $searchRequest) + protected function addSearch(SearchRequestInterface $searchRequest, array &$query) { - $this->query = ArrayUtility::setValueByPath( - $this->query, + if (trim($searchRequest->getSearchTerm()) === '') { + return; + } + + $query = ArrayUtility::setValueByPath( + $query, 'query.bool.must.0.match._all.query', $searchRequest->getSearchTerm() ); $minimumShouldMatch = $this->configuration->getIfExists('searching.minimumShouldMatch'); if ($minimumShouldMatch) { - $this->query = ArrayUtility::setValueByPath( - $this->query, + $query = ArrayUtility::setValueByPath( + $query, 'query.bool.must.0.match._all.minimum_should_match', $minimumShouldMatch ); @@ -125,8 +127,9 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest + * @param array $query */ - protected function addBoosts(SearchRequestInterface $searchRequest) + protected function addBoosts(SearchRequestInterface $searchRequest, array &$query) { try { $fields = $this->configuration->get('searching.boost'); @@ -147,7 +150,7 @@ class QueryFactory ]; } - $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ 'query' => [ 'bool' => [ 'should' => $boostQueryParts, @@ -156,12 +159,15 @@ class QueryFactory ]); } - protected function addFactorBoost() + /** + * @param array $query + */ + protected function addFactorBoost(array &$query) { try { - $this->query['query'] = [ + $query['query'] = [ 'function_score' => [ - 'query' => $this->query['query'], + 'query' => $query['query'], 'field_value_factor' => $this->configuration->get('searching.fieldValueFactor'), ], ]; @@ -172,8 +178,9 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest + * @param array $query */ - protected function addFilter(SearchRequestInterface $searchRequest) + protected function addFilter(SearchRequestInterface $searchRequest, array &$query) { if (! $searchRequest->hasFilter()) { return; @@ -188,7 +195,7 @@ class QueryFactory ]; } - $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ 'query' => [ 'bool' => [ 'filter' => $terms, @@ -199,11 +206,12 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest + * @param array $query */ - protected function addFacets(SearchRequestInterface $searchRequest) + protected function addFacets(SearchRequestInterface $searchRequest, array &$query) { foreach ($searchRequest->getFacets() as $facet) { - $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ 'aggs' => [ $facet->getIdentifier() => [ 'terms' => [ diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index bb8b138..384f6aa 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -69,8 +69,10 @@ class SearchService */ public function search(SearchRequestInterface $searchRequest) { + $searchRequest->setConnection($this->connection); $this->addSize($searchRequest); $this->addConfiguredFacets($searchRequest); + $this->addConfiguredFilters($searchRequest); return $this->connection->search($searchRequest); } @@ -82,7 +84,7 @@ class SearchService */ protected function addSize(SearchRequestInterface $searchRequest) { - $searchRequest->setSize( + $searchRequest->setLimit( $this->configuration->getIfExists('searching.size') ?: 10 ); } @@ -112,4 +114,21 @@ class SearchService )); } } + + /** + * Add filters from configuration, e.g. flexform or TypoScript. + * + * @param SearchRequestInterface $searchRequest + */ + protected function addConfiguredFilters(SearchRequestInterface $searchRequest) + { + try { + $searchRequest->setFilter(array_merge( + $searchRequest->getFilter(), + $this->configuration->get('searching.filter') + )); + } catch (InvalidArgumentException $e) { + // Nothing todo, no filter configured. + } + } } diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php new file mode 100644 index 0000000..6a976c6 --- /dev/null +++ b/Configuration/TCA/Overrides/tt_content.php @@ -0,0 +1,3 @@ +setFilter(['CType' => 'HTML']); $result = $searchService->search($searchRequest); - $this->assertSame('5', $result->getResults()[0]['uid'], 'Did not get the expected result entry.'); + $this->assertSame(5, (int) $result->getResults()[0]['uid'], 'Did not get the expected result entry.'); $this->assertSame(1, count($result), 'Did not receive the single filtered element.'); } diff --git a/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php b/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php index e095f5f..02e6ca3 100644 --- a/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php +++ b/Tests/Functional/Connection/Elasticsearch/IndexTcaTableTest.php @@ -59,6 +59,29 @@ class IndexTcaTableTest extends AbstractFunctionalTestCase ); } + /** + * @test + */ + public function indexSingleBasicTtContent() + { + \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class) + ->get(IndexerFactory::class) + ->getIndexer('tt_content') + ->indexDocument(6) + ; + + $response = $this->client->request('typo3content/_search?q=*:*'); + + $this->assertTrue($response->isOK(), 'Elastica did not answer with ok code.'); + $this->assertSame($response->getData()['hits']['total'], 1, 'Not exactly 1 document was indexed.'); + $this->assertArraySubset( + ['_source' => ['header' => 'indexed content element']], + $response->getData()['hits']['hits'][0], + false, + 'Record was not indexed.' + ); + } + /** * @test * @expectedException \Codappix\SearchCore\Domain\Index\IndexingException diff --git a/Tests/Functional/Fixtures/Indexing/IndexTcaTable.xml b/Tests/Functional/Fixtures/Indexing/IndexTcaTable.xml index c236f07..3692135 100644 --- a/Tests/Functional/Fixtures/Indexing/IndexTcaTable.xml +++ b/Tests/Functional/Fixtures/Indexing/IndexTcaTable.xml @@ -29,7 +29,7 @@ 72 header
endtime hidden record
- + Some content 0 0 0 @@ -49,7 +49,7 @@ 72 header
Hidden record
- + Some content 0 0 0 diff --git a/Tests/Functional/Fixtures/Indexing/PagesIndexer/BrokenRootLine.xml b/Tests/Functional/Fixtures/Indexing/PagesIndexer/BrokenRootLine.xml new file mode 100644 index 0000000..81f2316 --- /dev/null +++ b/Tests/Functional/Fixtures/Indexing/PagesIndexer/BrokenRootLine.xml @@ -0,0 +1,20 @@ + + + + + 3 + 2 + Some disabled page due broken root line + + + 4 + 3 + Some disabled page due to parent pages root line being broken + + + + 6 + 1 + Some enabled page due valid root line + + diff --git a/Tests/Functional/Fixtures/Indexing/PagesIndexer/InheritedTiming.xml b/Tests/Functional/Fixtures/Indexing/PagesIndexer/InheritedTiming.xml new file mode 100644 index 0000000..552ba74 --- /dev/null +++ b/Tests/Functional/Fixtures/Indexing/PagesIndexer/InheritedTiming.xml @@ -0,0 +1,34 @@ + + + + + 2 + 1 + Some disabled page due to timing + 1502186635 + 1 + + + 3 + 2 + Some disabled page due to inherited timing + + + 4 + 1 + Some disabled page due to timing + 2147483647 + 1 + + + 5 + 4 + Some disabled page due to inherited timing + + + + 6 + 1 + Some enabled page due to no be below inherited disabled timing + + diff --git a/Tests/Functional/Fixtures/Indexing/PagesIndexer/Recycler.xml b/Tests/Functional/Fixtures/Indexing/PagesIndexer/Recycler.xml new file mode 100644 index 0000000..1421ad5 --- /dev/null +++ b/Tests/Functional/Fixtures/Indexing/PagesIndexer/Recycler.xml @@ -0,0 +1,21 @@ + + + + + 2 + 1 + Some disabled page due being recycler + 255 + + + 3 + 2 + Some disabled page due to parent page being recycler + + + + 6 + 1 + Some enabled page due to no be below recycler + + diff --git a/Tests/Functional/Fixtures/Indexing/UserWhereClause.xml b/Tests/Functional/Fixtures/Indexing/UserWhereClause.xml index 12347ef..b9d9c7c 100644 --- a/Tests/Functional/Fixtures/Indexing/UserWhereClause.xml +++ b/Tests/Functional/Fixtures/Indexing/UserWhereClause.xml @@ -9,7 +9,7 @@ 72 header
Also indexable record
- + Some content 0 0 0 @@ -29,7 +29,7 @@ 72 div
Not indexed by user where ctype condition
- + Some content 0 0 0 diff --git a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php index 40b64ea..84ac0bd 100644 --- a/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php +++ b/Tests/Functional/Hooks/DataHandler/AbstractDataHandlerTest.php @@ -50,7 +50,6 @@ abstract class AbstractDataHandlerTest extends AbstractFunctionalTestCase ->setMethods(['add', 'update', 'delete']) ->getMock(); - // This way TYPO3 will use our mock instead of a new instance. - $GLOBALS['T3_VAR']['getUserObj']['&' . DataHandlerHook::class] = new DataHandlerHook($this->subject); + GeneralUtility::setSingletonInstance(DataHandlerHook::class, new DataHandlerHook($this->subject)); } } diff --git a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php index 715fe29..6276616 100644 --- a/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php +++ b/Tests/Functional/Hooks/DataHandler/ProcessesAllowedTablesTest.php @@ -47,7 +47,8 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest */ public function deletionWillBeTriggeredForTtContent() { - $this->subject->expects($this->exactly(1))->method('delete') + $this->subject->expects($this->exactly(1)) + ->method('delete') ->with($this->equalTo('tt_content'), $this->equalTo('1')); $tce = GeneralUtility::makeInstance(Typo3DataHandler::class); @@ -71,9 +72,9 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest ->with( $this->equalTo('tt_content'), $this->callback(function ($record) { - return isset($record['uid']) && $record['uid'] === '1' - && isset($record['pid']) && $record['pid'] === '1' - && isset($record['colPos']) && $record['colPos'] === '1' + return isset($record['uid']) && $record['uid'] == 1 + && isset($record['pid']) && $record['pid'] == 1 + && isset($record['colPos']) && $record['colPos'] == 1 ; }) ); @@ -99,9 +100,9 @@ class ProcessesAllowedTablesTest extends AbstractDataHandlerTest ->with( $this->equalTo('tt_content'), $this->callback(function ($record) { - return isset($record['uid']) && $record['uid'] === 2 - && isset($record['pid']) && $record['pid'] === 1 - && isset($record['header']) && $record['header'] === 'a new record' + return isset($record['uid']) && $record['uid'] == 2 + && isset($record['pid']) && $record['pid'] == 1 + && isset($record['header']) && $record['header'] == 'a new record' ; }) ); diff --git a/Tests/Functional/Indexing/PagesIndexerTest.php b/Tests/Functional/Indexing/PagesIndexerTest.php index d0440ba..244413d 100644 --- a/Tests/Functional/Indexing/PagesIndexerTest.php +++ b/Tests/Functional/Indexing/PagesIndexerTest.php @@ -61,4 +61,44 @@ class PagesIndexerTest extends AbstractFunctionalTestCase $this->inject($indexer, 'connection', $connection); $indexer->indexAllDocuments(); } + + /** + * @test + * @dataProvider rootLineDataSets + * @param string $dataSetPath + */ + public function rootLineIsRespectedDuringIndexing($dataSetPath) + { + $this->importDataSet($dataSetPath); + + $objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class); + $tableName = 'pages'; + + $connection = $this->getMockBuilder(Elasticsearch::class) + ->setMethods(['addDocuments']) + ->disableOriginalConstructor() + ->getMock(); + + $connection->expects($this->once()) + ->method('addDocuments') + ->with( + $this->stringContains($tableName), + $this->callback(function ($documents) { + return count($documents) === 2; + }) + ); + + $indexer = $objectManager->get(IndexerFactory::class)->getIndexer($tableName); + $this->inject($indexer, 'connection', $connection); + $indexer->indexAllDocuments(); + } + + public function rootLineDataSets() + { + return [ + 'Broken root line' => ['Tests/Functional/Fixtures/Indexing/PagesIndexer/BrokenRootLine.xml'], + 'Recycler doktype' => ['Tests/Functional/Fixtures/Indexing/PagesIndexer/Recycler.xml'], + 'Extended timing to sub pages' => ['Tests/Functional/Fixtures/Indexing/PagesIndexer/InheritedTiming.xml'], + ]; + } } diff --git a/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php b/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php index 984b6ff..a491677 100644 --- a/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php +++ b/Tests/Functional/Indexing/TcaIndexer/RelationResolverTest.php @@ -37,10 +37,6 @@ class RelationResolverTest extends AbstractFunctionalTestCase $objectManager = GeneralUtility::makeInstance(ObjectManager::class); $table = 'sys_file'; - // Only by adding the field to showitem, it will be processed by FormEngine. - // We use this field to test inline relations, as there is only one alternative. - $GLOBALS['TCA']['sys_file']['types'][1]['showitem'] .= ',metadata'; - $subject = $objectManager->get(TcaTableService::class, $table); $record = BackendUtility::getRecord($table, 1); $subject->prepareRecord($record); diff --git a/Tests/Unit/Controller/SearchControllerTest.php b/Tests/Unit/Controller/SearchControllerTest.php new file mode 100644 index 0000000..67c6d98 --- /dev/null +++ b/Tests/Unit/Controller/SearchControllerTest.php @@ -0,0 +1,128 @@ + + * + * 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\Controller\SearchController; +use Codappix\SearchCore\Domain\Model\SearchRequest; +use Codappix\SearchCore\Domain\Search\SearchService; +use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; +use TYPO3\CMS\Extbase\Mvc\Web\Request; +use TYPO3\CMS\Extbase\Object\ObjectManager; + +class SearchControllerTest extends AbstractUnitTestCase +{ + /** + * @var SearchController + */ + protected $subject; + + /** + * @var Request + */ + protected $request; + + public function setUp() + { + \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance( + \TYPO3\CMS\Core\Cache\CacheManager::class + )->setCacheConfigurations([ + 'extbase_object' => [ + 'backend' => \TYPO3\CMS\Core\Cache\Backend\NullBackend::class, + ], + 'extbase_datamapfactory_datamap' => [ + 'backend' => \TYPO3\CMS\Core\Cache\Backend\NullBackend::class, + ], + ]); + + parent::setUp(); + + $searchService = $this->getMockBuilder(SearchService::class) + ->disableOriginalConstructor() + ->getMock(); + $this->request = new Request(); + + $this->subject = new SearchController($searchService); + $this->inject($this->subject, 'request', $this->request); + $this->inject($this->subject, 'objectManager', new ObjectManager()); + } + + /** + * @test + */ + public function searchRequestArgumentIsAddedIfModeIsFilterAndArgumentDoesNotExist() + { + $this->inject($this->subject, 'settings', [ + 'searching' => [ + 'mode' => 'filter', + ] + ]); + + $this->subject->initializeSearchAction(); + $this->assertInstanceOf( + SearchRequest::class, + $this->request->getArgument('searchRequest'), + 'Search request was not created.' + ); + } + + /** + * @test + */ + public function searchRequestArgumentIsAddedToExistingArguments() + { + $this->request->setArguments([ + '@widget_0' => [ + 'currentPage' => '7', + ] + ]); + $this->inject($this->subject, 'settings', [ + 'searching' => [ + 'mode' => 'filter', + ] + ]); + + $this->subject->initializeSearchAction(); + $this->assertInstanceOf( + SearchRequest::class, + $this->request->getArgument('searchRequest'), + 'Search request was not created.' + ); + $this->assertSame( + ['currentPage' => '7'], + $this->request->getArgument('@widget_0'), + 'Existing arguments were not kept.' + ); + } + + /** + * @test + */ + public function searchRequestArgumentIsNotAddedIfModeIsNotFilter() + { + $this->inject($this->subject, 'settings', ['searching' => []]); + + $this->subject->initializeSearchAction(); + $this->assertFalse( + $this->request->hasArgument('searchRequest'), + 'Search request should not exist.' + ); + } +} diff --git a/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php b/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php index 9e88d4b..7fb0280 100644 --- a/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php +++ b/Tests/Unit/Domain/Index/TcaIndexer/TcaTableServiceTest.php @@ -60,10 +60,18 @@ class TcaTableServiceTest extends AbstractUnitTestCase ->method('getIfExists') ->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist']) ->will($this->onConsecutiveCalls(null, false)); + $this->subject->expects($this->once()) + ->method('getSystemWhereClause') + ->will($this->returnValue('1=1 AND pages.no_search = 0')); + $whereClause = $this->subject->getWhereClause(); $this->assertSame( '1=1 AND pages.no_search = 0', - $this->subject->getWhereClause() + $whereClause->getStatement() + ); + $this->assertSame( + [], + $whereClause->getParameters() ); } @@ -76,10 +84,18 @@ class TcaTableServiceTest extends AbstractUnitTestCase ->method('getIfExists') ->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist']) ->will($this->onConsecutiveCalls('table.field = "someValue"', false)); + $this->subject->expects($this->once()) + ->method('getSystemWhereClause') + ->will($this->returnValue('1=1 AND pages.no_search = 0')); + $whereClause = $this->subject->getWhereClause(); $this->assertSame( '1=1 AND pages.no_search = 0 AND table.field = "someValue"', - $this->subject->getWhereClause() + $whereClause->getStatement() + ); + $this->assertSame( + [], + $whereClause->getParameters() ); } } diff --git a/Tests/Unit/Domain/Search/QueryFactoryTest.php b/Tests/Unit/Domain/Search/QueryFactoryTest.php index 9ff690a..54fdc2a 100644 --- a/Tests/Unit/Domain/Search/QueryFactoryTest.php +++ b/Tests/Unit/Domain/Search/QueryFactoryTest.php @@ -157,13 +157,19 @@ class QueryFactoryTest extends AbstractUnitTestCase ->method('get') ->will($this->throwException(new InvalidArgumentException)); $searchRequest = new SearchRequest('SearchWord'); - $searchRequest->setSize(45); + $searchRequest->setLimit(45); + $searchRequest->setOffset(35); $query = $this->subject->create($searchRequest); $this->assertSame( 45, $query->toArray()['size'], - 'Size was not added to query.' + 'Limit was not added to query.' + ); + $this->assertSame( + 35, + $query->toArray()['from'], + 'From was not added to query.' ); } @@ -318,4 +324,23 @@ class QueryFactoryTest extends AbstractUnitTestCase 'Boosts were not added to query.' ); } + + /** + * @test + */ + public function emptySearchStringWillNotAddSearchToQuery() + { + $searchRequest = new SearchRequest(); + + $this->configuration->expects($this->any()) + ->method('get') + ->will($this->throwException(new InvalidArgumentException)); + + $query = $this->subject->create($searchRequest); + $this->assertInstanceOf( + stdClass, + $query->toArray()['query']['match_all'], + 'Empty search request does not create expected query.' + ); + } } diff --git a/Tests/Unit/Domain/Search/SearchServiceTest.php b/Tests/Unit/Domain/Search/SearchServiceTest.php index 6a90a3c..9149e51 100644 --- a/Tests/Unit/Domain/Search/SearchServiceTest.php +++ b/Tests/Unit/Domain/Search/SearchServiceTest.php @@ -21,6 +21,7 @@ namespace Copyright\SearchCore\Tests\Unit\Domain\Search; */ use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Domain\Model\SearchRequest; use Codappix\SearchCore\Domain\Search\SearchService; @@ -64,10 +65,14 @@ class SearchServiceTest extends AbstractUnitTestCase ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(45, null)); + $this->configuration->expects($this->exactly(1)) + ->method('get') + ->with('searching.filter') + ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { - return $searchRequest->getSize() === 45; + return $searchRequest->getLimit() === 45; })); $searchRequest = new SearchRequest('SearchWord'); @@ -83,13 +88,94 @@ class SearchServiceTest extends AbstractUnitTestCase ->method('getIfExists') ->withConsecutive(['searching.size'], ['searching.facets']) ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->exactly(1)) + ->method('get') + ->with('searching.filter') + ->will($this->throwException(new InvalidArgumentException)); $this->connection->expects($this->once()) ->method('search') ->with($this->callback(function ($searchRequest) { - return $searchRequest->getSize() === 10; + return $searchRequest->getLimit() === 10; })); $searchRequest = new SearchRequest('SearchWord'); $this->subject->search($searchRequest); } + + /** + * @test + */ + public function configuredFilterAreAddedToRequestWithoutAnyFilter() + { + $this->configuration->expects($this->exactly(2)) + ->method('getIfExists') + ->withConsecutive(['searching.size'], ['searching.facets']) + ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->exactly(1)) + ->method('get') + ->with('searching.filter') + ->willReturn(['property' => 'something']); + + $this->connection->expects($this->once()) + ->method('search') + ->with($this->callback(function ($searchRequest) { + return $searchRequest->getFilter() === ['property' => 'something']; + })); + + $searchRequest = new SearchRequest('SearchWord'); + $this->subject->search($searchRequest); + } + + /** + * @test + */ + public function configuredFilterAreAddedToRequestWithExistingFilter() + { + $this->configuration->expects($this->exactly(2)) + ->method('getIfExists') + ->withConsecutive(['searching.size'], ['searching.facets']) + ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->exactly(1)) + ->method('get') + ->with('searching.filter') + ->willReturn(['property' => 'something']); + + $this->connection->expects($this->once()) + ->method('search') + ->with($this->callback(function ($searchRequest) { + return $searchRequest->getFilter() === [ + 'anotherProperty' => 'anything', + 'property' => 'something', + ]; + })); + + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->setFilter(['anotherProperty' => 'anything']); + $this->subject->search($searchRequest); + } + + /** + * @test + */ + public function nonConfiguredFilterIsNotChangingRequestWithExistingFilter() + { + $this->configuration->expects($this->exactly(2)) + ->method('getIfExists') + ->withConsecutive(['searching.size'], ['searching.facets']) + ->will($this->onConsecutiveCalls(null, null)); + $this->configuration->expects($this->exactly(1)) + ->method('get') + ->with('searching.filter') + ->will($this->throwException(new InvalidArgumentException)); + + $this->connection->expects($this->once()) + ->method('search') + ->with($this->callback(function ($searchRequest) { + return $searchRequest->getFilter() === ['anotherProperty' => 'anything']; + })); + + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->setFilter(['anotherProperty' => 'anything']); + $this->subject->search($searchRequest); + } } diff --git a/composer.json b/composer.json index c76f0b8..96c2dea 100644 --- a/composer.json +++ b/composer.json @@ -30,14 +30,14 @@ }, "scripts": { "post-autoload-dump": [ - "mkdir -p .Build/Web/typo3conf/ext/", - "[ -L .Build/Web/typo3conf/ext/search_core ] || ln -snvf ../../../../. .Build/Web/typo3conf/ext/search_core" + "mkdir -p .Build/web/typo3conf/ext/", + "[ -L .Build/web/typo3conf/ext/search_core ] || ln -snvf ../../../../. .Build/web/typo3conf/ext/search_core" ] }, "extra": { "typo3/cms": { "cms-package-dir": "{$vendor-dir}/typo3/cms", - "web-dir": ".Build/Web" + "web-dir": ".Build/web" } }, "authors": [ diff --git a/ext_localconf.php b/ext_localconf.php index 1c1f9cd..54bf43b 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -16,10 +16,10 @@ call_user_func( ], 't3lib/class.t3lib_tcemain.php' => [ 'processCmdmapClass' => [ - $extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class, + $extensionKey => \Codappix\SearchCore\Hook\DataHandler::class, ], 'processDatamapClass' => [ - $extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class, + $extensionKey => \Codappix\SearchCore\Hook\DataHandler::class, ], ], ],