Merge remote-tracking branch 'origin/develop' into feature/phpcs-travis

Conflicts:
    Classes/Hook/DataHandler.php
This commit is contained in:
Daniel Siepmann 2018-03-06 13:17:29 +01:00
commit ae51de9041
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
34 changed files with 437 additions and 269 deletions

View file

@ -11,6 +11,7 @@ before_install:
language: php language: php
php: php:
- 7.0
- 7.1 - 7.1
env: env:

View file

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

View file

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

View file

@ -48,6 +48,12 @@ abstract class AbstractIndexer implements IndexerInterface
*/ */
protected $logger; protected $logger;
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
* @inject
*/
protected $objectManager;
/** /**
* Inject log manager to get concrete logger from it. * Inject log manager to get concrete logger from it.
* *
@ -141,7 +147,8 @@ abstract class AbstractIndexer implements IndexerInterface
} else { } else {
$className = $configuration['_typoScriptNodeValue']; $className = $configuration['_typoScriptNodeValue'];
} }
$dataProcessor = GeneralUtility::makeInstance($className);
$dataProcessor = $this->objectManager->get($className);
if ($dataProcessor instanceof ProcessorInterface) { if ($dataProcessor instanceof ProcessorInterface) {
$record = $dataProcessor->processRecord($record, $configuration); $record = $dataProcessor->processRecord($record, $configuration);
} }

View file

@ -20,6 +20,7 @@ namespace Codappix\SearchCore\Domain\Index\TcaIndexer;
* 02110-1301, USA. * 02110-1301, USA.
*/ */
use Codappix\SearchCore\Utility\FrontendUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Core\SingletonInterface as Singleton;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
@ -32,14 +33,16 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/ */
class RelationResolver implements Singleton class RelationResolver implements Singleton
{ {
public function resolveRelationsForRecord(TcaTableService $service, array &$record) : void public function resolveRelationsForRecord(TcaTableService $service, array &$record)
{ {
foreach (array_keys($record) as $column) { foreach (array_keys($record) as $column) {
// TODO: Define / configure fields to exclude?! // TODO: Define / configure fields to exclude?!
if ($column === 'pid') { if ($column === 'pid') {
continue; continue;
} }
$record[$column] = BackendUtility::getProcessedValueExtra(
$record[$column] = GeneralUtility::makeInstance($this->getUtilityForMode())
::getProcessedValueExtra(
$service->getTableName(), $service->getTableName(),
$column, $column,
$record[$column], $record[$column],
@ -93,4 +96,13 @@ class RelationResolver implements Singleton
{ {
return array_map('trim', explode(',', $value)); return array_map('trim', explode(',', $value));
} }
protected function getUtilityForMode(): string
{
if (TYPO3_MODE === 'BE') {
return BackendUtility::class;
}
return FrontendUtility::class;
}
} }

View file

@ -129,7 +129,7 @@ class TcaTableService
* @param array &$records * @param array &$records
* @return void * @return void
*/ */
public function filterRecordsByRootLineBlacklist(array &$records) : void public function filterRecordsByRootLineBlacklist(array &$records)
{ {
$records = array_filter( $records = array_filter(
$records, $records,
@ -142,7 +142,7 @@ class TcaTableService
/** /**
* @param array &$record * @param array &$record
*/ */
public function prepareRecord(array &$record) : void public function prepareRecord(array &$record)
{ {
$this->relationResolver->resolveRelationsForRecord($this, $record); $this->relationResolver->resolveRelationsForRecord($this, $record);

View file

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

View file

@ -99,20 +99,18 @@ class QueryFactory
return; return;
} }
$query = ArrayUtility::setValueByPath( $matchExpression = [
$query, 'type' => 'most_fields',
'query.bool.must.0.match._all.query', 'query' => $searchRequest->getSearchTerm(),
$searchRequest->getSearchTerm() 'fields' => GeneralUtility::trimExplode(',', $this->configuration->get('searching.fields.query')),
); ];
$minimumShouldMatch = $this->configuration->getIfExists('searching.minimumShouldMatch'); $minimumShouldMatch = $this->configuration->getIfExists('searching.minimumShouldMatch');
if ($minimumShouldMatch) { if ($minimumShouldMatch) {
$query = ArrayUtility::setValueByPath( $matchExpression['minimum_should_match'] = $minimumShouldMatch;
$query,
'query.bool.must.0.match._all.minimum_should_match',
$minimumShouldMatch
);
} }
$query = ArrayUtility::setValueByPath($query, 'query.bool.must.0.multi_match', $matchExpression);
} }
protected function addBoosts(SearchRequestInterface $searchRequest, array &$query) protected function addBoosts(SearchRequestInterface $searchRequest, array &$query)
@ -248,11 +246,7 @@ class QueryFactory
foreach ($searchRequest->getFacets() as $facet) { foreach ($searchRequest->getFacets() as $facet) {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
'aggs' => [ 'aggs' => [
$facet->getIdentifier() => [ $facet->getIdentifier() => $facet->getConfig(),
'terms' => [
'field' => $facet->getField(),
],
],
], ],
]); ]);
} }

View file

@ -103,15 +103,10 @@ class SearchService
} }
foreach ($facetsConfig as $identifier => $facetConfig) { foreach ($facetsConfig as $identifier => $facetConfig) {
if (!isset($facetConfig['field']) || trim($facetConfig['field']) === '') {
// TODO: Finish throw
throw new \Exception('message', 1499171142);
}
$searchRequest->addFacet($this->objectManager->get( $searchRequest->addFacet($this->objectManager->get(
FacetRequest::class, FacetRequest::class,
$identifier, $identifier,
$facetConfig['field'] $facetConfig
)); ));
} }
} }

