Merge remote-tracking branch 'origin/develop' into feature/cms-8-support

This commit is contained in:
Daniel Siepmann 2017-07-07 11:58:15 +02:00
commit 3d90bad58d
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
78 changed files with 2066 additions and 319 deletions

View file

@ -1,7 +1,15 @@
sudo: true
addons:
apt:
packages:
- oracle-java8-set-default
before_install:
- curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.0.deb && sudo dpkg -i --force-confnew elasticsearch-5.2.0.deb && sudo service elasticsearch start
language: php
php:
- 5.6
- 7.0
- 7.1
@ -16,18 +24,12 @@ env:
- typo3DatabaseUsername="travis"
- typo3DatabasePassword=""
matrix:
- TYPO3_VERSION="~7.6"
- TYPO3_VERSION="~8"
- TYPO3_VERSION="dev-master"
matrix:
fast_finish: true
exclude:
# TYPO3 no longer supports 5.6
- env: TYPO3_VERSION="~8"
php: 5.6
- env: TYPO3_VERSION="dev-master"
php: 5.6
allow_failures:
- env: TYPO3_VERSION="~8"
php: 7.0
@ -40,11 +42,12 @@ matrix:
services:
- mysql
- elasticsearch
install: make install
script: make functionalTests
script:
- make unitTests
- make functionalTests
after_script:
- make uploadCodeCoverage

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Command;
namespace Codappix\SearchCore\Command;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,8 @@ namespace Leonmrni\SearchCore\Command;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\CommandController;
@ -34,12 +35,6 @@ class IndexCommandController extends CommandController
*/
protected $indexerFactory;
/**
* @var \Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface
* @inject
*/
protected $configuration;
/**
* @param IndexerFactory $factory
*/
@ -49,19 +44,18 @@ class IndexCommandController extends CommandController
}
/**
* Will index the given table or everything.
* Will index the given identifier.
*
* @param string $table
* @param string $identifier
*/
public function indexCommand($table)
public function indexCommand($identifier)
{
// TODO: Allow to index multiple tables at once?
// TODO: Also allow to index everything?
if (! in_array($table, GeneralUtility::trimExplode(',', $this->configuration->get('indexer.tca.allowedTables')))) {
$this->outputLine('Table is not allowed for indexing.');
$this->quit(1);
try {
$this->indexerFactory->getIndexer($identifier)->indexAllDocuments();
$this->outputLine($identifier . ' was indexed.');
} catch (NoMatchingIndexerException $e) {
$this->outputLine('No indexer found for: ' . $identifier);
}
$this->indexerFactory->getIndexer($table)->indexAllDocuments();
$this->outputLine('Table was indexed.');
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Configuration;
namespace Codappix\SearchCore\Configuration;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Configuration;
namespace Codappix\SearchCore\Configuration;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -34,6 +34,8 @@ interface ConfigurationContainerInterface extends Singleton
*
* @param string $path In dot notation. E.g. indexer.tca.allowedTables
* @return mixed
*
* @throws InvalidArgumentException
*/
public function get($path);

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Configuration;
namespace Codappix\SearchCore\Configuration;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Configuration;
namespace Codappix\SearchCore\Configuration;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection;
namespace Codappix\SearchCore\Connection;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection;
namespace Codappix\SearchCore\Connection;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,10 @@ namespace Leonmrni\SearchCore\Connection;
* 02110-1301, USA.
*/
use Codappix\SearchCore\Connection\Elasticsearch\SearchResult;
use Codappix\SearchCore\Domain\Search\QueryFactory;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* Outer wrapper to elasticsearch.
@ -42,16 +45,31 @@ class Elasticsearch implements Singleton, ConnectionInterface
*/
protected $typeFactory;
/**
* @var Elasticsearch\MappingFactory
*/
protected $mappingFactory;
/**
* @var Elasticsearch\DocumentFactory
*/
protected $documentFactory;
/**
* @var QueryFactory
*/
protected $queryFactory;
/**
* @var \TYPO3\CMS\Core\Log\Logger
*/
protected $logger;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* Inject log manager to get concrete logger from it.
*
@ -62,22 +80,36 @@ class Elasticsearch implements Singleton, ConnectionInterface
$this->logger = $logManager->getLogger(__CLASS__);
}
/**
* @param ObjectManagerInterface $objectManager
*/
public function injectObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @param Elasticsearch\Connection $connection
* @param Elasticsearch\IndexFactory $indexFactory
* @param Elasticsearch\TypeFactory $typeFactory
* @param Elasticsearch\MappingFactory $mappingFactory
* @param Elasticsearch\DocumentFactory $documentFactory
* @param QueryFactory $queryFactory
*/
public function __construct(
Elasticsearch\Connection $connection,
Elasticsearch\IndexFactory $indexFactory,
Elasticsearch\TypeFactory $typeFactory,
Elasticsearch\DocumentFactory $documentFactory
Elasticsearch\MappingFactory $mappingFactory,
Elasticsearch\DocumentFactory $documentFactory,
QueryFactory $queryFactory
) {
$this->connection = $connection;
$this->indexFactory = $indexFactory;
$this->typeFactory = $typeFactory;
$this->mappingFactory = $mappingFactory;
$this->documentFactory = $documentFactory;
$this->queryFactory = $queryFactory;
}
public function addDocument($documentType, array $document)
@ -133,6 +165,13 @@ class Elasticsearch implements Singleton, ConnectionInterface
protected function withType($documentType, callable $callback)
{
$type = $this->getType($documentType);
// TODO: Check whether it's to heavy to send it so often e.g. for every single document.
// Perhaps add command controller to submit mapping?!
// Also it's not possible to change mapping without deleting index first.
// Mattes told about a solution.
// So command looks like the best way so far, except we manage mattes solution.
// Still then this should be done once. So perhaps singleton which tracks state and does only once?
$this->mappingFactory->getMapping($type)->send();
$callback($type);
$type->getIndex()->refresh();
}
@ -140,7 +179,7 @@ class Elasticsearch implements Singleton, ConnectionInterface
/**
* @param SearchRequestInterface $searchRequest
*
* @return \Elastica\ResultSet
* @return SearchResultInterface
*/
public function search(SearchRequestInterface $searchRequest)
{
@ -148,10 +187,9 @@ class Elasticsearch implements Singleton, ConnectionInterface
$search = new \Elastica\Search($this->connection->getClient());
$search->addIndex('typo3content');
$search->setQuery($this->queryFactory->create($searchRequest));
// TODO: Return wrapped result to implement our interface.
// Also update php doc to reflect the change.
return $search->search('"' . $searchRequest->getSearchTerm() . '"');
return $this->objectManager->get(SearchResult::class, $search->search());
}
/**

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Connection\Elasticsearch;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
/**

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -0,0 +1,93 @@
<?php
namespace Codappix\SearchCore\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\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Connection\FacetInterface;
class Facet implements FacetInterface
{
/**
* @var string
*/
protected $name = '';
/**
* @var string
*/
protected $field = '';
/**
* @var array
*/
protected $buckets = [];
/**
* @var array<FacetOption>
*/
protected $options = [];
public function __construct($name, array $aggregation, ConfigurationContainerInterface $configuration)
{
$this->name = $name;
$this->buckets = $aggregation['buckets'];
$this->field = $configuration->getIfExists('searching.facets.' . $this->name . '.field') ?: '';
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* Returns all possible options for this facet.
*
* @return array<FacetOptionInterface>
*/
public function getOptions()
{
$this->initOptions();
return $this->options;
}
protected function initOptions()
{
if ($this->options !== []) {
return;
}
foreach ($this->buckets as $bucket) {
$this->options[] = new FacetOption($bucket);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Codappix\SearchCore\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\Connection\FacetOptionInterface;
class FacetOption implements FacetOptionInterface
{
/**
* @var string
*/
protected $name = '';
/**
* @var int
*/
protected $count = 0;
/**
* @param array $bucket
*/
public function __construct(array $bucket)
{
$this->name = $bucket['key'];
$this->count = $bucket['doc_count'];
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return int
*/
public function getCount()
{
return $this->count;
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -0,0 +1,73 @@
<?php
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* 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\Configuration\InvalidArgumentException;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
/**
* Factory to get mappings.
*/
class MappingFactory implements Singleton
{
/**
* @var ConfigurationContainerInterface
*/
protected $configuration;
/**
* @param ConfigurationContainerInterface $configuration
*/
public function __construct(ConfigurationContainerInterface $configuration)
{
$this->configuration = $configuration;
}
/**
* Get an mapping based on type.
*
* @param \Elastica\Type $type
*
* @return \Elastica\Mapping
*/
public function getMapping(\Elastica\Type $type)
{
$mapping = new \Elastica\Type\Mapping();
$mapping->setType($type);
$mapping->setProperties($this->getConfiguration($type->getName()));
return $mapping;
}
/**
* @param string $identifier
* @return array
*/
protected function getConfiguration($identifier)
{
try {
return $this->configuration->get('indexing.' . $identifier . '.mapping');
} catch (InvalidArgumentException $e) {
return [];
}
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Codappix\SearchCore\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\Connection\ResultItemInterface;
class ResultItem implements ResultItemInterface
{
/**
* @var array
*/
protected $data = [];
public function __construct(\Elastica\Result $result)
{
$this->data = $result->getData();
}
public function offsetExists($offset)
{
return isset($this->data[$offset]);
}
public function offsetGet($offset)
{
return $this->data[$offset];
}
public function offsetSet($offset, $value)
{
throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077);
}
public function offsetUnset($offset)
{
throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077);
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,12 +20,127 @@ namespace Leonmrni\SearchCore\Connection\Elasticsearch;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Connection\FacetInterface;
use Codappix\SearchCore\Connection\ResultItemInterface;
use Codappix\SearchCore\Connection\SearchResultInterface;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
*
*/
class SearchResult extends \Elastica\SearchResult implements SearchResultInterface
class SearchResult implements SearchResultInterface
{
/**
* @var \Elastica\ResultSet
*/
protected $result;
/**
* @var array<FacetInterface>
*/
protected $facets = [];
/**
* @var array<ResultItemInterface>
*/
protected $results = [];
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
public function __construct(\Elastica\ResultSet $result, ObjectManagerInterface $objectManager)
{
$this->result = $result;
$this->objectManager = $objectManager;
}
/**
* @return array<ResultItemInterface>
*/
public function getResults()
{
$this->initResults();
return $this->results;
}
/**
* Return all facets, if any.
*
* @return array<FacetIterface>
*/
public function getFacets()
{
$this->initFacets();
return $this->facets;
}
/**
* Returns the total sum of matching results.
*
* @return int
*/
public function getTotalCount()
{
return $this->result->getTotalHits();
}
// Countable - Interface
/**
* Returns the total sum of results contained in this result.
*
* @return int
*/
public function count()
{
return $this->result->count();
}
// Iterator - Interface
public function current()
{
return $this->result->current();
}
public function next()
{
return $this->result->next();
}
public function key()
{
return $this->result->key();
}
public function valid()
{
return $this->result->valid();
}
public function rewind()
{
$this->result->rewind();
}
protected function initResults()
{
if ($this->results !== []) {
return;
}
foreach ($this->result->getResults() as $result) {
$this->results[] = new ResultItem($result);
}
}
protected function initFacets()
{
if ($this->facets !== [] || !$this->result->hasAggregations()) {
return;
}
foreach ($this->result->getAggregations() as $aggregationName => $aggregation) {
$this->facets[] = $this->objectManager->get(Facet::class, $aggregationName, $aggregation);
}
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection\Elasticsearch;
namespace Codappix\SearchCore\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -0,0 +1,39 @@
<?php
namespace Codappix\SearchCore\Connection;
/*
* 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.
*/
/**
* A single facet, e.g. keyword based.
*/
interface FacetInterface
{
/**
* @return string
*/
public function getName();
/**
* Returns all possible options for this facet.
*
* @return array<FacetOptionInterface>
*/
public function getOptions();
}

View file

@ -0,0 +1,42 @@
<?php
namespace Codappix\SearchCore\Connection;
/*
* 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.
*/
/**
* A single possible option of a facet.
*/
interface FacetOptionInterface
{
/**
* Returns the name of this option. Equivalent
* to value used for filtering.
*
* @return string
*/
public function getName();
/**
* Returns the number of found results for this option.
*
* @return int
*/
public function getCount();
}

View file

@ -0,0 +1,42 @@
<?php
namespace Codappix\SearchCore\Connection;
/*
* 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.
*/
/**
* Used to request facets / aggregates from connection.
*/
interface FacetRequestInterface
{
/**
* The identifier of the facet, used as key in arrays and to get the facet
* from search request, etc.
*
* @return string
*/
public function getIdentifier();
/**
* The field to use for facet building.
*
* @return string
*/
public function getField();
}

View file

@ -0,0 +1,29 @@
<?php
namespace Codappix\SearchCore\Connection;
/*
* 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 ArrayAccess to enable retrieval of information in fluid.
*/
interface ResultItemInterface extends \ArrayAccess
{
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection;
namespace Codappix\SearchCore\Connection;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -31,4 +31,14 @@ interface SearchRequestInterface
* @return string
*/
public function getSearchTerm();
/**
* @return bool
*/
public function hasFilter();
/**
* @return array
*/
public function getFilter();
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Connection;
namespace Codappix\SearchCore\Connection;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -21,9 +21,35 @@ namespace Leonmrni\SearchCore\Connection;
*/
/**
*
* A search result.
*/
interface SearchResultInterface extends \Iterator, \Countable, \ArrayAccess
interface SearchResultInterface extends \Iterator, \Countable
{
/**
* @return array<ResultItemInterface>
*/
public function getResults();
/**
* Return all facets, if any.
*
* @return array<FacetIterface>
*/
public function getFacets();
/**
* Returns the total sum of matching results.
*
* @return int
*/
public function getTotalCount();
// Countable - Interface
/**
* Returns the total sum of results contained in this result.
*
* @return int
*/
public function count();
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Controller;
namespace Codappix\SearchCore\Controller;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Controller;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Domain\Model\SearchRequest;
use Leonmrni\SearchCore\Domain\Search\SearchService;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\SearchService;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
/**

View file

@ -0,0 +1,113 @@
<?php
namespace Codappix\SearchCore\Domain\Index;
/*
* 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\Connection\ConnectionInterface;
abstract class AbstractIndexer implements IndexerInterface
{
/**
* @var ConnectionInterface
*/
protected $connection;
/**
* @var \TYPO3\CMS\Core\Log\Logger
*/
protected $logger;
/**
* Inject log manager to get concrete logger from it.
*
* @param \TYPO3\CMS\Core\Log\LogManager $logManager
*/
public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager)
{
$this->logger = $logManager->getLogger(__CLASS__);
}
/**
* @param ConnectionInterface $connection
*/
public function __construct(ConnectionInterface $connection)
{
$this->connection = $connection;
}
public function indexAllDocuments()
{
$this->logger->info('Start indexing');
foreach ($this->getRecordGenerator() as $records) {
$this->logger->debug('Index records.', [$records]);
if ($records === null) {
break;
}
$this->connection->addDocuments($this->getDocumentName(), $records);
}
$this->logger->info('Finish indexing');
}
public function indexDocument($identifier)
{
$this->logger->info('Start indexing single record.', [$identifier]);
try {
$this->connection->addDocument($this->getDocumentName(), $this->getRecord($identifier));
} catch (NoRecordFoundException $e) {
$this->logger->info('Could not index document.', [$e->getMessage()]);
}
$this->logger->info('Finish indexing');
}
/**
* @return \Generator
*/
protected function getRecordGenerator()
{
$offset = 0;
// TODO: Make configurable.
$limit = 50;
while (($records = $this->getRecords($offset, $limit)) !== []) {
yield $records;
$offset += $limit;
}
}
/**
* @param int $offset
* @param int $limit
* @return array|null
*/
abstract protected function getRecords($offset, $limit);
/**
* @param int $identifier
* @return array
* @throws NoRecordFoundException If record could not be found.
*/
abstract protected function getRecord($identifier);
/**
* @return string
*/
abstract protected function getDocumentName();
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index;
namespace Codappix\SearchCore\Domain\Index;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,6 +20,11 @@ namespace Leonmrni\SearchCore\Domain\Index;
* 02110-1301, USA.
*/
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Domain\Index\IndexerInterface;
use Codappix\SearchCore\Domain\Index\TcaIndexer;
use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
@ -34,27 +39,61 @@ class IndexerFactory implements Singleton
protected $objectManager;
/**
* @param ObjectManagerInterface $objectManager
* @var ConfigurationContainerInterface
*/
public function __construct(ObjectManagerInterface $objectManager)
{
protected $configuration;
/**
* @param ObjectManagerInterface $objectManager
* @param ConfigurationContainerInterface $configuration
*/
public function __construct(
ObjectManagerInterface $objectManager,
ConfigurationContainerInterface $configuration
) {
$this->objectManager = $objectManager;
$this->configuration = $configuration;
}
/**
* @param string $tableName
* @param string $identifier
*
* @return IndexerInterface
* @throws NoMatchingIndexer
*/
public function getIndexer($tableName)
public function getIndexer($identifier)
{
// This is the place to use configuration to return different indexer.
return $this->objectManager->get(
TcaIndexer::Class,
$this->objectManager->get(
TcaIndexer\TcaTableService::class,
$tableName
)
);
try {
return $this->buildIndexer($this->configuration->get('indexing.' . $identifier . '.indexer'), $identifier);
} catch (NoMatchingIndexerException $e) {
// Nothing to do, we throw exception below
} catch (InvalidArgumentException $e) {
// Nothing to do, we throw exception below
}
throw new NoMatchingIndexerException('Could not find an indexer for ' . $identifier, 1497341442);
}
/**
* @param string $indexerClass
* @param string $identifier
*
* @return IndexerInterface
* @throws NoMatchingIndexer
*/
protected function buildIndexer($indexerClass, $identifier)
{
if ($indexerClass === TcaIndexer::class) {
return $this->objectManager->get(
TcaIndexer::class,
$this->objectManager->get(TcaTableService::class, $identifier)
);
}
if (class_exists($indexerClass) && in_array(IndexerInterface::class, class_implements($indexerClass))) {
return $this->objectManager->get($indexerClass);
}
throw new NoMatchingIndexerException('Could not find indexer: ' . $indexerClass, 1497341442);
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index;
namespace Codappix\SearchCore\Domain\Index;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index;
namespace Codappix\SearchCore\Domain\Index;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -0,0 +1,25 @@
<?php
namespace Codappix\SearchCore\Domain\Index;
/*
* 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.
*/
class NoMatchingIndexerException extends IndexingException
{
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index;
namespace Codappix\SearchCore\Domain\Index;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index;
namespace Codappix\SearchCore\Domain\Index;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,39 +20,18 @@ namespace Leonmrni\SearchCore\Domain\Index;
* 02110-1301, USA.
*/
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use Leonmrni\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\ConnectionInterface;
/**
* Will index the given table using configuration from TCA.
*/
class TcaIndexer implements IndexerInterface
class TcaIndexer extends AbstractIndexer
{
/**
* @var ConnectionInterface
*/
protected $connection;
/**
* @var TcaIndexer\TcaTableService
*/
protected $tcaTableService;
/**
* @var \TYPO3\CMS\Core\Log\Logger
*/
protected $logger;
/**
* Inject log manager to get concrete logger from it.
*
* @param \TYPO3\CMS\Core\Log\LogManager $logManager
*/
public function injectLogger(\TYPO3\CMS\Core\Log\LogManager $logManager)
{
$this->logger = $logManager->getLogger(__CLASS__);
}
/**
* @param TcaIndexer\TcaTableService $tcaTableService
* @param ConnectionInterface $connection
@ -65,46 +44,6 @@ class TcaIndexer implements IndexerInterface
$this->connection = $connection;
}
public function indexAllDocuments()
{
$this->logger->info('Start indexing');
foreach ($this->getRecordGenerator() as $records) {
$this->logger->debug('Index records.', [$records]);
if ($records === null) {
break;
}
$this->connection->addDocuments($this->tcaTableService->getTableName(), $records);
}
$this->logger->info('Finish indexing');
}
public function indexDocument($identifier)
{
$this->logger->info('Start indexing single record.', [$identifier]);
try {
$this->connection->addDocument($this->tcaTableService->getTableName(), $this->getRecord($identifier));
} catch (NoRecordFoundException $e) {
$this->logger->info('Could not index document.', [$e->getMessage()]);
}
$this->logger->info('Finish indexing');
}
/**
* @return \Generator
*/
protected function getRecordGenerator()
{
$offset = 0;
// TODO: Make configurable.
$limit = 50;
while (($records = $this->getRecords($offset, $limit)) !== []) {
yield $records;
$offset += $limit;
}
}
/**
* @param int $offset
* @param int $limit
@ -157,6 +96,14 @@ class TcaIndexer implements IndexerInterface
return $record;
}
/**
* @return string
*/
protected function getDocumentName()
{
return $this->tcaTableService->getTableName();
}
protected function getDatabaseConnection()
{
return GeneralUtility::makeInstance(ConnectionPool::class)

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
namespace Codappix\SearchCore\Domain\Index\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
namespace Codappix\SearchCore\Domain\Index\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
namespace Codappix\SearchCore\Domain\Index\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Domain\Index\TcaIndexer;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Domain\Index\IndexingException;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Index\IndexingException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@ -151,7 +151,7 @@ class TcaTableService
. ' AND pages.no_search = 0'
;
$userDefinedWhere = $this->configuration->getIfExists('indexer.tca.' . $this->tableName . '.additionalWhereClause');
$userDefinedWhere = $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.additionalWhereClause');
if (is_string($userDefinedWhere)) {
$whereClause .= ' AND ' . $userDefinedWhere;
}
@ -269,7 +269,7 @@ class TcaTableService
*/
protected function isBlackListedRootLineConfigured()
{
return (bool) $this->configuration->getIfExists('indexer.tca.rootLineBlacklist');
return (bool) $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist');
}
/**
@ -279,6 +279,6 @@ class TcaTableService
*/
protected function getBlackListedRootLine()
{
return GeneralUtility::intExplode(',', $this->configuration->getIfExists('indexer.tca.rootLineBlacklist'));
return GeneralUtility::intExplode(',', $this->configuration->getIfExists('indexing.' . $this->getTableName() . '.rootLineBlacklist'));
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Codappix\SearchCore\Domain\Model;
/*
* 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\Connection\FacetRequestInterface;
class FacetRequest implements FacetRequestInterface
{
/**
* @var string
*/
protected $identifier = '';
/**
* @var string
*/
protected $field = '';
/**
* TODO: Add validation / exception?
* As the facets come from configuration this might be a good idea to help
* integrators find issues.
*
* @param string $identifier
* @param string $field
*/
public function __construct($identifier, $field)
{
$this->identifier = $identifier;
$this->field = $field;
}
/**
* @return string
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* @return string
*/
public function getField()
{
return $this->field;
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Model;
namespace Codappix\SearchCore\Domain\Model;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,8 @@ namespace Leonmrni\SearchCore\Domain\Model;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Connection\FacetRequestInterface;
use Codappix\SearchCore\Connection\SearchRequestInterface;
/**
* Represents a search request used to process an actual search.
@ -32,14 +33,24 @@ class SearchRequest implements SearchRequestInterface
*
* @var string
*/
protected $query;
protected $query = '';
/**
* @var array
*/
protected $filter = [];
/**
* @var array
*/
protected $facets = [];
/**
* @param string $query
*/
public function __construct($query)
{
$this->query = $query;
$this->query = (string) $query;
}
/**
@ -57,4 +68,48 @@ class SearchRequest implements SearchRequestInterface
{
return $this->query;
}
/**
* @param array $filter
*/
public function setFilter(array $filter)
{
$this->filter = array_filter(array_map('strval', $filter));
}
/**
* @return bool
*/
public function hasFilter()
{
return count($this->filter) > 0;
}
/**
* @return array
*/
public function getFilter()
{
return $this->filter;
}
/**
* Add a facet to gather in this search request.
*
* @param FacetRequestInterface $facet
*/
public function addFacet(FacetRequestInterface $facet)
{
$this->facets[$facet->getIdentifier()] = $facet;
}
/**
* Returns all configured facets to fetch in this search request.
*
* @return array
*/
public function getFacets()
{
return $this->facets;
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace Codappix\SearchCore\Domain\Search;
/*
* 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\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\Elasticsearch\Query;
use Codappix\SearchCore\Connection\SearchRequestInterface;
use TYPO3\CMS\Extbase\Utility\ArrayUtility;
class QueryFactory
{
/**
* @var \TYPO3\CMS\Core\Log\Logger
*/
protected $logger;
/**
* @var array
*/
protected $query = [];
/**
* @param \TYPO3\CMS\Core\Log\LogManager $logManager
*/
public function __construct(\TYPO3\CMS\Core\Log\LogManager $logManager)
{
$this->logger = $logManager->getLogger(__CLASS__);
}
/**
* @param SearchRequestInterface $searchRequest
*
* @return \Elastica\Query
*/
public function create(SearchRequestInterface $searchRequest)
{
return $this->createElasticaQuery($searchRequest);
}
/**
* @param SearchRequestInterface $searchRequest
*
* TODO: This is not in scope Elasticsearch, therefore should not return elastica.
* @return \Elastica\Query
*/
protected function createElasticaQuery(SearchRequestInterface $searchRequest)
{
$this->addSearch($searchRequest);
$this->addFilter($searchRequest);
$this->addFacets($searchRequest);
$this->logger->debug('Generated elasticsearch query.', [$this->query]);
return new \Elastica\Query($this->query);
}
/**
* @param SearchRequestInterface $searchRequest
*/
protected function addSearch(SearchRequestInterface $searchRequest)
{
$this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [
'query' => [
'bool' => [
'must' => [
[
'match' => [
'_all' => $searchRequest->getSearchTerm()
],
],
],
],
],
]);
}
/**
* @param SearchRequestInterface $searchRequest
*/
protected function addFilter(SearchRequestInterface $searchRequest)
{
if (! $searchRequest->hasFilter()) {
return;
}
$terms = [];
foreach ($searchRequest->getFilter() as $name => $value) {
$terms[] = [
'term' => [
$name => $value,
],
];
}
$this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [
'query' => [
'bool' => [
'filter' => $terms,
],
],
]);
}
/**
* @param SearchRequestInterface $searchRequest
*/
protected function addFacets(SearchRequestInterface $searchRequest)
{
foreach ($searchRequest->getFacets() as $facet) {
$this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [
'aggs' => [
$facet->getIdentifier() => [
'terms' => [
'field' => $facet->getField(),
],
],
],
]);
}
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Search;
namespace Codappix\SearchCore\Domain\Search;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,10 +20,13 @@ namespace Leonmrni\SearchCore\Domain\Search;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Connection\ConnectionInterface;
use Leonmrni\SearchCore\Connection\SearchRequestInterface;
use Leonmrni\SearchCore\Connection\SearchResultInterface;
use Leonmrni\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Connection\SearchResultInterface;
use Codappix\SearchCore\Domain\Model\FacetRequest;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* Service to process a search request.
@ -36,11 +39,28 @@ class SearchService
protected $connection;
/**
* @param ConnectionInterface $connection
* @var ConfigurationContainerInterface
*/
public function __construct(ConnectionInterface $connection)
{
protected $configuration;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param ConnectionInterface $connection
* @param ConfigurationContainerInterface $configuration
* @param ObjectManagerInterface $objectManager
*/
public function __construct(
ConnectionInterface $connection,
ConfigurationContainerInterface $configuration,
ObjectManagerInterface $objectManager
) {
$this->connection = $connection;
$this->configuration = $configuration;
$this->objectManager = $objectManager;
}
/**
@ -49,6 +69,34 @@ class SearchService
*/
public function search(SearchRequestInterface $searchRequest)
{
$this->addConfiguredFacets($searchRequest);
return $this->connection->search($searchRequest);
}
/**
* Add facets from configuration to request.
*
* @param SearchRequestInterface $searchRequest
*/
protected function addConfiguredFacets(SearchRequestInterface $searchRequest)
{
$facetsConfig = $this->configuration->getIfExists('searching.facets');
if ($facetsConfig === null) {
return;
}
foreach ($facetsConfig as $identifier => $facetConfig) {
if (!isset($facetConfig['field']) || trim($facetConfig['field']) === '') {
// TODO: Finish throw
throw new \Exception('message', 1499171142);
}
$searchRequest->addFacet($this->objectManager->get(
FacetRequest::class,
$identifier,
$facetConfig['field']
));
}
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Domain\Service;
namespace Codappix\SearchCore\Domain\Service;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,10 @@ namespace Leonmrni\SearchCore\Domain\Service;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException;
use Codappix\SearchCore\Domain\Index\TcaIndexer;
use TYPO3\CMS\Core\SingletonInterface as Singleton;
use TYPO3\CMS\Core\Utility\GeneralUtility;
@ -40,14 +43,13 @@ class DataHandler implements Singleton
/**
* TODO: Only inject on first use?!
*
* @var \Leonmrni\SearchCore\Connection\ConnectionInterface
* @var \Codappix\SearchCore\Connection\ConnectionInterface
* @inject
*/
protected $connection;
/**
* @var \Leonmrni\SearchCore\Domain\Index\IndexerFactory
* @inject
* @var IndexerFactory
*/
protected $indexerFactory;
@ -73,20 +75,12 @@ class DataHandler implements Singleton
/**
* @param ConfigurationContainerInterface $configuration
* @param IndexerFactory $indexerFactory
*/
public function __construct(ConfigurationContainerInterface $configuration)
public function __construct(ConfigurationContainerInterface $configuration, IndexerFactory $indexerFactory)
{
$this->configuration = $configuration;
}
/**
* Get all tables that are allowed for indexing.
*
* @return array<String>
*/
public function getAllowedTablesForIndexing()
{
return GeneralUtility::trimExplode(',', $this->configuration->get('indexer.tca.allowedTables'));
$this->indexerFactory = $indexerFactory;
}
/**
@ -96,7 +90,7 @@ class DataHandler implements Singleton
public function add($table, array $record)
{
$this->logger->debug('Record received for add.', [$table, $record]);
$this->indexerFactory->getIndexer($table)->indexDocument($record['uid']);
$this->getIndexer($table)->indexDocument($record['uid']);
}
/**
@ -105,7 +99,7 @@ class DataHandler implements Singleton
public function update($table, array $record)
{
$this->logger->debug('Record received for update.', [$table, $record]);
$this->indexerFactory->getIndexer($table)->indexDocument($record['uid']);
$this->getIndexer($table)->indexDocument($record['uid']);
}
/**
@ -117,4 +111,31 @@ class DataHandler implements Singleton
$this->logger->debug('Record received for delete.', [$table, $identifier]);
$this->connection->deleteDocument($table, $identifier);
}
/**
* @param string $table
* @return IndexerInterface
*
* @throws NoMatchingIndexerException
*/
protected function getIndexer($table)
{
return $this->indexerFactory->getIndexer($table);
}
/**
* @param string $table
* @return bool
*/
public function canHandle($table)
{
try {
$this->getIndexer($table);
return true;
} catch (NoMatchingIndexerException $e) {
return false;
}
return false;
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Hook;
namespace Codappix\SearchCore\Hook;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,8 +20,9 @@ namespace Leonmrni\SearchCore\Hook;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\NoConfigurationException;
use Leonmrni\SearchCore\Domain\Service\DataHandler as OwnDataHandler;
use Codappix\SearchCore\Configuration\NoConfigurationException;
use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException;
use Codappix\SearchCore\Domain\Service\DataHandler as OwnDataHandler;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler as CoreDataHandler;
use TYPO3\CMS\Core\Log\LogManager;
@ -140,7 +141,7 @@ class DataHandler implements Singleton
$this->logger->debug('Datahandler could not be setup.');
return false;
}
if (! $this->shouldProcessTable($table)) {
if (! $this->dataHandler->canHandle($table)) {
$this->logger->debug('Table is not allowed.', [$table]);
return false;
}
@ -148,15 +149,6 @@ class DataHandler implements Singleton
return true;
}
/**
* @param string $table
* @return bool
*/
protected function shouldProcessTable($table)
{
return in_array($table, $this->dataHandler->getAllowedTablesForIndexing());
}
/**
* Wrapper to allow unit testing.
*

24
Configuration/TypoScript/constants.txt Executable file → Normal file
View file

@ -8,20 +8,16 @@ plugin {
}
}
indexer {
tca {
# Pages are not supported yet, see
# https://github.com/DanielSiepmann/search_core/issues/24 but
# should also be added, together with additionalWhereClause
# based on doktypes
allowedTables = tt_content
tt_content {
additionalWhereClause (
pages.doktype NOT IN (3, 199)
AND tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu', 'shortcut', 'search', 'login')
)
}
indexing {
# Pages are not supported yet, see
# https://github.com/DanielSiepmann/search_core/issues/24 but
# should also be added, together with additionalWhereClause
# based on doktypes
tt_content {
additionalWhereClause (
pages.doktype NOT IN (3, 199)
AND tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu', 'shortcut', 'search', 'login')
)
}
}
}

11
Configuration/TypoScript/setup.txt Executable file → Normal file
View file

@ -8,13 +8,10 @@ plugin {
}
}
indexer {
tca {
allowedTables = {$plugin.tx_searchcore.settings.indexer.tca.allowedTables}
tt_content {
additionalWhereClause = {$plugin.tx_searchcore.settings.indexer.tca.tt_content.additionalWhereClause}
}
indexing {
tt_content {
indexer = Codappix\SearchCore\Domain\Index\TcaIndexer
additionalWhereClause = {$plugin.tx_searchcore.settings.indexing.tt_content.additionalWhereClause}
}
}
}

View file

@ -24,7 +24,7 @@ Currently only :ref:`Elasticsearch` is provided.
Indexing
--------
The indexing is done by one of the available indexer. It should be possible to define the indexer to
use for certein document types. Also it should be possible to write custom indexer to use.
The indexing is done by one of the available indexer. For each identifier it's possible to define
the indexer to use. Also it's possible to write custom indexer to use.
Currently only the :ref:`TcaIndexer` is provided.

View file

@ -120,7 +120,7 @@ html_theme = 'alabaster'
# documentation.
html_theme_options = {
'description': 'TYPO3 Extension to integrate search services.',
'github_user': 'DanielSiepmann',
'github_user': 'Codappix',
'github_repo': 'search_core',
'github_button': True,
'github_banner': True,
@ -306,7 +306,7 @@ intersphinx_mapping = {
't3tcaref': ('https://docs.typo3.org/typo3cms/TCAReference/', None),
}
extlinks = {
'project': ('https://github.com/DanielSiepmann/search_core/projects/%s', 'Github project: '),
'pr': ('https://github.com/DanielSiepmann/search_core/pull/%s', 'Github pull request: '),
'issue': ('https://github.com/DanielSiepmann/search_core/issues/%s', 'Github issue: '),
'project': ('https://github.com/Codappix/search_core/projects/%s', 'Github project: '),
'pr': ('https://github.com/Codappix/search_core/pull/%s', 'Github pull request: '),
'issue': ('https://github.com/Codappix/search_core/issues/%s', 'Github issue: '),
}

View file

@ -38,7 +38,7 @@ Options
The following section contains the different options, e.g. for :ref:`connections` and
:ref:`indexer`: ``plugin.tx_searchcore.settings.connection`` or
``plugin.tx_searchcore.settings.index``.
``plugin.tx_searchcore.settings.indexing``.
.. _configuration_options_connection:
@ -96,8 +96,8 @@ The following settings are available. For each setting its documented which conn
.. _configuration_options_index:
index
^^^^^
Indexing
^^^^^^^^
Holds settings regarding the indexing, e.g. of TYPO3 records, to search services.
@ -106,8 +106,9 @@ Configured as::
plugin {
tx_searchcore {
settings {
indexer {
indexerName {
indexing {
identifier {
indexer = FullyQualifiedClassname
// the settings
}
}
@ -115,26 +116,10 @@ Configured as::
}
}
Where ``indexerName`` is one of the available :ref:`indexer`.
Where ``identifier`` is up to you, but should match table names to make :ref:`TcaIndexer` work.
The following settings are available. For each setting its documented which indexer consumes it.
.. _allowedTables:
``allowedTables``
"""""""""""""""""
Used by: :ref:`TcaIndexer`.
Defines which TYPO3 tables are allowed to be indexed. Only white listed tables will be processed
through Command Line Interface and Hooks.
Contains a comma separated list of table names. Spaces are trimmed.
Example::
plugin.tx_searchcore.settings.indexer.tca.allowedTables = tt_content, fe_users
.. _rootLineBlacklist:
``rootLineBlacklist``
@ -151,7 +136,7 @@ The following settings are available. For each setting its documented which inde
Example::
plugin.tx_searchcore.settings.index.tca.rootLineBlacklist = 3, 10, 100
plugin.tx_searchcore.settings.indexing.<identifier>.rootLineBlacklist = 3, 10, 100
Also it's possible to define some behaviour for the different document types. In context of TYPO3
tables are used as document types 1:1. It's possible to configure different tables. The following
@ -170,9 +155,57 @@ options are available:
Example::
plugin.tx_searchcore.settings.index.tca.tt_content.additionalWhereClause = tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu')
plugin.tx_searchcore.settings.indexing.<identifier>.additionalWhereClause = tt_content.CType NOT IN ('gridelements_pi1', 'list', 'div', 'menu')
.. attention::
Make sure to prefix all fields with the corresponding table name. The selection from
database will contain joins and can lead to SQL errors if a field exists in multiple tables.
.. _mapping:
``mapping``
"""""""""""
Used by: Elasticsearch connection while indexing.
Define mapping for Elasticsearch, have a look at the official docs: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/mapping.html
You are able to define the mapping for each property / columns.
Example::
plugin.tx_searchcore.settings.indexing.tt_content.mapping {
CType {
type = keyword
}
}
The above example will define the ``CType`` field of ``tt_content`` as ``type: keyword``. This
makes building a facet possible.
.. _configuration_options_search:
Searching
^^^^^^^^^
.. _facets:
``facets``
"""""""""""
Used by: Elasticsearch connection while building search query.
Define aggregations for Elasticsearch, have a look at the official docs: https://www.elastic.co/guide/en/elasticsearch/reference/5.2/search-aggregations-bucket-terms-aggregation.html
Currently only the term facet is provided.
Example::
plugin.tx_searchcore.settings.searching.facets {
contentTypes {
field = CType
}
}
The above example will provide a facet with options for all found ``CType`` results together
with a count.

View file

@ -22,5 +22,9 @@ The connection is configurable through the following options:
* :ref:`port`
* :ref:`mapping`
* :ref:`facets`
.. _elastic Elasticsearch: https://www.elastic.co/products/elasticsearch
.. _elastica: http://elastica.io/

View file

@ -32,9 +32,10 @@ Then setup your system::
git clone git@github.com:DanielSiepmann/search_core.git \
&& cd search_core \
&& export typo3DatabaseName="searchcoretest62" \
&& export TYPO3_VERSION="~6.2" \
&& export typo3DatabaseName="searchcoretest76" \
&& export TYPO3_VERSION="~7.6" \
&& make install \
&& make unitTests \
&& make functionalTests
If all tests are okay, start your work.
@ -42,8 +43,8 @@ If all tests are okay, start your work.
If you are working with multiple TYPO3 versions make sure to export `typo3DatabaseName` and
`TYPO3_VERSION` in your environment like::
export typo3DatabaseName="searchcoretest76"
export TYPO3_VERSION="~7.6"
export typo3DatabaseName="searchcoretest62"
export TYPO3_VERSION="~6.2"
Also run the install command for each version before running any tests. Only this will make sure you
are testing against the actual TYPO3 Version and database scheme.

View file

@ -0,0 +1,38 @@
.. _features:
Features
========
The following features are currently provided:
.. _features_indexing:
Indexing
--------
Indexing data to Elasticsearch is provided. The extension delivers an indexer for TCA with zero
configuration needs. Still it's possible to configure the indexer.
Also custom classes can be used as indexers.
.. _features_search:
Searching
---------
Currently all fields are searched for a single search input.
Also multiple filter are supported. Filtering results by fields for string contents.
Even facets / aggregates are now possible. Therefore a mapping has to be defined in TypoScript for
indexing, and the facets itself while searching.
.. _features_planned:
Planned
---------
The following features are currently planned and will be integrated:
#. Pagination
Add a pagination to search results, to allow users to walk through all results.

View file

@ -7,6 +7,7 @@ Table of Contents
:maxdepth: 1
:glob:
features
installation
configuration
usage

View file

@ -6,7 +6,7 @@ Installation
The extension can be installed through composer::
composer require "leonmrni/search_core dev-feature/integrate-elasticsearch"
composer require "leonmrni/search_core dev-master as 1.0.x-dev"
or by `downloading`_ and placing it inside the :file:`typo3conf/ext`-Folder of your installation.
In that case you need to install all dependencies yourself. Dependencies are:
@ -16,8 +16,7 @@ In that case you need to install all dependencies yourself. Dependencies are:
:lines: 19-21
:dedent: 8
Afterwards you need to enable the extension through the extension manager and include the static
typoscript setup.
TypoScript setup.
.. _downloading: https://github.com/DanielSiepmann/search_core/archive/feature/integrate-elasticsearch.zip
.. _downloading: https://github.com/DanielSiepmann/search_core/archive/master.zip

View file

@ -18,14 +18,14 @@ reindexing.
Current state
-------------
This is still a very early alpha version. More information can be taken from Github at
This is still a very early beta version. More information can be taken from Github at
`current issues`_ and `current projects`_.
We are also focusing on Code Quality and Testing through `travis ci`_, `scrutinizer`_ and `codacy`_.
.. _current issues: https://github.com/DanielSiepmann/search_core/issues
.. _current projects: https://github.com/DanielSiepmann/search_core/projects
.. _travis ci: https://travis-ci.org/DanielSiepmann/search_core
.. _scrutinizer: https://scrutinizer-ci.com/g/DanielSiepmann/search_core/inspections
.. _codacy: https://www.codacy.com/app/daniel-siepmann/search_core/dashboard
.. _current issues: https://github.com/Codappix/search_core/issues
.. _current projects: https://github.com/Codappix/search_core/projects
.. _travis ci: https://travis-ci.org/Codappix/search_core
.. _scrutinizer: https://scrutinizer-ci.com/g/Codappix/search_core/inspections
.. _codacy: https://www.codacy.com/app/Codappix/search_core/dashboard

View file

@ -11,12 +11,12 @@ Manual indexing
You can trigger indexing from CLI::
./typo3/cli_dispatch.phpsh extbase index:index --table 'tt_content'
./typo3/cli_dispatch.phpsh extbase index:index --identifier 'tt_content'
This will index the table ``tt_content`` using the :ref:`TcaIndexer`.
Only one table per call is available, to index multiple tables just make multiple calls.
The tables have to be white listed through :ref:`allowedTables` option.
Only one index per call is available, to run multiple indexers, just make multiple calls.
The indexers have to be defined in TypoScript via :ref:`configuration_options_index`.
.. _usage_auto_indexing:
@ -24,7 +24,7 @@ Auto indexing
-------------
Indexing is done through hooks every time an record is changed.
The tables have to be white listed through :ref:`allowedTables` option.
The tables have to be configured via :ref:`configuration_options_index`.
.. note::
@ -37,3 +37,48 @@ Searching / Frontend Plugin
To provide a search interface you can insert the frontend Plugin as normal content element of type
plugin. The plugin is named *Search Core*.
Please provide your own template, the extension will not deliver a useful template for now.
The extbase mapping is used, this way you can create a form:
.. code-block:: html
<f:form name="searchRequest" object="{searchRequest}">
<f:form.textfield property="query" />
<f:form.submit value="search" />
</f:form>
.. _usage_searching_filter:
Filter
""""""
Thanks to extbase mapping, filter are added to the form:
.. code-block:: html
<!-- Case sensitive for fields of type keyword. -->
<f:form.textfield property="filter.exampleName" value="the value to match" />
.. _usage_searching_facets:
Facets
""""""
To add a facet as criteria for searching, use :ref:`usage_searching_filter`.
To display facet results use:
.. code-block:: html
<f:for each="{searchResult.facets}" as="facet">
<f:for each="{facet.options}" as="option">
<label for="{option.name}-desktop">
<f:form.checkbox value="{option.name}" property="filter.{facet.field}" />
{f:translate(id: 'search.filter.channel.{option.name}', default: option.name, extensionName: 'SitePackage')}
({option.count})
</label>
</f:for>
</f:for>

View file

@ -25,6 +25,11 @@ functionalTests:
--stop-on-error \
-c Tests/Functional/FunctionalTests.xml
unitTests:
TYPO3_PATH_WEB=$(TYPO3_WEB_DIR) \
.Build/bin/phpunit --colors --debug -v \
-c Tests/Unit/UnitTests.xml
uploadCodeCoverage: uploadCodeCoverageToScrutinizer uploadCodeCoverageToCodacy
uploadCodeCoverageToScrutinizer:

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional;
namespace Codappix\SearchCore\Tests\Functional;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch;
namespace Codappix\SearchCore\Tests\Functional\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase as BaseFunctionalTestCase;
use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase as BaseFunctionalTestCase;
/**
* All functional tests should extend this base class.
@ -43,11 +43,19 @@ abstract class AbstractFunctionalTestCase extends BaseFunctionalTestCase
'host' => getenv('ES_HOST') ?: \Elastica\Connection::DEFAULT_HOST,
'port' => getenv('ES_PORT') ?: \Elastica\Connection::DEFAULT_PORT,
]);
// Start with clean system for test.
$this->cleanUp();
}
public function tearDown()
{
// Delete everything so next test starts clean.
// Make system clean again.
$this->cleanUp();
}
protected function cleanUp()
{
$this->client->getIndex('_all')->delete();
$this->client->getIndex('_all')->clearCache();
}

View file

@ -0,0 +1,94 @@
<?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 FilterTest extends AbstractFunctionalTestCase
{
protected function getDataSets()
{
return array_merge(
parent::getDataSets(),
['Tests/Functional/Fixtures/Searching/Filter.xml']
);
}
/**
* @test
*/
public function itsPossibleToFilterResultsByASingleField()
{
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->indexAllDocuments()
;
$searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(SearchService::class);
$searchRequest = new SearchRequest('Search Word');
$result = $searchService->search($searchRequest);
$this->assertSame(2, count($result), 'Did not receive both indexed elements without filter.');
$searchRequest->setFilter(['CType' => 'HTML']);
$result = $searchService->search($searchRequest);
$this->assertSame('5', $result->getResults()[0]['uid'], 'Did not get the expected result entry.');
$this->assertSame(1, count($result), 'Did not receive the single filtered element.');
}
/**
* @test
*/
public function itsPossibleToFetchFacetsForField()
{
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(IndexerFactory::class)
->getIndexer('tt_content')
->indexAllDocuments()
;
$searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class)
->get(SearchService::class);
$searchRequest = new SearchRequest('Search Word');
$result = $searchService->search($searchRequest);
$this->assertSame(1, count($result->getFacets()), 'Did not receive the single defined facet.');
$facet = $result->getFacets()[0];
$this->assertSame('contentTypes', $facet->getName(), 'Name of facet was not as expected.');
$this->assertSame('CType', $facet->getField(), 'Field of facet was not expected.');
$options = $facet->getOptions();
$this->assertSame(2, count($options), 'Did not receive the expected number of possible options for facet.');
$option = $options[0];
$this->assertSame('HTML', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
$option = $options[1];
$this->assertSame('Header', $option->getName(), 'Option did not have expected Name.');
$this->assertSame(1, $option->getCount(), 'Option did not have expected count.');
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch;
namespace Codappix\SearchCore\Tests\Functional\Connection\Elasticsearch;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,7 +20,7 @@ namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use TYPO3\CMS\Extbase\Object\ObjectManager;
/**
@ -61,7 +61,7 @@ class IndexTcaTableTest extends AbstractFunctionalTestCase
/**
* @test
* @expectedException \Leonmrni\SearchCore\Domain\Index\IndexingException
* @expectedException \Codappix\SearchCore\Domain\Index\IndexingException
*/
public function indexingNonConfiguredTableWillThrowException()
{

View file

@ -8,9 +8,23 @@ plugin {
}
}
indexer {
tca {
allowedTables = tt_content
indexing {
tt_content {
indexer = Codappix\SearchCore\Domain\Index\TcaIndexer
mapping {
CType {
type = keyword
}
}
}
}
searching {
facets {
contentTypes {
field = CType
}
}
}
}

View file

@ -1,8 +1,8 @@
plugin {
tx_searchcore {
settings {
indexer {
tca {
indexing {
tt_content {
rootLineBlacklist = 3
}
}

View file

@ -1,11 +1,9 @@
plugin {
tx_searchcore {
settings {
indexer {
tca {
tt_content {
additionalWhereClause = tt_content.CType NOT IN ('div')
}
indexing {
tt_content {
additionalWhereClause = tt_content.CType NOT IN ('div')
}
}
}

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<tt_content>
<uid>5</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>html</CType>
<header>indexed content element with html ctype</header>
<bodytext>Search Word</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<filelink_sorting>0</filelink_sorting>
</tt_content>
<tt_content>
<uid>6</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>header</CType>
<header>indexed content element with header ctype</header>
<bodytext>Search Word</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<filelink_sorting>0</filelink_sorting>
</tt_content>
</dataset>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,10 +20,11 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook;
use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
@ -42,7 +43,10 @@ abstract class AbstractDataHandlerTest extends AbstractFunctionalTestCase
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
$this->subject = $this->getMockBuilder(DataHandlerService::class)
->setConstructorArgs([$objectManager->get(ConfigurationContainerInterface::class)])
->setConstructorArgs([
$objectManager->get(ConfigurationContainerInterface::class),
$objectManager->get(IndexerFactory::class)
])
->setMethods(['add', 'update', 'delete'])
->getMock();

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,9 +20,9 @@ namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Leonmrni\SearchCore\Hook\DataHandler as DataHandlerHook;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Service\DataHandler as DataHandlerService;
use Codappix\SearchCore\Hook\DataHandler as DataHandlerHook;
use TYPO3\CMS\Core\DataHandling\DataHandler as Typo3DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Hooks\DataHandler;
namespace Codappix\SearchCore\Tests\Functional\Hooks\DataHandler;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Indexing\TcaIndexer;
namespace Codappix\SearchCore\Tests\Indexing\TcaIndexer;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,8 +20,8 @@ namespace Leonmrni\SearchCore\Tests\Indexing\TcaIndexer;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;

View file

@ -1,5 +1,5 @@
<?php
namespace Leonmrni\SearchCore\Tests\Indexing;
namespace Codappix\SearchCore\Tests\Indexing;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
@ -20,12 +20,12 @@ namespace Leonmrni\SearchCore\Tests\Indexing;
* 02110-1301, USA.
*/
use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface;
use Leonmrni\SearchCore\Connection\Elasticsearch;
use Leonmrni\SearchCore\Domain\Index\TcaIndexer;
use Leonmrni\SearchCore\Domain\Index\TcaIndexer\RelationResolver;
use Leonmrni\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use Leonmrni\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Connection\Elasticsearch;
use Codappix\SearchCore\Domain\Index\TcaIndexer;
use Codappix\SearchCore\Domain\Index\TcaIndexer\RelationResolver;
use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use Codappix\SearchCore\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3\CMS\Extbase\Object\ObjectManager;
class TcaIndexerTest extends AbstractFunctionalTestCase

View file

@ -0,0 +1,46 @@
<?php
namespace Codappix\SearchCore\Tests\Unit;
/*
* 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 TYPO3\CMS\Core\Tests\UnitTestCase as CoreTestCase;
abstract class AbstractUnitTestCase extends CoreTestCase
{
/**
* @return \TYPO3\CMS\Core\Log\LogManager
*/
protected function getMockedLogger()
{
$logger = $this->getMockBuilder(\TYPO3\CMS\Core\Log\LogManager::class)
->disableOriginalConstructor()
->setMethods(['getLogger'])
->getMock();
$logger->expects($this->once())
->method('getLogger')
->will($this->returnValue(
$this->getMockBuilder(\TYPO3\CMS\Core\Log\Logger::class)
->disableOriginalConstructor()
->getMock()
));
return $logger;
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Command;
/*
* 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\Command\IndexCommandController;
use Codappix\SearchCore\Domain\Index\IndexerFactory;
use Codappix\SearchCore\Domain\Index\NoMatchingIndexerException;
use Codappix\SearchCore\Domain\Index\TcaIndexer;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
use TYPO3\CMS\Extbase\Mvc\Controller\CommandController;
use TYPO3\CMS\Extbase\Mvc\Exception\StopActionException;
class IndexCommandControllerTest extends AbstractUnitTestCase
{
/**
* @var IndexCommandController
*/
protected $subject;
/**
* @var IndexerFactory
*/
protected $indexerFactory;
public function setUp()
{
parent::setUp();
$this->indexerFactory = $this->getMockBuilder(IndexerFactory::class)
->disableOriginalConstructor()
->getMock();
$this->subject = $this->getMockBuilder(IndexCommandController::class)
->disableOriginalConstructor()
->setMethods(['quit', 'outputLine'])
->getMock();
$this->subject->injectIndexerFactory($this->indexerFactory);
}
/**
* @test
*/
public function indexerStopsForNonAllowedTable()
{
$this->subject->expects($this->once())
->method('outputLine')
->with('No indexer found for: nonAllowedTable');
$this->indexerFactory->expects($this->once())
->method('getIndexer')
->with('nonAllowedTable')
->will($this->throwException(new NoMatchingIndexerException));
$this->subject->indexCommand('nonAllowedTable');
}
/**
* @test
*/
public function indexerExecutesForAllowedTable()
{
$indexerMock = $this->getMockBuilder(TcaIndexer::class)
->disableOriginalConstructor()
->getMock();
$this->subject->expects($this->never())
->method('quit');
$this->subject->expects($this->once())
->method('outputLine')
->with('allowedTable was indexed.');
$this->indexerFactory->expects($this->once())
->method('getIndexer')
->with('allowedTable')
->will($this->returnValue($indexerMock));
$this->subject->indexCommand('allowedTable');
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Domain\Index\TcaIndexer;
/*
* 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\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Domain\Index\TcaIndexer\TcaTableService;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class TcaTableServiceTest extends AbstractUnitTestCase
{
/**
* @var TcaTableService
*/
protected $subject;
/**
* @var ConfigurationContainerInterface
*/
protected $configuration;
public function setUp()
{
parent::setUp();
$this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock();
$this->subject = $this->getMockBuilder(TcaTableService::class)
->disableOriginalConstructor()
->setMethodsExcept(['getWhereClause', 'injectLogger', 'getTableName'])
->getMock();
$this->inject($this->subject, 'configuration', $this->configuration);
$this->inject($this->subject, 'logger', $this->getMockedLogger());
$this->inject($this->subject, 'tableName', 'table');
}
/**
* @test
*/
public function doUsePlainQueryIfNoAdditionalWhereClauseIsDefined()
{
$this->configuration->expects($this->exactly(2))
->method('getIfExists')
->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist'])
->will($this->onConsecutiveCalls(null, false));
$this->assertSame(
'1=1 AND pages.no_search = 0',
$this->subject->getWhereClause()
);
}
/**
* @test
*/
public function configuredAdditionalWhereClauseIsAdded()
{
$this->configuration->expects($this->exactly(2))
->method('getIfExists')
->withConsecutive(['indexing.table.additionalWhereClause'], ['indexing.table.rootLineBlacklist'])
->will($this->onConsecutiveCalls('table.field = "someValue"', false));
$this->assertSame(
'1=1 AND pages.no_search = 0 AND table.field = "someValue"',
$this->subject->getWhereClause()
);
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Domain\Search;
/*
* 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\Model\FacetRequest;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Domain\Search\QueryFactory;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class QueryFactoryTest extends AbstractUnitTestCase
{
/**
* @var QueryFactory
*/
protected $subject;
public function setUp()
{
parent::setUp();
$this->subject = new QueryFactory($this->getMockedLogger());
}
/**
* @test
*/
public function creatonOfQueryWorksInGeneral()
{
$searchRequest = new SearchRequest('SearchWord');
$query = $this->subject->create($searchRequest);
$this->assertInstanceOf(
\Elastica\Query::class,
$query,
'Factory did not create the expected instance.'
);
}
/**
* @test
*/
public function filterIsAddedToQuery()
{
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter(['field' => 'content']);
$query = $this->subject->create($searchRequest);
$this->assertSame(
[
['term' => ['field' => 'content']]
],
$query->toArray()['query']['bool']['filter'],
'Filter was not added to query.'
);
}
/**
* @test
*/
public function emptyFilterIsNotAddedToQuery()
{
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter([
'field' => '',
'field1' => 0,
'field2' => false,
]);
$this->assertFalse(
$searchRequest->hasFilter(),
'Search request contains filter even if it should not.'
);
$query = $this->subject->create($searchRequest);
$this->assertSame(
null,
$query->toArray()['query']['bool']['filter'],
'Filter was added to query, even if no filter exists.'
);
}
/**
* @test
*/
public function userInputIsAlwaysString()
{
$searchRequest = new SearchRequest(10);
$searchRequest->setFilter(['field' => 20]);
$query = $this->subject->create($searchRequest);
$this->assertSame(
'10',
$query->toArray()['query']['bool']['must'][0]['match']['_all'],
'Search word was not escaped as expected.'
);
$this->assertSame(
'20',
$query->toArray()['query']['bool']['filter'][0]['term']['field'],
'Search word was not escaped as expected.'
);
}
/**
* @test
*/
public function facetsAreAddedToQuery()
{
$searchRequest = new SearchRequest('SearchWord');
$searchRequest->addFacet(new FacetRequest('Identifier', 'FieldName'));
$searchRequest->addFacet(new FacetRequest('Identifier 2', 'FieldName 2'));
$query = $this->subject->create($searchRequest);
$this->assertSame(
[
'Identifier' => [
'terms' => [
'field' => 'FieldName',
],
],
'Identifier 2' => [
'terms' => [
'field' => 'FieldName 2',
],
],
],
$query->toArray()['aggs'],
'Facets were not added to query.'
);
}
}

28
Tests/Unit/UnitTests.xml Normal file
View file

@ -0,0 +1,28 @@
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../.Build/vendor/typo3/cms/typo3/sysext/core/Build/UnitTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="false"
convertWarningsToExceptions="false"
forceCoversAnnotation="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false">
<testsuites>
<testsuite name="unit-tests">
<directory>.</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../../Classes</directory>
</whitelist>
</filter>
</phpunit>

View file

@ -1,17 +1,17 @@
{
"name": "leonmrni/search_core",
"name": "codappix/search_core",
"type": "typo3-cms-extension",
"description": "Leonmrni Search Core.",
"homepage": "http://www.leonmrni.com",
"description": "Codappix Search Core.",
"homepage": "https://github.com/Codappix/search_core",
"license": ["GPL-2.0+"],
"autoload": {
"psr-4": {
"Leonmrni\\SearchCore\\": "Classes"
"Codappix\\SearchCore\\": "Classes"
}
},
"autoload-dev": {
"psr-4": {
"Leonmrni\\SearchCore\\Tests\\": "Tests/",
"Codappix\\SearchCore\\Tests\\": "Tests/",
"TYPO3\\CMS\\Core\\Tests\\": ".Build/vendor/typo3/cms/typo3/sysext/core/Tests/"
}
},

View file

@ -4,6 +4,7 @@ $EM_CONF[$_EXTKEY] = [
'title' => 'Search Core',
'description' => 'Search core for implementing various search types.',
'category' => 'be',
'clearCacheOnLoad' => 1,
'constraints' => [
'depends' => [
'typo3' => '7.6.0-7.6.99',
@ -13,12 +14,11 @@ $EM_CONF[$_EXTKEY] = [
],
'autoload' => [
'psr-4' => [
'Leonmrni\\SearchCore\\' => 'Classes',
'Codappix\\SearchCore\\' => 'Classes',
],
],
'state' => 'alpha',
'clearCacheOnLoad' => 1,
'state' => 'beta',
'version' => '1.0.0',
'author' => 'Daniel Siepmann',
'author_email' => 'coding@daniel-siepmann.de',
'version' => '1.0.0',
];

View file

@ -11,15 +11,15 @@ call_user_func(
'SC_OPTIONS' => [
'extbase' => [
'commandControllers' => [
Leonmrni\SearchCore\Command\IndexCommandController::class,
Codappix\SearchCore\Command\IndexCommandController::class,
],
],
't3lib/class.t3lib_tcemain.php' => [
'processCmdmapClass' => [
$extensionKey => '&' . \Leonmrni\SearchCore\Hook\DataHandler::class,
$extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class,
],
'processDatamapClass' => [
$extensionKey => '&' . \Leonmrni\SearchCore\Hook\DataHandler::class,
$extensionKey => '&' . \Codappix\SearchCore\Hook\DataHandler::class,
],
],
],
@ -27,7 +27,7 @@ call_user_func(
);
TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'Leonmrni.' . $extensionKey,
'Codappix.' . $extensionKey,
'search',
[
'Search' => 'search'
@ -39,8 +39,8 @@ call_user_func(
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\CMS\Extbase\Object\Container\Container')
->registerImplementation(
'Leonmrni\SearchCore\Connection\ConnectionInterface',
'Leonmrni\SearchCore\Connection\Elasticsearch'
'Codappix\SearchCore\Connection\ConnectionInterface',
'Codappix\SearchCore\Connection\Elasticsearch'
);
},
$_EXTKEY

View file

@ -7,7 +7,7 @@
);
TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin(
'Leonmrni.' . $_EXTKEY,
'Codappix.' . $_EXTKEY,
'search',
'Search Core'
);