From be752485176938ac302dd9d88367b6c6fe026fce Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Fri, 15 Sep 2017 21:35:52 +0200 Subject: [PATCH 1/5] FEATURE: Allow configured filters Add new feature to allow pre configured filters. The filters will be applied to all requests. --- Classes/Domain/Search/SearchService.php | 18 ++++ Documentation/source/configuration.rst | 17 ++++ .../Unit/Domain/Search/SearchServiceTest.php | 86 +++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index 114ebfe..384f6aa 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -72,6 +72,7 @@ class SearchService $searchRequest->setConnection($this->connection); $this->addSize($searchRequest); $this->addConfiguredFacets($searchRequest); + $this->addConfiguredFilters($searchRequest); return $this->connection->search($searchRequest); } @@ -113,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/Documentation/source/configuration.rst b/Documentation/source/configuration.rst index 8ea8e29..c4677e1 100644 --- a/Documentation/source/configuration.rst +++ b/Documentation/source/configuration.rst @@ -279,6 +279,23 @@ Searching The above example will provide a facet with options for all found ``CType`` results together with a count. +.. _filter: + +``filter`` +""""""""""" + + Used by: While building search request. + + Define filter that should be set for all requests. + + Example:: + + plugin.tx_searchcore.settings.searching.filter { + property = value + } + + For Elasticsearch the fields have to be filterable, e.g. need a mapping as ``keyword``. + .. _minimumShouldMatch: ``minimumShouldMatch`` diff --git a/Tests/Unit/Domain/Search/SearchServiceTest.php b/Tests/Unit/Domain/Search/SearchServiceTest.php index 046f751..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,6 +65,10 @@ 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) { @@ -83,6 +88,10 @@ 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) { @@ -92,4 +101,81 @@ class SearchServiceTest extends AbstractUnitTestCase $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); + } } From 4de18289052beca9bf1ac9acfb77d464df1b6208 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Fri, 15 Sep 2017 21:36:52 +0200 Subject: [PATCH 2/5] FIX: Fix phpcs issues with annotations --- Classes/Domain/Search/QueryFactory.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index 60f22e2..f3efd68 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -89,7 +89,7 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest - * @param array &$query + * @param array $query */ protected function addSize(SearchRequestInterface $searchRequest, array &$query) { @@ -101,7 +101,7 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest - * @param array &$query + * @param array $query */ protected function addSearch(SearchRequestInterface $searchRequest, array &$query) { @@ -123,7 +123,7 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest - * @param array &$query + * @param array $query */ protected function addBoosts(SearchRequestInterface $searchRequest, array &$query) { @@ -156,7 +156,7 @@ class QueryFactory } /** - * @param array &$query + * @param array $query */ protected function addFactorBoost(array &$query) { @@ -174,7 +174,7 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest - * @param array &$query + * @param array $query */ protected function addFilter(SearchRequestInterface $searchRequest, array &$query) { @@ -202,7 +202,7 @@ class QueryFactory /** * @param SearchRequestInterface $searchRequest - * @param array &$query + * @param array $query */ protected function addFacets(SearchRequestInterface $searchRequest, array &$query) { From 13004e86f2694aa576cb99c08bbe3f9c3279d6a3 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Fri, 15 Sep 2017 21:54:47 +0200 Subject: [PATCH 3/5] FEATURE: Allow filter mode by not forcing a search term --- Classes/Domain/Model/SearchRequest.php | 2 +- Classes/Domain/Search/QueryFactory.php | 4 ++++ Tests/Unit/Domain/Search/QueryFactoryTest.php | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index 7d6436c..39e477c 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -66,7 +66,7 @@ class SearchRequest implements SearchRequestInterface /** * @param string $query */ - public function __construct($query) + public function __construct($query = '') { $this->query = (string) $query; } diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index f3efd68..9455f80 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -105,6 +105,10 @@ class QueryFactory */ protected function addSearch(SearchRequestInterface $searchRequest, array &$query) { + if (trim($searchRequest->getSearchTerm()) === '') { + return; + } + $query = ArrayUtility::setValueByPath( $query, 'query.bool.must.0.match._all.query', diff --git a/Tests/Unit/Domain/Search/QueryFactoryTest.php b/Tests/Unit/Domain/Search/QueryFactoryTest.php index de82ebf..54fdc2a 100644 --- a/Tests/Unit/Domain/Search/QueryFactoryTest.php +++ b/Tests/Unit/Domain/Search/QueryFactoryTest.php @@ -324,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.' + ); + } } From 9a0d73f1c784ea09dd57ac3a9a0bcacce10a31ac Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Fri, 15 Sep 2017 22:26:52 +0200 Subject: [PATCH 4/5] FEATURE: Allow to switch from search to filter mode --- Classes/Controller/SearchController.php | 11 +++ Documentation/source/configuration.rst | 17 ++++ .../Unit/Controller/SearchControllerTest.php | 99 +++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 Tests/Unit/Controller/SearchControllerTest.php diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index 068f9a1..a02cd80 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -44,6 +44,17 @@ 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([ + 'searchRequest' => $this->objectManager->get(SearchRequest::class), + ]); + } + } + /** * Process a search and deliver original request and result to view. * diff --git a/Documentation/source/configuration.rst b/Documentation/source/configuration.rst index c4677e1..549ca68 100644 --- a/Documentation/source/configuration.rst +++ b/Documentation/source/configuration.rst @@ -346,3 +346,20 @@ Searching factor = 2 missing = 1 } + +.. _mode: + +``mode`` +"""""""" + + Used by: Controller while preparing action. + + Define to switch from search to filter mode. + + Example:: + + plugin.tx_searchcore.settings.searching { + mode = filter + } + + Only ``filter`` is allowed as value. Will submit an empty query to switch to filter mode. diff --git a/Tests/Unit/Controller/SearchControllerTest.php b/Tests/Unit/Controller/SearchControllerTest.php new file mode 100644 index 0000000..fa38665 --- /dev/null +++ b/Tests/Unit/Controller/SearchControllerTest.php @@ -0,0 +1,99 @@ + + * + * 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 searchRequestArgumentIsNotAddedIfModeIsNotFilter() + { + $this->inject($this->subject, 'settings', ['searching' => []]); + + $this->subject->initializeSearchAction(); + $this->assertFalse( + $this->request->hasArgument('searchRequest'), + 'Search request should not exist.' + ); + } +} From a47b1c3a97751b477c0319867f89911e2985620f Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Fri, 15 Sep 2017 22:29:20 +0200 Subject: [PATCH 5/5] TASK: Remove unused fields for plugin content element As we do not make use of recursion or pages, we hide the inputs. --- Configuration/TCA/Overrides/tt_content.php | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Configuration/TCA/Overrides/tt_content.php 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 @@ +