View file

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

View file

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

View file

@ -58,10 +58,8 @@ class DataHandlerFinisher extends AbstractFinisher
switch ($action) { switch ($action) {
case 'update': case 'update':
$this->dataHandler->update($tableName, $record);
break;
case 'add': case 'add':
$this->dataHandler->add($tableName, $record); $this->dataHandler->update($tableName, $record);
break; break;
case 'delete': case 'delete':
$this->dataHandler->delete($tableName, $record['uid']); $this->dataHandler->delete($tableName, $record['uid']);

View file

@ -0,0 +1,39 @@
<?php
namespace Codappix\SearchCore\Utility;
/*
* Copyright (C) 2018 Justus Moroni <developer@leonmrni.com>
*
* 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\Backend\Utility\BackendUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* Overwrite BackendUtility to use in frontend.
* LanguageService was only usable in backend.
*/
class FrontendUtility extends BackendUtility
{
/**
* @return TypoScriptFrontendController
*/
protected static function getLanguageService()
{
return $GLOBALS['TSFE'];
}
}

View file

@ -21,6 +21,12 @@ plugin {
abstractFields = {$plugin.tx_searchcore.settings.indexing.pages.abstractFields} abstractFields = {$plugin.tx_searchcore.settings.indexing.pages.abstractFields}
} }
} }
searching {
fields {
query = _all
}
}
} }
} }
} }

View file

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

View file

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

View file

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

View file

@ -205,3 +205,6 @@ class name) as done in the examples above.
By implementing also the same interface as necessary for TYPO3 By implementing also the same interface as necessary for TYPO3
:ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`, you are able to reuse the same code :ref:`t3tsref:cobj-fluidtemplate-properties-dataprocessing`, you are able to reuse the same code
also for Fluid to prepare the same record fetched from DB for your fluid. also for Fluid to prepare the same record fetched from DB for your fluid.
Dependency injection is possible inside of processors, as we instantiate through extbase
``ObjectManager``.

View file

@ -151,7 +151,18 @@ filtering. This way you can use arbitrary filter names and map them to existing
fields fields
------ ------
Defines the fields to fetch from elasticsearch. Two sub entries exist: Defines the fields to fetch and search from elasticsearch. With the following sub keys:
``query`` defines the fields to search in. Default is ``_all`` from 5.x times of elasticsearch.
Configure a comma separated list of fields to search in. This is necessary if you have configured
special mapping for some fields, or just want to search some fields.
The most hits get ranked highest. The following is an example configuration::
fields {
query = _all, city
}
The following sub properties configure the fields to fetch from elasticsearch:
First ``stored_fields`` which is a list of comma separated fields which actually exist and will be First ``stored_fields`` which is a list of comma separated fields which actually exist and will be
added. Typically you will use ``_source`` to fetch the whole indexed fields. added. Typically you will use ``_source`` to fetch the whole indexed fields.

View file

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

View file

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

View file

@ -0,0 +1,78 @@
<?php
namespace Codappix\SearchCore\Tests\Functional\Connection\Elasticsearch;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\SearchService;
use TYPO3\CMS\Extbase\Object\ObjectManager;
class FacetTest extends AbstractFunctionalTestCase
{
protected function getTypoScriptFilesForFrontendRootPage()
{
return array_merge(
parent::getTypoScriptFilesForFrontendRootPage(),
['EXT:search_core/Tests/Functional/Fixtures/Searching/Facet.ts']
);
}
protected function getDataSets()
{
return array_merge(
parent::getDataSets(),
['Tests/Functional/Fixtures/Searching/Filter.xml']
);
}
/**
* @test
*/
public function itsPossibleToFetchFacetsForField()
{
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->indexAllDocuments()
;
$searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(SearchService::class);
$searchRequest = new SearchRequest();
$result = $searchService->search($searchRequest);
$this->assertSame(1, count($result->getFacets()), 'Did not receive the single defined facet.');
$facet = current($result->getFacets());
$this->assertSame('contentTypes', $facet->getName(), 'Name of facet was not as expected.');
$this->assertSame('CType', $facet->getField(), 'Field of facet was not expected.');
$options = $facet->getOptions();
$this->assertSame(2, count($options), 'Did not receive the expected number of possible options for facet.');
$option = $options['HTML'];
$this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
$option = $options['Header'];
$this->assertSame('Header', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
}
}

View file

@ -58,37 +58,4 @@ class FilterTest extends AbstractFunctionalTestCase
$this->assertSame(5, $result->getResults()[0]['uid'], 'Did not get the expected result entry.'); $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.'); $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 = current($result->getFacets());
$this->assertSame('contentTypes', $facet->getName(), 'Name of facet was not as expected.');
$this->assertSame('CType', $facet->getField(), 'Field of facet was not expected.');
$options = $facet->getOptions();
$this->assertSame(2, count($options), 'Did not receive the expected number of possible options for facet.');
$option = $options['HTML'];
$this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
$option = $options['Header'];
$this->assertSame('Header', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
}
} }

View file

@ -37,10 +37,8 @@ plugin {
} }
searching { searching {
facets { fields {
contentTypes { query = _all
field = CType
}
} }
} }
} }

View file

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

View file

@ -1,54 +0,0 @@
<?php
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
class IgnoresUnkownOperationTest extends AbstractDataHandlerTest
{
/**
* @var DataHandlerService|\PHPUnit_Framework_MockObject_MockObject|AccessibleObjectInterface
*/
protected $subject;
/**
* @test
*/
public function dataHandlerCommandSomethingIsIgnored()
{
$subject = new DataHandlerHook($this->subject);
$this->assertFalse(
$subject->processDatamap_afterDatabaseOperations(
'something',
'tt_content',
1,
[],
new Typo3DataHandler
),
'Hook processed status "something".'
);
}
}

View file

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

View file

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

View file

@ -26,6 +26,7 @@ use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\DataProcessing\CopyToProcessor; use Codappix\SearchCore\DataProcessing\CopyToProcessor;
use Codappix\SearchCore\Domain\Index\AbstractIndexer; use Codappix\SearchCore\Domain\Index\AbstractIndexer;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase; use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
class AbstractIndexerTest extends AbstractUnitTestCase class AbstractIndexerTest extends AbstractUnitTestCase
{ {
@ -44,17 +45,25 @@ class AbstractIndexerTest extends AbstractUnitTestCase
*/ */
protected $connection; protected $connection;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
public function setUp() public function setUp()
{ {
parent::setUp(); parent::setUp();
$this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock();
$this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock(); $this->connection = $this->getMockBuilder(ConnectionInterface::class)->getMock();
$this->objectManager = $this->getMockBuilder(ObjectManagerInterface::class)->getMock();
$this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [ $this->subject = $this->getMockForAbstractClass(AbstractIndexer::class, [
$this->connection, $this->connection,
$this->configuration $this->configuration
]); ]);
$this->inject($this->subject, 'objectManager', $this->objectManager);
$this->subject->injectLogger($this->getMockedLogger()); $this->subject->injectLogger($this->getMockedLogger());
$this->subject->setIdentifier('testTable'); $this->subject->setIdentifier('testTable');
$this->subject->expects($this->any()) $this->subject->expects($this->any())
@ -73,6 +82,11 @@ class AbstractIndexerTest extends AbstractUnitTestCase
$expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test'; $expectedRecord['new_test_field2'] = 'test' . PHP_EOL . 'test';
$expectedRecord['search_abstract'] = ''; $expectedRecord['search_abstract'] = '';
$this->objectManager->expects($this->any())
->method('get')
->with(CopyToProcessor::class)
->willReturn(new CopyToProcessor());
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->exactly(2))
->method('get') ->method('get')
->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields']) ->withConsecutive(['indexing.testTable.dataProcessing'], ['indexing.testTable.abstractFields'])

View file

@ -56,9 +56,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
{ {
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertInstanceOf( $this->assertInstanceOf(
@ -73,9 +71,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
*/ */
public function filterIsAddedToQuery() public function filterIsAddedToQuery()
{ {
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['field' => 'content']); $searchRequest->setFilter(['field' => 'content']);
@ -95,9 +91,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
*/ */
public function emptyFilterIsNotAddedToQuery() public function emptyFilterIsNotAddedToQuery()
{ {
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter([ $searchRequest->setFilter([
@ -122,12 +116,10 @@ class QueryFactoryTest extends AbstractUnitTestCase
*/ */
public function facetsAreAddedToQuery() public function facetsAreAddedToQuery()
{ {
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->addFacet(new FacetRequest('Identifier', 'FieldName')); $searchRequest->addFacet(new FacetRequest('Identifier', ['terms' => ['field' => 'FieldName']]));
$searchRequest->addFacet(new FacetRequest('Identifier 2', 'FieldName 2')); $searchRequest->addFacet(new FacetRequest('Identifier 2', ['terms' => ['field' => 'FieldName 2']]));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertSame( $this->assertSame(
@ -153,9 +145,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
*/ */
public function sizeIsAddedToQuery() public function sizeIsAddedToQuery()
{ {
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setLimit(45); $searchRequest->setLimit(45);
$searchRequest->setOffset(35); $searchRequest->setOffset(35);
@ -179,9 +169,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
public function searchTermIsAddedToQuery() public function searchTermIsAddedToQuery()
{ {
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertSame( $this->assertSame(
@ -189,9 +177,11 @@ class QueryFactoryTest extends AbstractUnitTestCase
'bool' => [ 'bool' => [
'must' => [ 'must' => [
[ [
'match' => [ 'multi_match' => [
'_all' => [ 'type' => 'most_fields',
'query' => 'SearchWord', 'query' => 'SearchWord',
'fields' => [
'_all',
], ],
], ],
], ],
@ -219,9 +209,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
'50%', '50%',
null null
)); ));
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertArraySubset( $this->assertArraySubset(
@ -229,10 +217,13 @@ class QueryFactoryTest extends AbstractUnitTestCase
'bool' => [ 'bool' => [
'must' => [ 'must' => [
[ [
'match' => [ 'multi_match' => [
'_all' => [ 'type' => 'most_fields',
'minimum_should_match' => '50%', 'query' => 'SearchWord',
'fields' => [
'_all',
], ],
'minimum_should_match' => '50%',
], ],
], ],
], ],
@ -253,12 +244,14 @@ class QueryFactoryTest extends AbstractUnitTestCase
$this->configuration->expects($this->any()) $this->configuration->expects($this->any())
->method('get') ->method('get')
->withConsecutive( ->withConsecutive(
['searching.fields.query'],
['searching.boost'], ['searching.boost'],
['searching.fields.stored_fields'], ['searching.fields.stored_fields'],
['searching.fields.script_fields'], ['searching.fields.script_fields'],
['searching.fieldValueFactor'] ['searching.fieldValueFactor']
) )
->will($this->onConsecutiveCalls( ->will($this->onConsecutiveCalls(
'_all',
[ [
'search_title' => 3, 'search_title' => 3,
'search_abstract' => 1.5, 'search_abstract' => 1.5,
@ -308,12 +301,14 @@ class QueryFactoryTest extends AbstractUnitTestCase
$this->configuration->expects($this->any()) $this->configuration->expects($this->any())
->method('get') ->method('get')
->withConsecutive( ->withConsecutive(
['searching.fields.query'],
['searching.boost'], ['searching.boost'],
['searching.fields.stored_fields'], ['searching.fields.stored_fields'],
['searching.fields.script_fields'], ['searching.fields.script_fields'],
['searching.fieldValueFactor'] ['searching.fieldValueFactor']
) )
->will($this->onConsecutiveCalls( ->will($this->onConsecutiveCalls(
'_all',
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
@ -328,9 +323,11 @@ class QueryFactoryTest extends AbstractUnitTestCase
'bool' => [ 'bool' => [
'must' => [ 'must' => [
[ [
'match' => [ 'multi_match' => [
'_all' => [ 'type' => 'most_fields',
'query' => 'SearchWord', 'query' => 'SearchWord',
'fields' => [
'_all',
], ],
], ],
], ],
@ -352,9 +349,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
{ {
$searchRequest = new SearchRequest(); $searchRequest = new SearchRequest();
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertInstanceOf( $this->assertInstanceOf(
@ -364,6 +359,54 @@ class QueryFactoryTest extends AbstractUnitTestCase
); );
} }
/**
* @test
*/
public function configuredQueryFieldsAreAddedToQuery()
{
$searchRequest = new SearchRequest('SearchWord');
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(
['searching.fields.query'],
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls(
'_all, field1, field2',
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException)
));
$query = $this->subject->create($searchRequest);
$this->assertArraySubset(
[
'bool' => [
'must' => [
[
'multi_match' => [
'type' => 'most_fields',
'query' => 'SearchWord',
'fields' => [
'_all',
'field1',
'field2',
],
],
],
],
],
],
$query->toArray()['query'],
'Configured fields were not added to query as configured.'
);
}
/** /**
* @test * @test
*/ */
@ -433,12 +476,14 @@ class QueryFactoryTest extends AbstractUnitTestCase
$this->configuration->expects($this->any()) $this->configuration->expects($this->any())
->method('get') ->method('get')
->withConsecutive( ->withConsecutive(
['searching.fields.query'],
['searching.boost'], ['searching.boost'],
['searching.fields.stored_fields'], ['searching.fields.stored_fields'],
['searching.fields.script_fields'], ['searching.fields.script_fields'],
['searching.fieldValueFactor'] ['searching.fieldValueFactor']
) )
->will($this->onConsecutiveCalls( ->will($this->onConsecutiveCalls(
'_all',
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
[ [
@ -521,9 +566,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
] ]
)); ));
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertSame( $this->assertSame(
@ -558,9 +601,7 @@ class QueryFactoryTest extends AbstractUnitTestCase
null null
)); ));
$this->configuration->expects($this->any()) $this->configureConfigurationMockWithDefault();
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest); $query = $this->subject->create($searchRequest);
$this->assertTrue( $this->assertTrue(
@ -568,4 +609,17 @@ class QueryFactoryTest extends AbstractUnitTestCase
'Sort was added to query even if not configured.' 'Sort was added to query even if not configured.'
); );
} }
protected function configureConfigurationMockWithDefault()
{
$this->configuration->expects($this->any())
->method('get')
->will($this->returnCallback(function ($configName) {
if ($configName === 'searching.fields.query') {
return '_all';
}
throw new InvalidArgumentException();
}));
}
} }

View file

@ -83,19 +83,14 @@ class DataHandlerFinisherTest extends AbstractUnitTestCase
public function possibleFinisherSetup() : array public function possibleFinisherSetup() : array
{ {
return [ return [
'valid add configuration' => [
'action' => 'add',
'nonCalledActions' => ['delete', 'update'],
'expectedSecondArgument' => ['uid' => 23],
],
'valid update configuration' => [ 'valid update configuration' => [
'action' => 'update', 'action' => 'update',
'nonCalledActions' => ['delete', 'add'], 'nonCalledActions' => ['delete'],
'expectedSecondArgument' => ['uid' => 23], 'expectedSecondArgument' => ['uid' => 23],
], ],
'valid delete configuration' => [ 'valid delete configuration' => [
'action' => 'delete', 'action' => 'delete',
'nonCalledActions' => ['update', 'add'], 'nonCalledActions' => ['update'],
'expectedSecondArgument' => 23, 'expectedSecondArgument' => 23,
], ],
]; ];
@ -109,7 +104,7 @@ class DataHandlerFinisherTest extends AbstractUnitTestCase
{ {
$this->subject->setOptions($options); $this->subject->setOptions($options);
foreach (['add', 'update', 'delete'] as $nonCalledAction) { foreach (['update', 'delete'] as $nonCalledAction) {
$this->dataHandlerMock->expects($this->never())->method($nonCalledAction); $this->dataHandlerMock->expects($this->never())->method($nonCalledAction);
} }

View file

@ -16,7 +16,7 @@
} }
}, },
"require": { "require": {
"php": ">=7.1.0", "php": ">=7.0.0",
"typo3/cms": "~8.7", "typo3/cms": "~8.7",
"ruflin/elastica": "~3.2" "ruflin/elastica": "~3.2"
}, },

4
ext_conf_template.txt Normal file
View file

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

View file

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