first commit

This commit is contained in:
Sven Wappler
2021-04-17 00:26:33 +02:00
commit 866c63cc63
813 changed files with 100696 additions and 0 deletions

View File

@@ -0,0 +1,290 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacet
{
const TYPE_ABSTRACT = 'abstract';
/**
* String
* @var string
*/
protected static $type = self::TYPE_ABSTRACT;
/**
* The resultSet where this facet belongs to.
*
* @var SearchResultSet
*/
protected $resultSet = null;
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $field;
/**
* @var string
*/
protected $label;
/**
* @var array
*/
protected $configuration;
/**
* @var boolean
*/
protected $isAvailable = false;
/**
* @var bool
*/
protected $isUsed = false;
/**
* @var bool
*/
protected $allRequirementsMet = true;
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* AbstractFacet constructor.
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
$this->resultSet = $resultSet;
$this->name = $name;
$this->field = $field;
$this->label = $label;
$this->configuration = $configuration;
}
/**
* Injects the object manager
*
* @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
*/
public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get solr field name
*
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* @param string $label
*/
public function setLabel($label)
{
$this->label = $label;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @param boolean $isAvailable
*/
public function setIsAvailable($isAvailable)
{
$this->isAvailable = $isAvailable;
}
/**
* @return boolean
*/
public function getIsAvailable()
{
return $this->isAvailable;
}
/**
* @param boolean $isUsed
*/
public function setIsUsed($isUsed)
{
$this->isUsed = $isUsed;
}
/**
* @return boolean
*/
public function getIsUsed()
{
return $this->isUsed;
}
/**
* @return string
*/
public function getType()
{
return static::$type;
}
/**
* @return boolean
*/
public function getAllRequirementsMet()
{
return $this->allRequirementsMet;
}
/**
* @param boolean $allRequirementsMet
*/
public function setAllRequirementsMet($allRequirementsMet)
{
$this->allRequirementsMet = $allRequirementsMet;
}
/**
* @return SearchResultSet
*/
public function getResultSet()
{
return $this->resultSet;
}
/**
* Get configuration
*
* @return mixed
*/
public function getConfiguration()
{
return $this->configuration;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return 'Default';
}
/**
* @return string
*/
public function getGroupName()
{
return isset($this->configuration['groupName']) ? $this->configuration['groupName'] : 'main';
}
/**
* Indicates if this facet should ne included in the available facets. When nothing is configured,
* the method return TRUE.
*
* @return boolean
*/
public function getIncludeInAvailableFacets()
{
return ((int)$this->getFacetSettingOrDefaultValue('includeInAvailableFacets', 1)) === 1;
}
/**
* Indicates if this facets should be included in the used facets. When nothing is configured,
* the methods returns true.
*
* @return boolean
*/
public function getIncludeInUsedFacets()
{
return ((int)$this->getFacetSettingOrDefaultValue('includeInUsedFacets', 1)) === 1;
}
/**
* Returns the configured requirements
*
* @return array
*/
public function getRequirements()
{
return $this->getFacetSettingOrDefaultValue('requirements.', []);
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return AbstractFacetItemCollection
*/
abstract public function getAllFacetItems();
/**
* @param string $key
* @param mixed $defaultValue
* @return mixed
*/
protected function getFacetSettingOrDefaultValue($key, $defaultValue)
{
if (!isset($this->configuration[$key])) {
return $defaultValue;
}
return ($this->configuration[$key]);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Abstract item that represent a value of a facet. E.g. an option or a node
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetItem
{
/**
* @var string
*/
protected $label = '';
/**
* @var int
*/
protected $documentCount = 0;
/**
* @var bool
*/
protected $selected = false;
/**
* @var array
*/
protected $metrics = [];
/**
* @var AbstractFacet
*/
protected $facet;
/**
* @param AbstractFacet $facet
* @param string $label
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(AbstractFacet $facet, $label = '', $documentCount = 0, $selected = false, $metrics = [])
{
$this->facet = $facet;
$this->label = $label;
$this->documentCount = $documentCount;
$this->selected = $selected;
$this->metrics = $metrics;
}
/**
* @return int
*/
public function getDocumentCount()
{
return $this->documentCount;
}
/**
* @return \WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet
*/
public function getFacet()
{
return $this->facet;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @return boolean
*/
public function getSelected()
{
return $this->selected;
}
/**
* @return array
*/
public function getMetrics()
{
return $this->metrics;
}
/**
* @return string
*/
abstract public function getUriValue();
/**
* @return string
*/
abstract function getCollectionKey();
}

View File

@@ -0,0 +1,102 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetItemCollection extends AbstractCollection
{
/**
* @param AbstractFacetItem $item
* @return AbstractFacetItemCollection
*/
public function add($item)
{
if ($item === null) {
return $this;
}
$this->data[$item->getCollectionKey()] = $item;
return $this;
}
/**
* @param string $value
* @return AbstractFacetItem
*/
public function getByValue($value)
{
return isset($this->data[$value]) ? $this->data[$value] : null;
}
/**
* Retrieves the count (with get prefixed to be usable in fluid).
*
* @return int
*/
public function getCount()
{
return $this->count();
}
/**
* @return AbstractFacetItemCollection
*/
public function getSelected()
{
return $this->getFilteredCopy(function(AbstractFacetItem $item) {
return $item->getSelected();
});
}
/**
* @param array $manualSorting
* @return AbstractFacetItemCollection
*/
public function getManualSortedCopy(array $manualSorting)
{
$result = clone $this;
$copiedItems = $result->data;
$sortedOptions = [];
foreach ($manualSorting as $item) {
if (isset($copiedItems[$item])) {
$sortedOptions[$item] = $copiedItems[$item];
unset($copiedItems[$item]);
}
}
// in the end all items get appended that are not configured in the manual sort order
$sortedOptions = $sortedOptions + $copiedItems;
$result->data = $sortedOptions;
return $result;
}
/**
* @return AbstractFacetItemCollection
*/
public function getReversedOrderCopy()
{
$result = clone $this;
$result->data = array_reverse($result->data, true);
return $result;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* Class AbstractFacetPackage
*/
abstract class AbstractFacetPackage {
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param ObjectManagerInterface $objectManager
*/
public function setObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @return string
*/
abstract public function getParserClassName();
/**
* @return FacetParserInterface
* @throws InvalidFacetPackageException
*/
public function getParser()
{
$parser = $this->objectManager->get($this->getParserClassName());
if (!$parser instanceof FacetParserInterface) {
throw new InvalidFacetPackageException('Invalid parser for package ' . __CLASS__);
}
return $parser;
}
/**
* @return string
*/
public function getUrlDecoderClassName() {
return (string)DefaultUrlDecoder::class;
}
/**
* @throws InvalidUrlDecoderException
* @return FacetUrlDecoderInterface
*/
public function getUrlDecoder()
{
$urlDecoder = $this->objectManager->get($this->getUrlDecoderClassName());
if (!$urlDecoder instanceof FacetUrlDecoderInterface) {
throw new InvalidUrlDecoderException('Invalid urldecoder for package ' . __CLASS__);
}
return $urlDecoder;
}
/**
* @return string
*/
public function getQueryBuilderClassName() {
return (string)DefaultFacetQueryBuilder::class;
}
/**
* @throws InvalidQueryBuilderException
* @return FacetQueryBuilderInterface
*/
public function getQueryBuilder()
{
$urlDecoder = $this->objectManager->get($this->getQueryBuilderClassName());
if(!$urlDecoder instanceof FacetQueryBuilderInterface) {
throw new InvalidQueryBuilderException('Invalid querybuilder for package ' . __CLASS__);
}
return $urlDecoder;
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Class AbstractFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractFacetParser implements FacetParserInterface
{
/**
* @var ContentObjectRenderer
*/
protected static $reUseAbleContentObject;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param ObjectManagerInterface $objectManager
*/
public function injectObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @return ContentObjectRenderer
*/
protected function getReUseAbleContentObject()
{
/** @var $contentObject ContentObjectRenderer */
if (self::$reUseAbleContentObject !== null) {
return self::$reUseAbleContentObject;
}
self::$reUseAbleContentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
return self::$reUseAbleContentObject;
}
/**
* @param array $configuration
* @return string
*/
protected function getPlainLabelOrApplyCObject($configuration)
{
// when no label is configured we return an empty string
if (!isset($configuration['label'])) {
return '';
}
// when no sub configuration is set, we use the string, configured as label
if (!isset($configuration['label.'])) {
return $configuration['label'];
}
// when label and label. was set, we apply the cObject
return $this->getReUseAbleContentObject()->cObjGetSingle($configuration['label'], $configuration['label.']);
}
/**
* @param mixed $value
* @param integer $count
* @param string $facetName
* @param array $facetConfiguration
* @return string
*/
protected function getLabelFromRenderingInstructions($value, $count, $facetName, $facetConfiguration)
{
$hasRenderingInstructions = isset($facetConfiguration['renderingInstruction']) && isset($facetConfiguration['renderingInstruction.']);
if (!$hasRenderingInstructions) {
return $value;
}
$this->getReUseAbleContentObject()->start(['optionValue' => $value, 'optionCount' => $count, 'facetName' => $facetName]);
return $this->getReUseAbleContentObject()->cObjGetSingle(
$facetConfiguration['renderingInstruction'],
$facetConfiguration['renderingInstruction.']
);
}
/**
* Retrieves the active facetValue for a facet from the search request.
* @param SearchResultSet $resultSet
* @param string $facetName
* @return array
*/
protected function getActiveFacetValuesFromRequest(SearchResultSet $resultSet, $facetName)
{
$activeFacetValues = $resultSet->getUsedSearchRequest()->getActiveFacetValuesByName($facetName);
$activeFacetValues = is_array($activeFacetValues) ? $activeFacetValues : [];
return $activeFacetValues;
}
/**
* @param array $facetValuesFromSolrResponse
* @param array $facetValuesFromSearchRequest
* @return mixed
*/
protected function getMergedFacetValueFromSearchRequestAndSolrResponse($facetValuesFromSolrResponse, $facetValuesFromSearchRequest)
{
$facetValueItemsToCreate = $facetValuesFromSolrResponse;
foreach ($facetValuesFromSearchRequest as $valueFromRequest) {
// if we have options in the request that have not been in the response we add them with a count of 0
if (!isset($facetValueItemsToCreate[$valueFromRequest])) {
$facetValueItemsToCreate[$valueFromRequest] = 0;
}
}
return $facetValueItemsToCreate;
}
/**
* @param AbstractOptionsFacet $facet
* @param array $facetConfiguration
* @return AbstractOptionsFacet
*/
protected function applyManualSortOrder(AbstractOptionsFacet $facet, array $facetConfiguration)
{
if (!isset($facetConfiguration['manualSortOrder'])) {
return $facet;
}
$fields = GeneralUtility::trimExplode(',', $facetConfiguration['manualSortOrder']);
// @extensionScannerIgnoreLine
$sortedOptions = $facet->getOptions()->getManualSortedCopy($fields);
// @extensionScannerIgnoreLine
$facet->setOptions($sortedOptions);
return $facet;
}
/**
* @param AbstractOptionsFacet $facet
* @param array $facetConfiguration
* @return AbstractOptionsFacet
*/
protected function applyReverseOrder(AbstractOptionsFacet $facet, array $facetConfiguration)
{
if (empty($facetConfiguration['reverseOrder'])) {
return $facet;
}
// @extensionScannerIgnoreLine
$facet->setOptions($facet->getOptions()->getReversedOrderCopy());
return $facet;
}
/**
* @param mixed $value
* @param array $facetConfiguration
* @return boolean
*/
protected function getIsExcludedFacetValue($value, array $facetConfiguration)
{
if (!isset($facetConfiguration['excludeValues'])) {
return false;
}
$excludedValue = GeneralUtility::trimExplode(',', $facetConfiguration['excludeValues']);
return in_array($value, $excludedValue);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$tags = $this->buildExcludeTags($facetConfiguration, $configuration);
$facetParameters['facet.field'][] = $tags . $facetConfiguration['field'];
$sortingExpression = new SortingExpression();
$facetSortExpression = $sortingExpression->getForFacet($facetConfiguration['sortBy']);
if (!empty($facetSortExpression)) {
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.sort'] = $facetSortExpression;
}
return $facetParameters;
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return string
*/
protected function buildExcludeTags(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
// simple for now, may add overrides f.<field_name>.facet.* later
if ($configuration->getSearchFacetingKeepAllFacetsOnSelection()) {
$facets = [];
foreach ($configuration->getSearchFacetingFacets() as $facet) {
$facets[] = $facet['field'];
}
return '{!ex=' . implode(',', $facets) . '}';
} elseif ($facetConfiguration['keepAllOptionsOnSelection'] == 1) {
return '{!ex=' . $facetConfiguration['field'] . '}';
}
return '';
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class DefaultUrlDecoder implements FacetUrlDecoderInterface {
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $value the filter query from plugin
* @param array $configuration Facet configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($value, array $configuration = [])
{
return '"' . addslashes($value) . '"';
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* Class FacetCollection
*/
class FacetCollection extends AbstractCollection
{
/**
* @param AbstractFacet $facet
*/
public function addFacet(AbstractFacet $facet)
{
$this->data[$facet->getName()] = $facet;
}
/**
* @return FacetCollection
*/
public function getUsed()
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) {
return $facet->getIsUsed() && $facet->getIncludeInUsedFacets();
}
);
}
/**
* @return FacetCollection
*/
public function getAvailable()
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) {
return $facet->getIsAvailable() && $facet->getIncludeInAvailableFacets() && $facet->getAllRequirementsMet();
}
);
}
/**
* @param string $requiredGroup
* @return AbstractCollection
*/
public function getByGroupName($requiredGroup = 'all')
{
return $this->getFilteredCopy(
function(AbstractFacet $facet) use ($requiredGroup) {
return $facet->getGroupName() == $requiredGroup;
}
);
}
/**
* @param string $requiredName
* @return AbstractCollection
*/
public function getByName($requiredName) {
return $this->getFilteredCopy(
function(AbstractFacet $facet) use ($requiredName) {
return $facet->getName() == $requiredName;
}
);
}
/**
* @param int $position
* @return AbstractFacet
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Interface FacetParserInterface
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
interface FacetParserInterface
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return AbstractFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* Query Filter Encoder Interface
*
* @author Ingo Renner <ingo@typo3.org>
*/
interface FacetQueryBuilderInterface
{
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration);
}

View File

@@ -0,0 +1,108 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy\HierarchyPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options\OptionsPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup\QueryGroupPackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange\DateRangePackage;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange\NumericRangePackage;
use WapplerSystems\Meilisearch\System\Object\AbstractClassRegistry;
/**
* Class FacetRegistry
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class FacetRegistry extends AbstractClassRegistry
{
/**
* Array of available parser classNames
*
* @var array
*/
protected $classMap = [
'options' => OptionsPackage::class,
'hierarchy' => HierarchyPackage::class,
'queryGroup' => QueryGroupPackage::class,
'dateRange' => DateRangePackage::class,
'numericRange' => NumericRangePackage::class,
];
/**
* Default parser className
*
* @var string
*/
protected $defaultClass = OptionsPackage::class;
/**
* Get defaultParser
*
* @return string
*/
public function getDefaultPackage()
{
return $this->defaultClass;
}
/**
* Set defaultParser
*
* @param string $defaultPackageClassName
*/
public function setDefaultPackage($defaultPackageClassName)
{
$this->defaultClass = $defaultPackageClassName;
}
/**
* Get registered parser classNames
*
* @return array
*/
public function getPackages()
{
return $this->classMap;
}
/**
* @param string $className
* @param string $type
* @throws \InvalidArgumentException
*/
public function registerPackage($className, $type)
{
return $this->register($className, $type, AbstractFacetPackage::class);
}
/**
* Get package
*
* @param string $type
* @return AbstractFacetPackage
* @throws InvalidFacetPackageException
*/
public function getPackage($type)
{
$instance = $this->getInstance($type);
if (!$instance instanceof AbstractFacetPackage) {
throw new InvalidFacetPackageException('Invalid class registered for ' . htmlspecialchars($type));
}
$instance->setObjectManager($this->objectManager);
return $instance;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* The facet url encode is responsible to encode and decode values for EXT:meilisearch urls.
*
* @author Ingo Renner <ingo@typo3.org>
* @author Timo Hund <timo.hund@dkd.de>
*/
interface FacetUrlDecoderInterface
{
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $value the filter query from plugin
* @param array $configuration Facet configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($value, array $configuration = []);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidFacetPackageException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidFacetParserException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidQueryBuilderException extends \Exception {}

View File

@@ -0,0 +1,27 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
class InvalidUrlDecoderException extends \Exception {}

View File

@@ -0,0 +1,69 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
/**
* Base class for all facet items that are represented as option
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionFacetItem extends AbstractFacetItem
{
/**
* @var string
*/
protected $value = '';
/**
* @param AbstractFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(AbstractFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false, $metrics = [])
{
$this->value = $value;
parent::__construct($facet, $label, $documentCount, $selected, $metrics);
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return string
*/
public function getUriValue()
{
return $this->getValue();
}
/**
* @return string
*/
public function getCollectionKey()
{
return $this->getValue();
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class AbstractOptionsFacet
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionsFacet extends AbstractFacet
{
/**
* @var OptionCollection
*/
protected $options;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->options = new OptionCollection();
}
/**
* @return OptionCollection
*/
public function getOptions()
{
return $this->options;
}
/**
* @param OptionCollection $options
*/
public function setOptions($options)
{
$this->options = $options;
}
/**
* @param AbstractOptionFacetItem $option
*/
public function addOption(AbstractOptionFacetItem $option)
{
$this->options->add($option);
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return OptionCollection
*/
public function getAllFacetItems()
{
return $this->options;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Options';
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class HierarchyFacet extends AbstractFacet
{
const TYPE_HIERARCHY = 'hierarchy';
/**
* String
* @var string
*/
protected static $type = self::TYPE_HIERARCHY;
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var NodeCollection
*/
protected $allNodes;
/**
* @var array
*/
protected $nodesByKey = [];
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->childNodes = new NodeCollection();
$this->allNodes = new NodeCollection();
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* Creates a new node on the right position with the right parent node.
*
* @param string $parentKey
* @param string $key
* @param string $label
* @param string $value
* @param integer $count
* @param boolean $selected
*/
public function createNode($parentKey, $key, $label, $value, $count, $selected)
{
/** @var $parentNode Node|null */
$parentNode = isset($this->nodesByKey[$parentKey]) ? $this->nodesByKey[$parentKey] : null;
/** @var Node $node */
$node = $this->objectManager->get(Node::class, $this, $parentNode, $key, $label, $value, $count, $selected);
$this->nodesByKey[$key] = $node;
if ($parentNode === null) {
$this->addChildNode($node);
} else {
$parentNode->addChildNode($node);
}
$this->allNodes->add($node);
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Hierarchy';
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return AbstractFacetItemCollection
*/
public function getAllFacetItems()
{
return $this->allNodes;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ParsingUtil;
/**
* Class HierarchyFacetParser
*/
class HierarchyFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return HierarchyFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$optionsFromSolrResponse = isset($response->facet_counts->facet_fields->{$fieldName}) ? ParsingUtil::getMapArrayFromFlatArray($response->facet_counts->facet_fields->{$fieldName}) : [];
$optionsFromRequest = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$hasOptionsInResponse = !empty($optionsFromSolrResponse);
$hasSelectedOptionsInRequest = count($optionsFromRequest) > 0;
$hasNoOptionsToShow = !$hasOptionsInResponse && !$hasSelectedOptionsInRequest;
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($hasNoOptionsToShow && $hideEmpty) {
return null;
}
/** @var $facet HierarchyFacet */
$facet = $this->objectManager->get(HierarchyFacet::class, $resultSet, $facetName, $fieldName, $label, $facetConfiguration);
$hasActiveOptions = count($optionsFromRequest) > 0;
$facet->setIsUsed($hasActiveOptions);
$facet->setIsAvailable($hasOptionsInResponse);
$nodesToCreate = $this->getMergedFacetValueFromSearchRequestAndSolrResponse($optionsFromSolrResponse, $optionsFromRequest);
if ($this->facetOptionsMustBeResorted($facetConfiguration)) {
$nodesToCreate = $this->sortFacetOptionsInNaturalOrder($nodesToCreate);
}
foreach ($nodesToCreate as $value => $count) {
if ($this->getIsExcludedFacetValue($value, $facetConfiguration)) {
continue;
}
$isActive = in_array($value, $optionsFromRequest);
$delimiterPosition = strpos($value, '-');
$path = substr($value, $delimiterPosition + 1);
$pathArray = $this->getPathAsArray($path);
$key = array_pop($pathArray);
$parentKey = array_pop($pathArray);
$value = '/' . $path;
$label = $this->getLabelFromRenderingInstructions($key, $count, $facetName, $facetConfiguration);
$facet->createNode($parentKey, $key, $label, $value, $count, $isActive);
}
return $facet;
}
/**
* Sorts facet options in natural order.
* Options must be sorted in natural order,
* because lower nesting levels must be instantiated first, to serve as parents for higher nested levels.
* See implementation of HierarchyFacet::createNode().
*
* @param array $flatOptionsListForFacet
* @return void sorted list of facet options
*/
protected function sortFacetOptionsInNaturalOrder(array $flatOptionsListForHierarchyFacet)
{
uksort($flatOptionsListForHierarchyFacet, "strnatcmp");
return $flatOptionsListForHierarchyFacet;
}
/**
* Checks if options must be resorted.
*
* Apache Solr facet.sort can be set globally or per facet.
* Relevant TypoScript paths:
* plugin.tx_meilisearch.search.faceting.sortBy causes facet.sort Apache Solr parameter
* plugin.tx_meilisearch.search.faceting.facets.[facetName].sortBy causes f.<fieldname>.facet.sort parameter
*
* see: https://lucene.apache.org/solr/guide/6_6/faceting.html#Faceting-Thefacet.sortParameter
* see: https://wiki.apache.org/solr/SimpleFacetParameters#facet.sort : "This parameter can be specified on a per field basis."
*
* @param array $facetConfiguration
* @return bool
*/
protected function facetOptionsMustBeResorted(array $facetConfiguration)
{
if (isset($facetConfiguration['sortBy']) && $facetConfiguration['sortBy'] === 'index') {
return true;
}
return false;
}
/**
* This method is used to get the path array from a hierarchical facet. It substitutes escaped slashes to keep them
* when they are used inside a facetValue.
*
* @param string $path
* @return array
*/
protected function getPathAsArray($path)
{
$path = str_replace('\/', '@@@', $path);
$path = rtrim($path, "/");
$segments = explode('/', $path);
return array_map(function($item) {
return str_replace('@@@', '/', $item);
}, $segments);
}
/**
* Retrieves the active facetValue for a facet from the search request.
* @param SearchResultSet $resultSet
* @param string $facetName
* @return array
*/
protected function getActiveFacetValuesFromRequest(SearchResultSet $resultSet, $facetName)
{
$activeFacetValues = [];
$values = $resultSet->getUsedSearchRequest()->getActiveFacetValuesByName($facetName);
foreach (is_array($values) ? $values : [] as $valueFromRequest) {
// Attach the 'depth' param again to the value
if (strpos($valueFromRequest, '-') === false) {
$valueFromRequest = HierarchyTool::substituteSlashes($valueFromRequest);
$valueFromRequest = trim($valueFromRequest, '/');
$valueFromRequest = (count(explode('/', $valueFromRequest)) - 1) . '-' . $valueFromRequest . '/';
$valueFromRequest = HierarchyTool::unSubstituteSlashes($valueFromRequest);
}
$activeFacetValues[] = $valueFromRequest;
}
return $activeFacetValues;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class HierarchyPackage
*/
class HierarchyPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)HierarchyFacetParser::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName() {
return (string)HierarchyUrlDecoder::class;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class HierarchyTool
{
/**
* Replaces all escaped slashes in a hierarchy path with @@@slash@@@ to afterwards
* only have slashes in the content that are real path separators.
*
* @param string $pathWithContentSlashes
* @return string
*/
public static function substituteSlashes(string $pathWithContentSlashes): string
{
return (string)str_replace('\/', '@@@slash@@@', $pathWithContentSlashes);
}
/**
* Replaces @@@slash@@@ with \/ to have the path usable for solr again.
*
* @param string $pathWithReplacedContentSlashes
* @return string
*/
public static function unSubstituteSlashes(string $pathWithReplacedContentSlashes): string
{
return (string)str_replace('@@@slash@@@', '\/', $pathWithReplacedContentSlashes);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Filter encoder to build Solr hierarchy queries from tx_meilisearch[filter]
*
* @author Ingo Renner <ingo@typo3.org>
*/
class HierarchyUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Delimiter for hierarchies in the URL.
*
* @var string
*/
const DELIMITER = '/';
/**
* Parses the given hierarchy filter and returns a Solr filter query.
*
* @param string $hierarchy The hierarchy filter query.
* @param array $configuration Facet configuration
* @return string Lucene query language filter to be used for querying Solr
*/
public function decode($hierarchy, array $configuration = [])
{
$escapedHierarchy = HierarchyTool::substituteSlashes($hierarchy);
$escapedHierarchy = substr($escapedHierarchy, 1);
$escapedHierarchy = rtrim($escapedHierarchy, '/');
$hierarchyItems = explode(self::DELIMITER, $escapedHierarchy);
$filterContent = (count($hierarchyItems) - 1) . '-' . $escapedHierarchy . '/';
$filterContent = HierarchyTool::unSubstituteSlashes($filterContent);
return '"' . str_replace("\\", "\\\\", $filterContent) . '"';
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Node extends AbstractOptionFacetItem
{
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var Node
*/
protected $parentNode;
/**
* @var integer
*/
protected $depth;
/**
* @var string
*/
protected $key;
/**
* @param HierarchyFacet $facet
* @param Node $parentNode
* @param string $key
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
*/
public function __construct(HierarchyFacet $facet, $parentNode = null, $key = '', $label = '', $value = '', $documentCount = 0, $selected = false)
{
parent::__construct($facet, $label, $value, $documentCount, $selected);
$this->value = $value;
$this->childNodes = new NodeCollection();
$this->parentNode = $parentNode;
$this->key = $key;
}
/**
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* @return Node|null
*/
public function getParentNode()
{
return $this->parentNode;
}
/**
* @return bool
*/
public function getHasParentNode()
{
return $this->parentNode !== null;
}
/**
* @return bool
*/
public function getHasChildNodeSelected()
{
/** @var Node $childNode */
foreach ($this->childNodes as $childNode) {
if ($childNode->getSelected()) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NodeCollection extends AbstractFacetItemCollection
{
/**
* @param Node $node
* @return NodeCollection
*/
public function add($node)
{
return parent::add($node);
}
/**
* @param int $position
* @return Node
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options\Option;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class OptionCollection extends AbstractFacetItemCollection
{
/**
* Returns an array of prefixes from the option labels.
*
* Red, Blue, Green => r, g, b
*
* Can be used in combination with getByPrefix() to group facet options by prefix (e.g. alphabetical).
*
* @param int $length
* @return array
*/
public function getLowercaseLabelPrefixes($length = 1)
{
$prefixes = $this->getLabelPrefixes($length);
return array_map('mb_strtolower', $prefixes);
}
/**
* @param string $filteredPrefix
* @return AbstractFacetItemCollection
*/
public function getByLowercaseLabelPrefix($filteredPrefix)
{
return $this->getFilteredCopy(function(Option $option) use ($filteredPrefix)
{
$filteredPrefixLength = mb_strlen($filteredPrefix);
$currentPrefix = mb_substr(mb_strtolower($option->getLabel()), 0, $filteredPrefixLength);
return $currentPrefix === $filteredPrefix;
});
}
/**
* @param int $length
* @return array
*/
protected function getLabelPrefixes($length = 1) : array
{
$prefixes = [];
foreach ($this->data as $option) {
/** @var $option Option */
$prefix = mb_substr($option->getLabel(), 0, $length);
$prefixes[$prefix] = $prefix;
}
return array_values($prefixes);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Option extends AbstractOptionFacetItem
{
/**
* @param OptionsFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(OptionsFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false, $metrics = [])
{
parent::__construct($facet, $label, $value, $documentCount, $selected, $metrics);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class OptionsFacet extends AbstractOptionsFacet
{
const TYPE_OPTIONS = 'options';
/**
* String
* @var string
*/
protected static $type = self::TYPE_OPTIONS;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
/**
* Class OptionsFacetParser
*/
class OptionsFacetParser extends AbstractFacetParser
{
/**
* @var Dispatcher
*/
protected $dispatcher;
/**
* @param Dispatcher $dispatcher
*/
public function injectDispatcher(Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return OptionsFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$optionsFromSolrResponse = $this->getOptionsFromSolrResponse($facetName, $response);
$metricsFromSolrResponse = $this->getMetricsFromSolrResponse($facetName, $response);
$optionsFromRequest = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$hasOptionsInResponse = !empty($optionsFromSolrResponse);
$hasSelectedOptionsInRequest = count($optionsFromRequest) > 0;
$hasNoOptionsToShow = !$hasOptionsInResponse && !$hasSelectedOptionsInRequest;
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($hasNoOptionsToShow && $hideEmpty) {
return null;
}
/** @var $facet OptionsFacet */
$facet = $this->objectManager->get(
OptionsFacet::class,
$resultSet,
$facetName,
$fieldName,
$label,
$facetConfiguration
);
$hasActiveOptions = count($optionsFromRequest) > 0;
$facet->setIsUsed($hasActiveOptions);
$facet->setIsAvailable($hasOptionsInResponse);
$optionsToCreate = $this->getMergedFacetValueFromSearchRequestAndSolrResponse($optionsFromSolrResponse, $optionsFromRequest);
foreach ($optionsToCreate as $optionsValue => $count) {
if ($this->getIsExcludedFacetValue($optionsValue, $facetConfiguration)) {
continue;
}
$isOptionsActive = in_array($optionsValue, $optionsFromRequest);
$label = $this->getLabelFromRenderingInstructions($optionsValue, $count, $facetName, $facetConfiguration);
$facet->addOption($this->objectManager->get(Option::class, $facet, $label, $optionsValue, $count, $isOptionsActive, $metricsFromSolrResponse[$optionsValue]));
}
// after all options have been created we apply a manualSortOrder if configured
// the sortBy (lex,..) is done by the solr server and triggered by the query, therefore it does not
// need to be handled in the frontend.
$this->applyManualSortOrder($facet, $facetConfiguration);
$this->applyReverseOrder($facet, $facetConfiguration);
if(!is_null($this->dispatcher)) {
$this->dispatcher->dispatch(__CLASS__, 'optionsParsed', [&$facet, $facetConfiguration]);
}
return $facet;
}
/**
* @param string $facetName
* @param ResponseAdapter $response
* @return array
*/
protected function getOptionsFromSolrResponse($facetName, ResponseAdapter $response)
{
$optionsFromSolrResponse = [];
if (!isset($response->facets->{$facetName})) {
return $optionsFromSolrResponse;
}
foreach ($response->facets->{$facetName}->buckets as $bucket) {
$optionValue = $bucket->val;
$optionCount = $bucket->count;
$optionsFromSolrResponse[$optionValue] = $optionCount;
}
return $optionsFromSolrResponse;
}
/**
* @param string $facetName
* @param ResponseAdapter $response
* @return array
*/
protected function getMetricsFromSolrResponse($facetName, ResponseAdapter $response)
{
$metricsFromSolrResponse = [];
if (!isset($response->facets->{$facetName}->buckets)) {
return [];
}
foreach ($response->facets->{$facetName}->buckets as $bucket) {
$bucketVariables = get_object_vars($bucket);
foreach ($bucketVariables as $key => $value) {
if (strpos($key, 'metrics_') === 0) {
$metricsKey = str_replace('metrics_', '', $key);
$metricsFromSolrResponse[$bucket->val][$metricsKey] = $value;
}
}
}
return $metricsFromSolrResponse;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\DefaultFacetQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\SortingExpression;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* Class OptionsFacetQueryBuilder
*
* The Options facet query builder builds the facets as json structure
*
* @Todo: When we use json faceting for other facets some logic of this class can be moved to the base class.
*/
class OptionsFacetQueryBuilder extends DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$jsonFacetOptions = [
'type' => 'terms',
'field' => $facetConfiguration['field'],
];
$jsonFacetOptions['limit'] = $this->buildLimitForJson($facetConfiguration, $configuration);
$jsonFacetOptions['mincount'] = $this->buildMincountForJson($facetConfiguration, $configuration);
$sorting = $this->buildSortingForJson($facetConfiguration);
if (!empty($sorting)) {
$jsonFacetOptions['sort'] = $sorting;
}
if (is_array($facetConfiguration['metrics.'])) {
foreach ($facetConfiguration['metrics.'] as $key => $value) {
$jsonFacetOptions['facet']['metrics_' . $key] = $value;
}
}
$excludeTags = $this->buildExcludeTagsForJson($facetConfiguration, $configuration);
if (!empty($excludeTags)) {
$jsonFacetOptions['domain']['excludeTags'] = $excludeTags;
}
$facetParameters['json.facet'][$facetName] = $jsonFacetOptions;
return $facetParameters;
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return string
*/
protected function buildExcludeTagsForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
$excludeFields = [];
if ($configuration->getSearchFacetingKeepAllFacetsOnSelection()) {
if (!$configuration->getSearchFacetingCountAllFacetsForSelection()) {
// keepAllOptionsOnSelection globally active
foreach ($configuration->getSearchFacetingFacets() as $facet) {
$excludeFields[] = $facet['field'];
}
} else {
$excludeFields[] = $facetConfiguration['field'];
}
}
$isKeepAllOptionsActiveForSingleFacet = $facetConfiguration['keepAllOptionsOnSelection'] == 1;
if ($isKeepAllOptionsActiveForSingleFacet) {
$excludeFields[] = $facetConfiguration['field'];
}
if (!empty($facetConfiguration['additionalExcludeTags'])) {
$excludeFields[] = $facetConfiguration['additionalExcludeTags'];
}
return implode(',', array_unique($excludeFields));
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return int
*/
protected function buildLimitForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
if (isset($facetConfiguration['facetLimit'])) {
return (int)$facetConfiguration['facetLimit'];
} elseif (!is_null($configuration->getSearchFacetingFacetLimit()) && $configuration->getSearchFacetingFacetLimit() >= 0) {
return $configuration->getSearchFacetingFacetLimit();
} else {
return -1;
}
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return int
*/
protected function buildMincountForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
if (isset($facetConfiguration['minimumCount'])) {
return (int)$facetConfiguration['minimumCount'];
} elseif (!is_null($configuration->getSearchFacetingMinimumCount()) && (int)$configuration->getSearchFacetingMinimumCount() >= 0) {
return $configuration->getSearchFacetingMinimumCount();
} else {
return 1;
}
}
/**
* @param array $facetConfiguration
* @return string
*/
protected function buildSortingForJson(array $facetConfiguration) {
if (isset($facetConfiguration['sortBy'])) {
$sortingExpression = new SortingExpression();
$sorting = $facetConfiguration['sortBy'];
$direction = $facetConfiguration['sortDirection'];
return $sortingExpression->getForJsonFacet($sorting, $direction);
}
return '';
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class OptionsPackage
*/
class OptionsPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)OptionsFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName() {
return (string)OptionsFacetQueryBuilder::class;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a queryGroup facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Option extends AbstractOptionFacetItem
{
/**
* @param QueryGroupFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
*/
public function __construct(QueryGroupFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false)
{
parent::__construct($facet, $label, $value, $documentCount, $selected);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class QueryGroupFacet
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class QueryGroupFacet extends AbstractOptionsFacet
{
const TYPE_QUERY_GROUP = 'queryGroup';
/**
* String
* @var string
*/
protected static $type = self::TYPE_QUERY_GROUP;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
/**
* Class QueryGroupFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class QueryGroupFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return QueryGroupFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$rawOptions = $this->getRawOptions($response, $fieldName);
$noOptionsInResponse = $rawOptions === [];
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($noOptionsInResponse && $hideEmpty) {
return null;
}
/** @var QueryGroupFacet $facet */
$facet = $this->objectManager->get(
QueryGroupFacet::class,
$resultSet,
$facetName,
$fieldName,
$label,
$facetConfiguration
);
$activeFacets = $resultSet->getUsedSearchRequest()->getActiveFacetNames();
$facet->setIsUsed(in_array($facetName, $activeFacets, true));
if (!$noOptionsInResponse) {
$facet->setIsAvailable(true);
foreach ($rawOptions as $query => $count) {
$value = $this->getValueByQuery($query, $facetConfiguration);
// Skip unknown queries
if ($value === null) {
continue;
}
if ($this->getIsExcludedFacetValue($query, $facetConfiguration)) {
continue;
}
$isOptionsActive = $resultSet->getUsedSearchRequest()->getHasFacetValue($facetName, $value);
$label = $this->getLabelFromRenderingInstructions(
$value,
$count,
$facetName,
$facetConfiguration
);
$facet->addOption($this->objectManager->get(Option::class, $facet, $label, $value, $count, $isOptionsActive));
}
}
// after all options have been created we apply a manualSortOrder if configured
// the sortBy (lex,..) is done by the solr server and triggered by the query, therefore it does not
// need to be handled in the frontend.
$this->applyManualSortOrder($facet, $facetConfiguration);
$this->applyReverseOrder($facet, $facetConfiguration);
return $facet;
}
/**
* Get raw query options
*
* @param ResponseAdapter $response
* @param string $fieldName
* @return array
*/
protected function getRawOptions(ResponseAdapter $response, $fieldName)
{
$options = [];
foreach ($response->facet_counts->facet_queries as $rawValue => $count) {
if ((int)$count === 0) {
continue;
}
// todo: add test cases to check if this is needed https://forge.typo3.org/issues/45440
// remove tags from the facet.query response, for facet.field
// and facet.range Solr does that on its own automatically
$rawValue = preg_replace('/^\{!ex=[^\}]*\}(.*)/', '\\1', $rawValue);
list($field, $query) = explode(':', $rawValue, 2);
if ($field === $fieldName) {
$options[$query] = $count;
}
}
return $options;
}
/**
* @param string $query
* @param array $facetConfiguration
* @return string|null
*/
protected function getValueByQuery($query, array $facetConfiguration)
{
$value = null;
foreach ($facetConfiguration['queryGroup.'] as $valueKey => $config) {
if (isset($config['query']) && $config['query'] === $query) {
$value = rtrim($valueKey, '.');
break;
}
}
return $value;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\DefaultFacetQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class QueryGroupFacetQueryBuilder extends DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
foreach ($facetConfiguration['queryGroup.'] as $queryName => $queryConfiguration) {
$tags = $this->buildExcludeTags($facetConfiguration, $configuration);
$facetParameters['facet.query'][] = $tags . $facetConfiguration['field'] . ':' . $queryConfiguration['query'];
}
return $facetParameters;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class QueryGroupPackage
*/
class QueryGroupPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)QueryGroupFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName()
{
return (string)QueryGroupFacetQueryBuilder::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName()
{
return (string)QueryGroupUrlDecoder::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Filter encoder to build facet query parameters
*
* @author Timo Hund <timo.hund@dkd.de>
* @author Ingo Renner <ingo@typo3.org>
*/
class QueryGroupUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $filterValue the filter query from plugin
* @param array $configuration options set in a facet's configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($filterValue, array $configuration = [])
{
return $configuration[$filterValue . '.']['query'];
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
/**
* Abstract class that is used as base class for range facet items
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractRangeFacetItem extends AbstractFacetItem
{
/**
* @var array
*/
protected $rangeCounts;
/**
* @var string
*/
protected $gap;
/**
* @return string
*/
public function getUriValue()
{
return $this->getRangeString();
}
/**
* @return string
*/
public function getCollectionKey()
{
return $this->getRangeString();
}
/**
* @return array
*/
public function getRangeCounts()
{
return $this->rangeCounts;
}
/**
* @return string
*/
public function getGap()
{
return $this->gap;
}
/**
* @return string
*/
abstract protected function getRangeString();
}

View File

@@ -0,0 +1,107 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ParsingUtil;
/**
* Class AbstractRangeFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
abstract class AbstractRangeFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @param string $facetClass
* @param string $facetItemClass
* @param string $facetRangeCountClass
* @return AbstractRangeFacet|null
*/
protected function getParsedFacet(SearchResultSet $resultSet, $facetName, array $facetConfiguration, $facetClass, $facetItemClass, $facetRangeCountClass)
{
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$activeValue = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$response = $resultSet->getResponse();
$valuesFromResponse = isset($response->facet_counts->facet_ranges->{$fieldName}) ? get_object_vars($response->facet_counts->facet_ranges->{$fieldName}) : [];
$facet = $this->objectManager->get(
$facetClass,
$resultSet,
$facetName,
$fieldName,
$label,
$facetConfiguration
);
$facet->setIsAvailable(count($valuesFromResponse) > 0);
$facet->setIsUsed(count($activeValue) > 0);
if (is_array($valuesFromResponse)) {
$rangeCounts = [];
$allCount = 0;
$countsFromResponse = isset($valuesFromResponse['counts']) ? ParsingUtil::getMapArrayFromFlatArray($valuesFromResponse['counts']) : [];
foreach ($countsFromResponse as $rangeCountValue => $count) {
$rangeCountValue = $this->parseResponseValue($rangeCountValue);
$rangeCount = $this->objectManager->get($facetRangeCountClass, $rangeCountValue, $count);
$rangeCounts[] = $rangeCount;
$allCount += $count;
}
$fromInResponse = $this->parseResponseValue($valuesFromResponse['start']);
$toInResponse = $this->parseResponseValue($valuesFromResponse['end']);
if (preg_match('/(-?\d*?)-(-?\d*)/', $activeValue[0], $rawValues) == 1) {
$rawFrom = $rawValues[1];
$rawTo = $rawValues[2];
} else {
$rawFrom = 0;
$rawTo = 0;
}
$from = $this->parseRequestValue($rawFrom);
$to = $this->parseRequestValue($rawTo);
$type = isset($facetConfiguration['type']) ? $facetConfiguration['type'] : 'numericRange';
$gap = isset($facetConfiguration[$type . '.']['gap']) ? $facetConfiguration[$type . '.']['gap'] : 1;
$range = $this->objectManager->get($facetItemClass, $facet, $from, $to, $fromInResponse, $toInResponse, $gap, $allCount, $rangeCounts, true);
$facet->setRange($range);
}
return $facet;
}
/**
* @param string $requestValue
* @return mixed
*/
abstract protected function parseRequestValue($requestValue);
/**
* @param string $responseValue
* @return mixed
*/
abstract protected function parseResponseValue($responseValue);
}

View File

@@ -0,0 +1,124 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\AbstractRangeFacetItem;
use DateTime;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class DateRange extends AbstractRangeFacetItem
{
/**
* @var DateTime
*/
protected $startRequested;
/**
* @var DateTime
*/
protected $endRequested;
/**
* @var DateTime
*/
protected $startInResponse;
/**
* @var DateTime
*/
protected $endInResponse;
/**
* @param DateRangeFacet $facet
* @param DateTime|null $startRequested
* @param DateTime|null $endRequested
* @param DateTime|null $startInResponse
* @param DateTime|null $endInResponse
* @param string $gap
* @param int $documentCount
* @param array $rangeCounts
* @param bool $selected
*/
public function __construct(DateRangeFacet $facet, DateTime $startRequested = null, DateTime $endRequested = null, DateTime $startInResponse = null, DateTime $endInResponse = null, $gap = '', $documentCount = 0, $rangeCounts, $selected = false)
{
$this->startInResponse = $startInResponse;
$this->endInResponse = $endInResponse;
$this->startRequested = $startRequested;
$this->endRequested = $endRequested;
$this->rangeCounts = $rangeCounts;
$this->gap = $gap;
$label = '';
if ($startRequested instanceof DateTime && $endRequested instanceof DateTime) {
$label = $this->getRangeString();
}
parent::__construct($facet, $label, $documentCount, $selected);
}
/**
* @return string
*/
protected function getRangeString()
{
return $this->startRequested->format('Ymd') . '0000-' . $this->endRequested->format('Ymd') . '0000';
}
/**
* Retrieves the end date that was requested by the user for this facet.
*
* @return \DateTime
*/
public function getEndRequested()
{
return $this->endRequested;
}
/**
* Retrieves the start date that was requested by the used for the facet.
*
* @return \DateTime
*/
public function getStartRequested()
{
return $this->startRequested;
}
/**
* Retrieves the end date that was received from solr for this facet.
*
* @return \DateTime
*/
public function getEndInResponse()
{
return $this->endInResponse;
}
/**
* Retrieves the start date that was received from solr for this facet.
*
* @return \DateTime
*/
public function getStartInResponse()
{
return $this->startInResponse;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class DateRangeCollection extends AbstractFacetItemCollection
{
/**
* @param DateRange $dateRange
* @return DateRangeCollection
*/
public function add($dateRange)
{
return parent::add($dateRange);
}
/**
* @param int $position
* @return DateRange
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
use DateTime;
/**
* Value object that represent an date range count. The count has a date and the count of documents
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class DateRangeCount
{
/**
* @var DateTime
*/
protected $date;
/**
* @var int
*/
protected $documentCount = 0;
/**
* @param $date
* @param $documentCount
*/
public function __construct($date, $documentCount)
{
$this->date = $date;
$this->documentCount = $documentCount;
}
/**
* @return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* @return int
*/
public function getDocumentCount()
{
return $this->documentCount;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a date range facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class DateRangeFacet extends AbstractFacet
{
const TYPE_DATE_RANGE = 'dateRange';
/**
* String
* @var string
*/
protected static $type = self::TYPE_DATE_RANGE;
/**
* @var DateRange
*/
protected $range;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
/**
* @param DateRange $range
*/
public function setRange(DateRange $range)
{
$this->range = $range;
}
/**
* @return DateRange
*/
public function getRange()
{
return $this->range;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'RangeDate.html';
}
/**
* Since the DateRange contains only one or two items when return a collection with the range only to
* allow to render the date range as other facet items.
*
* @return AbstractFacetItemCollection
*/
public function getAllFacetItems()
{
return new DateRangeCollection([$this->range]);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\AbstractRangeFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Data\DateTime;
/**
* Class DateRangeFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class DateRangeFacetParser extends AbstractRangeFacetParser
{
/**
* @var string
*/
protected $facetClass = DateRangeFacet::class;
/**
* @var string
*/
protected $facetItemClass = DateRange::class;
/**
* @var string
*/
protected $facetRangeCountClass = DateRangeCount::class;
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return DateRangeFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
return $this->getParsedFacet(
$resultSet,
$facetName,
$facetConfiguration,
$this->facetClass,
$this->facetItemClass,
$this->facetRangeCountClass
);
}
/**
* @param string $rawDate
* @return DateTime|null
*/
protected function parseRequestValue($rawDate)
{
$rawDate = \DateTime::createFromFormat('Ymd', substr($rawDate, 0, 8));
if ($rawDate === false) {
return null;
}
$date = new DateTime($rawDate->format(DateTime::ISO8601));
return $date;
}
/**
* @param string $isoDateString
* @return DateTime
*/
protected function parseResponseValue($isoDateString)
{
$rawDate = \DateTime::createFromFormat(\DateTime::ISO8601, $isoDateString);
return new DateTime($rawDate->format(DateTime::ISO8601));
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class DateRangeFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$tag = '';
if ($facetConfiguration['keepAllOptionsOnSelection'] == 1) {
$tag = '{!ex=' . $facetConfiguration['field'] . '}';
}
$facetParameters['facet.range'][] = $tag . $facetConfiguration['field'];
$start = 'NOW/DAY-1YEAR';
if ($facetConfiguration['dateRange.']['start']) {
$start = $facetConfiguration['dateRange.']['start'];
}
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.start'] = $start;
$end = 'NOW/DAY+1YEAR';
if ($facetConfiguration['dateRange.']['end']) {
$end = $facetConfiguration['dateRange.']['end'];
}
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.end'] = $end;
$gap = '+1DAY';
if ($facetConfiguration['dateRange.']['gap']) {
$gap = $facetConfiguration['dateRange.']['gap'];
}
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.gap'] = $gap;
return $facetParameters;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class DateRangePackage
*/
class DateRangePackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)DateRangeFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName()
{
return (string)DateRangeFacetQueryBuilder::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName()
{
return (string)DateRangeUrlDecoder::class;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\DateRange;
/***************************************************************
* Copyright notice
*
* (c) 2010-2011 Markus Goldbach <markus.goldbach@dkd.de>
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
use WapplerSystems\Meilisearch\System\DateTime\FormatService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Parser to build solr range queries from tx_meilisearch[filter]
*
* @author Markus Goldbach <markus.goldbach@dkd.de>
*/
class DateRangeUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Delimiter for date parts in the URL.
*
* @var string
*/
const DELIMITER = '-';
/**
* Parses the given date range from a GET parameter and returns a Solr
* date range filter.
*
* @param string $dateRange The range filter query string from the query URL
* @param array $configuration Facet configuration
* @return string Lucene query language filter to be used for querying Solr
*/
public function decode($dateRange, array $configuration = [])
{
list($dateRangeStart, $dateRangeEnd) = explode(self::DELIMITER, $dateRange);
$formatService = GeneralUtility::makeInstance(FormatService::class);
$fromPart = '*';
if($dateRangeStart !== ''){
$fromPart = $formatService->timestampToIso(strtotime($dateRangeStart));
}
$toPart = '*';
if($dateRangeEnd !== ''){
$dateRangeEnd .= '59'; // adding 59 seconds
$toPart = $formatService->timestampToIso(strtotime($dateRangeEnd));
}
$dateRangeFilter = '[' . $fromPart . ' TO ' . $toPart . ']';
return $dateRangeFilter;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\AbstractRangeFacetItem;
/**
* Value object that represent an option of a numric range facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NumericRange extends AbstractRangeFacetItem
{
/**
* @var float
*/
protected $startRequested;
/**
* @var float
*/
protected $endRequested;
/**
* @var float
*/
protected $startInResponse;
/**
* @var float
*/
protected $endInResponse;
/**
* @param NumericRangeFacet $facet
* @param float|null $startRequested
* @param float|null $endRequested
* @param float|null $startInResponse
* @param float|null $endInResponse
* @param string $gap
* @param int $documentCount
* @param array $rangeCounts
* @param bool $selected
*/
public function __construct(NumericRangeFacet $facet, $startRequested = null, $endRequested = null, $startInResponse = null, $endInResponse = null, $gap = '', $documentCount = 0, $rangeCounts, $selected = false)
{
$this->startInResponse = $startInResponse;
$this->endInResponse = $endInResponse;
$this->startRequested = $startRequested;
$this->endRequested = $endRequested;
$this->rangeCounts = $rangeCounts;
$this->gap = $gap;
$label = '';
if ($startRequested !== null && $endRequested !== null) {
$label = $this->getRangeString();
}
parent::__construct($facet, $label, $documentCount, $selected);
}
/**
* @return string
*/
protected function getRangeString()
{
return $this->startRequested . '-' . $this->endRequested;
}
/**
* Retrieves the end date that was requested by the user for this facet.
*
* @return float
*/
public function getEndRequested()
{
return $this->endRequested;
}
/**
* Retrieves the start date that was requested by the used for the facet.
*
* @return float
*/
public function getStartRequested()
{
return $this->startRequested;
}
/**
* Retrieves the end date that was received from solr for this facet.
*
* @return float
*/
public function getEndInResponse()
{
return $this->endInResponse;
}
/**
* Retrieves the start date that was received from solr for this facet.
*
* @return float
*/
public function getStartInResponse()
{
return $this->startInResponse;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NumericRangeCollection extends AbstractFacetItemCollection
{
/**
* @param NumericRange $numericRange
* @return NumericRangeCollection
*/
public function add($numericRange)
{
return parent::add($numericRange);
}
/**
* @param int $position
* @return NumericRange
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItem;
use DateTime;
/**
* Value object that represent an date range count. The count has a date and the count of documents
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NumericRangeCount
{
/**
* @var float
*/
protected $rangeItem;
/**
* @var int
*/
protected $documentCount = 0;
/**
* @param float $rangeItem
* @param integer $documentCount
*/
public function __construct($rangeItem, $documentCount)
{
$this->rangeItem = $rangeItem;
$this->documentCount = $documentCount;
}
/**
* @return float
*/
public function getRangeItem()
{
return $this->rangeItem;
}
/**
* @return int
*/
public function getDocumentCount()
{
return $this->documentCount;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a date range facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NumericRangeFacet extends AbstractFacet
{
const TYPE_NUMERIC_RANGE = 'numericRange';
/**
* String
* @var string
*/
protected static $type = self::TYPE_NUMERIC_RANGE;
/**
* @var NumericRange
*/
protected $numericRange;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
/**
* @param NumericRange $range
*/
public function setRange(NumericRange $range)
{
$this->numericRange = $range;
}
/**
* @return NumericRange
*/
public function getRange()
{
return $this->numericRange;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'RangeNumeric.html';
}
/**
* Since the DateRange contains only one or two items when return a collection with the range only to
* allow to render the date range as other facet items.
*
* @return AbstractFacetItemCollection
*/
public function getAllFacetItems()
{
return new NumericRangeCollection([$this->numericRange]);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\AbstractRangeFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class NumericRangeFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NumericRangeFacetParser extends AbstractRangeFacetParser
{
/**
* @var string
*/
protected $facetClass = NumericRangeFacet::class;
/**
* @var string
*/
protected $facetItemClass = NumericRange::class;
/**
* @var string
*/
protected $facetRangeCountClass = NumericRangeCount::class;
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return NumericRangeFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
return $this->getParsedFacet(
$resultSet,
$facetName,
$facetConfiguration,
$this->facetClass,
$this->facetItemClass,
$this->facetRangeCountClass
);
}
/**
* @param mixed $rawValue
* @return mixed (numeric value)
*/
protected function parseRequestValue($rawValue)
{
return is_numeric($rawValue) ? $rawValue : 0;
}
/**
* @param $rawValue
* @return mixed (numeric value)
*/
protected function parseResponseValue($rawValue)
{
return is_numeric($rawValue) ? $rawValue : 0;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class NumericRangeFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$tag = '';
if ($facetConfiguration['keepAllOptionsOnSelection'] == 1) {
$tag = '{!ex=' . $facetConfiguration['field'] . '}';
}
$facetParameters['facet.range'][] = $tag . $facetConfiguration['field'];
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.start'] = $facetConfiguration['numericRange.']['start'];
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.end'] = $facetConfiguration['numericRange.']['end'];
$facetParameters['f.' . $facetConfiguration['field'] . '.facet.range.gap'] = $facetConfiguration['numericRange.']['gap'];
return $facetParameters;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacetPackage;
/**
* Class NumericRangePackage
*/
class NumericRangePackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)NumericRangeFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName()
{
return (string)NumericRangeFacetQueryBuilder::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName()
{
return (string)NumericRangeUrlDecoder::class;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RangeBased\NumericRange;
/***************************************************************
* Copyright notice
*
* (c) 2010-2011 Markus Goldbach <markus.goldbach@dkd.de>
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* (c) 2016 Markus Friedrich <markus.friedrich@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Parser to build Solr range queries from tx_meilisearch[filter]
*
* @author Markus Goldbach <markus.goldbach@dkd.de>
* @author Ingo Renner <ingo@typo3.org>
* @author Markus Friedrich <markus.friedrich@dkd.de>
*/
class NumericRangeUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Delimiter for ranges in the URL.
*
* @var string
*/
const DELIMITER = '-';
/**
* Parses the given range from a GET parameter and returns a Solr range
* filter.
*
* @param string $range The range filter from the URL.
* @param array $configuration Facet configuration
* @return string Lucene query language filter to be used for querying Solr
* @throws \InvalidArgumentException
*/
public function decode($range, array $configuration = [])
{
preg_match('/(-?\d*?)' . self::DELIMITER . '(-?\d*)/', $range, $filterParts);
if ($filterParts[1] == '' || $filterParts[2] == '') {
throw new \InvalidArgumentException(
'Invalid numeric range given',
1466062730
);
}
return '[' . (int)$filterParts[1] . ' TO ' . (int)$filterParts[2] . ']';
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RenderingInstructions;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Hendrik Putzek <hendrik.putzek@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\DateTime\FormatService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Formats a given date string to another format
*
* Accepted typoscript parameters:
* inputFormat -> The format of the input string
* outputFormat -> The format of the processed string (output)
*
* If the input date can not be processed by the given inputFormat string it is
* returned unprocessed.
*
* @author Hendrik Putzek <hendrik.putzek@dkd.de>
*/
class FormatDate
{
/**
* @var FormatService
*/
protected $formatService = null;
/**
* FormatDate constructor.
* @param FormatService|null $formatService
*/
public function __construct(FormatService $formatService = null)
{
$this->formatService = $formatService ?? GeneralUtility::makeInstance(FormatService::class);
}
/**
* Formats a given date string to another format
*
* @param string $content the content to process
* @param array $conf typoscript configuration
* @return string formatted date
*/
public function format($content, $conf)
{
// set default values
$inputFormat = $conf['inputFormat'] ?: 'Y-m-d\TH:i:s\Z';
$outputFormat = $conf['outputFormat'] ?: '';
return $this->formatService->format($content, $inputFormat, $outputFormat);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Service class to check for a facet if allRequirements are met for that facet.
*
* @author Timo Hund <timo.hund@dkd.de>
*/
class RequirementsService
{
/**
* Checks if facet meets all requirements.
*
* Evaluates configuration in "plugin.tx_meilisearch.search.faceting.facets.[facetName].requirements",
*
* @param AbstractFacet $facet
* @return bool true if facet might be rendered
*/
public function getAllRequirementsMet(AbstractFacet $facet)
{
$requirements = $facet->getRequirements();
if (!is_array($requirements) || count($requirements) === 0) {
return true;
}
foreach ($requirements as $requirement) {
$requirementMet = $this->getRequirementMet($facet, $requirement);
$requirementMet = $this->getNegationWhenConfigured($requirementMet, $requirement);
if (!$requirementMet) {
// early return as soon as one requirement is not met
return false;
}
}
return true;
}
/**
* Checks if a single requirement is met.
*
* @param AbstractFacet $facet
* @param array $requirement
* @return bool
*/
protected function getRequirementMet(AbstractFacet $facet, $requirement = []) {
$selectedItemValues = $this->getSelectedItemValues($facet, $requirement['facet']);
$csvActiveFacetItemValues = implode(', ', $selectedItemValues);
$requirementValues = GeneralUtility::trimExplode(',', $requirement['values']);
foreach ($requirementValues as $value) {
$noFacetOptionSelectedRequirementMet = ($value === '__none' && empty($selectedItemValues));
$anyFacetOptionSelectedRequirementMet = ($value === '__any' && !empty($selectedItemValues));
if ($noFacetOptionSelectedRequirementMet || $anyFacetOptionSelectedRequirementMet || in_array($value, $selectedItemValues) || fnmatch($value, $csvActiveFacetItemValues)) {
// when we find a single matching requirement we can exit and return true
return true;
}
}
return false;
}
/**
* Returns the active item values of a facet
*
* @param string $facetNameToCheckRequirementsOn
* @return AbstractFacetItem[]
*/
protected function getSelectedItemValues(AbstractFacet $facet, $facetNameToCheckRequirementsOn)
{
$facetToCheckRequirements = $facet->getResultSet()->getFacets()->getByName($facetNameToCheckRequirementsOn)->getByPosition(0);
if (!$facetToCheckRequirements instanceof AbstractFacet) {
throw new \InvalidArgumentException('Requirement for unexisting facet configured');
}
if (!$facetToCheckRequirements->getIsUsed()) {
// unused facets do not have active values.
return [];
}
$itemValues = [];
$activeFacetItems = $facetToCheckRequirements->getAllFacetItems();
foreach ($activeFacetItems as $item) {
/** @var AbstractFacetItem $item */
if ($item->getSelected()) {
$itemValues[] = $item->getUriValue();
}
}
return $itemValues;
}
/**
* Negates the result when configured.
*
* @param boolean $value
* @param array $configuration
* @return boolean
*/
protected function getNegationWhenConfigured($value, $configuration)
{
if (!is_array($configuration) || empty($configuration['negate']) || (bool)$configuration['negate'] === false) {
return $value;
}
return !((bool)$value);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets;
/***************************************************************
* Copyright notice
*
* (c) 2017- Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* Expression for facet sorting
*
* @author Timo Hund <timo.hund@dkd.de>
* @author Jens Jacobsen <jens.jacobsen@ueberbit.de>
*/
class SortingExpression
{
/**
* Return expression for facet sorting
*
* @param string $sorting
* @return string
*/
public function getForFacet($sorting)
{
$noSortingSet = $sorting !== 0 && $sorting !== FALSE && empty($sorting);
$sortingIsCount = $sorting === 'count' || $sorting === 1 || $sorting === '1' || $sorting === TRUE;
if ($noSortingSet) {
return '';
} elseif ($sortingIsCount) {
return 'count';
} else {
return 'index';
}
}
/**
* Return expression for facet sorting combined with direction
*
* @param string $sorting
* @param string $direction
* @return string
*/
public function getForJsonFacet($sorting, $direction)
{
$isMetricSorting = strpos($sorting, 'metrics_') === 0;
$expression = $isMetricSorting ? $sorting : $this->getForFacet($sorting);
$direction = strtolower($direction);
if (!empty($direction) && in_array($direction, ['asc', 'desc'])) {
$expression .= ' ' . $direction;
}
return $expression;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* A group is identified by a groupName and can contain multiple groupItems (that reference the search results).
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Group
{
/**
* @var string
*/
protected $groupName = '';
/**
* @var int
*/
protected $resultsPerPage = 10;
/**
* @var GroupItemCollection
*/
protected $groupItems = null;
/**
* @var array
*/
protected $groupConfiguration = [];
/**
* Group constructor.
* @param string $groupName
* @param int $resultsPerPage
*/
public function __construct(string $groupName, int $resultsPerPage = 10)
{
$this->groupName = $groupName;
$this->groupItems = new GroupItemCollection();
$this->resultsPerPage = $resultsPerPage;
}
/**
* @return string
*/
public function getGroupName(): string
{
return $this->groupName;
}
/**
* @param string $groupName
*/
public function setGroupName(string $groupName)
{
$this->groupName = $groupName;
}
/**
* @return GroupItemCollection
*/
public function getGroupItems(): GroupItemCollection
{
return $this->groupItems;
}
/**
* @param GroupItemCollection $groupItems
*/
public function setGroupItems(GroupItemCollection $groupItems)
{
$this->groupItems = $groupItems;
}
/**
* @param GroupItem $groupItem
*/
public function addGroupItem(GroupItem $groupItem)
{
$this->groupItems[] = $groupItem;
}
/**
* @return int
*/
public function getResultsPerPage(): int
{
return $this->resultsPerPage;
}
/**
* @param int $resultsPerPage
*/
public function setResultsPerPage(int $resultsPerPage)
{
$this->resultsPerPage = $resultsPerPage;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* The Group contains the Group objects.
*/
class GroupCollection extends AbstractCollection {
/**
* @param string $name
* @return Group|null
*/
public function getByName($name)
{
foreach ($this->data as $group) {
/** @var $group Group */
if ($group->getGroupName() === $name) {
return $group;
}
}
return null;
}
/**
* @param string $name
* @return boolean
*/
public function getHasWithName($name): bool
{
foreach ($this->data as $group) {
/** @var $group Group */
if ($group->getGroupName() === $name) {
return true;
}
}
return false;
}
/**
* @return array
*/
public function getGroupNames(): array
{
$names = [];
foreach ($this->data as $group) {
/** @var $group Group */
$names[] = $group->getGroupName();
}
return $names;
}
/**
* @param Group $group
*/
public function add(Group $group)
{
$this->data[] = $group;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResult;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection;
/**
* Class GroupItem
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class GroupItem
{
/**
* @var string
*/
protected $groupValue = '';
/**
* @var int
*/
protected $allResultCount = 0;
/**
* @var int
*/
protected $start = 0;
/**
* @var float
*/
protected $maximumScore = 0;
/**
* @var SearchResultCollection
*/
protected $searchResults;
/**
* @var Group
*/
protected $group;
/**
* @param Group $group
* @param string $groupValue
* @param int $numFound
* @param int $start
* @param float $maxScore
*/
public function __construct(Group $group, $groupValue, $numFound, $start, $maxScore)
{
$this->group = $group;
$this->groupValue = $groupValue;
$this->allResultCount = $numFound;
$this->start = $start;
$this->maximumScore = $maxScore;
$this->searchResults = new SearchResultCollection();
}
/**
* Get groupValue
*
* @return string
*/
public function getGroupValue()
{
return $this->groupValue;
}
/**
* Get numFound
*
* @return int
*/
public function getAllResultCount()
{
return $this->allResultCount;
}
/**
* Get start
*
* @return int
*/
public function getStart()
{
return $this->start;
}
/**
* Get maxScore
*
* @return float
*/
public function getMaximumScore()
{
return $this->maximumScore;
}
/**
* @return SearchResultCollection
*/
public function getSearchResults(): SearchResultCollection
{
return $this->searchResults;
}
/**
* @param SearchResultCollection $searchResults
*/
public function setSearchResults(SearchResultCollection $searchResults)
{
$this->searchResults = $searchResults;
}
/**
* @param SearchResult $searchResult
*/
public function addSearchResult(SearchResult $searchResult)
{
$this->searchResults[] = $searchResult;
}
/**
* @return Group
*/
public function getGroup(): Group
{
return $this->group;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* The GroupItemCollection contains the GroupItem objects.
*/
class GroupItemCollection extends AbstractCollection {}

View File

@@ -0,0 +1,72 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser;
/***************************************************************
* Copyright notice
*
* (c) 2015-2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A ResultParser is responsible to create the result object structure from the \Apache_Solr_Response
* and assign it to the SearchResultSet.
*/
abstract class AbstractResultParser {
/**
* @var SearchResultBuilder
*/
protected $searchResultBuilder;
/**
* @var DocumentEscapeService
*/
protected $documentEscapeService;
/**
* AbstractResultParser constructor.
* @param SearchResultBuilder|null $resultBuilder
* @param DocumentEscapeService|null $documentEscapeService
*/
public function __construct(SearchResultBuilder $resultBuilder = null, DocumentEscapeService $documentEscapeService = null) {
$this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
$this->documentEscapeService = $documentEscapeService ?? GeneralUtility::makeInstance(DocumentEscapeService::class);
}
/**
* @param SearchResultSet $resultSet
* @param bool $useRawDocuments
* @return SearchResultSet
*/
abstract public function parse(SearchResultSet $resultSet, bool $useRawDocuments = true);
/**
* @param SearchResultSet $resultSet
* @return mixed
*/
abstract public function canParse(SearchResultSet $resultSet);
}

View File

@@ -0,0 +1,88 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser;
/***************************************************************
* Copyright notice
*
* (c) 2015-2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The DefaultResultParser is able to parse normal(ungroupd results)
*/
class DefaultResultParser extends AbstractResultParser {
/**
* @param SearchResultSet $resultSet
* @param bool $useRawDocuments
* @return SearchResultSet
*/
public function parse(SearchResultSet $resultSet, bool $useRawDocuments = true)
{
$searchResults = GeneralUtility::makeInstance(SearchResultCollection::class);
$parsedData = $resultSet->getResponse()->getParsedData();
// @extensionScannerIgnoreLine
$resultSet->setMaximumScore($parsedData->response->maxScore ?? 0.0);
// @extensionScannerIgnoreLine
$resultSet->setAllResultCount($parsedData->response->numFound ?? 0);
// @extensionScannerIgnoreLine
if (!is_array($parsedData->response->docs)) {
return $resultSet;
}
// @extensionScannerIgnoreLine
$documents = $parsedData->response->docs;
if (!$useRawDocuments) {
$documents = $this->documentEscapeService->applyHtmlSpecialCharsOnAllFields($documents);
}
foreach ($documents as $searchResult) {
$searchResultObject = $this->searchResultBuilder->fromApacheSolrDocument($searchResult);
$searchResults[] = $searchResultObject;
}
$resultSet->setSearchResults($searchResults);
return $resultSet;
}
/**
* @param SearchResultSet $resultSet
* @return bool
*/
public function canParse(SearchResultSet $resultSet)
{
// This parsers should not be used when grouping is enabled
$configuration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration();
if ($configuration instanceof TypoScriptConfiguration && $configuration->getSearchGrouping())
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser;
/***************************************************************
* Copyright notice
*
* (c) 2015-2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use WapplerSystems\Meilisearch\Util;
/**
* Applies htmlspecialschars on documents of a solr response.
*/
class DocumentEscapeService {
/**
* @var TypoScriptConfiguration
*/
protected $typoScriptConfiguration = null;
/**
* DocumentEscapeService constructor.
* @param TypoScriptConfiguration|null $typoScriptConfiguration
*/
public function __construct(TypoScriptConfiguration $typoScriptConfiguration = null) {
$this->typoScriptConfiguration = $typoScriptConfiguration ?? Util::getSolrConfiguration();
}
/**
* This method is used to apply htmlspecialchars on all document fields that
* are not configured to be secure. Secure mean that we know where the content is coming from.
*
* @param Document[] $documents
* @return Document[]
*/
public function applyHtmlSpecialCharsOnAllFields(array $documents)
{
$trustedSolrFields = $this->typoScriptConfiguration->getSearchTrustedFieldsArray();
foreach ($documents as $key => $document) {
$fieldNames = array_keys($document->getFields() ?? []);
foreach ($fieldNames as $fieldName) {
if (is_array($trustedSolrFields) && in_array($fieldName, $trustedSolrFields)) {
// we skip this field, since it was marked as secure
continue;
}
$value = $this->applyHtmlSpecialCharsOnSingleFieldValue($document[$fieldName]);
$document->setField($fieldName, $value);
}
$documents[$key] = $document;
}
return $documents;
}
/**
* Applies htmlspecialchars on all items of an array of a single value.
*
* @param $fieldValue
* @return array|string
*/
protected function applyHtmlSpecialCharsOnSingleFieldValue($fieldValue)
{
if (is_array($fieldValue)) {
foreach ($fieldValue as $key => $fieldValueItem) {
$fieldValue[$key] = htmlspecialchars($fieldValueItem, null, null, false);
}
} else {
$fieldValue = htmlspecialchars($fieldValue, null, null, false);
}
return $fieldValue;
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class ResultParserRegistry
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class ResultParserRegistry implements SingletonInterface
{
/**
* Array of available parser classNames
*
* @var array
*/
protected $parsers = [
100 => DefaultResultParser::class,
];
/**
* @var AbstractResultParser[]
*/
protected $parserInstances;
/**
* Get registered parser classNames
*
* @return array
*/
public function getParsers()
{
return $this->parsers;
}
/**
* Can be used to register a custom parser.
*
* @param string $className classname of the parser that should be used
* @param int $priority higher priority means more important
* @throws \InvalidArgumentException
*/
public function registerParser($className, $priority)
{
// check if the class is available for TYPO3 before registering the driver
if (!class_exists($className)) {
throw new \InvalidArgumentException('Class ' . $className . ' does not exist.', 1468863997);
}
if (!is_subclass_of($className, AbstractResultParser::class)) {
throw new \InvalidArgumentException('Parser ' . $className . ' needs to implement the AbstractResultParser.', 1468863998);
}
if (array_key_exists((int)$priority, $this->parsers)) {
throw new \InvalidArgumentException('There is already a parser registerd with priority ' . (int)$priority . '.', 1468863999);
}
$this->parsers[(int)$priority] = $className;
}
/**
* Method to check if a certain parser is allready registered
*
* @param string $className
* @param int $priority
* @return boolean
*/
public function hasParser($className, $priority)
{
if (empty($this->parsers[$priority])) {
return false;
}
return $this->parsers[$priority] === $className;
}
/**
* @return AbstractResultParser[]
*/
public function getParserInstances()
{
if ($this->parserInstances === null) {
ksort($this->parsers);
$orderedParsers = array_reverse($this->parsers);
foreach ($orderedParsers as $className) {
$this->parserInstances[] = $this->createParserInstance($className);
}
}
return $this->parserInstances;
}
/**
* @param SearchResultSet $resultSet
* @return AbstractResultParser|null
*/
public function getParser(SearchResultSet $resultSet)
{
/** @var AbstractResultParser $parser */
foreach ($this->getParserInstances() as $parser) {
if ($parser->canParse($resultSet)) {
return $parser;
}
}
return null;
}
/**
* Create an instance of a certain parser class
*
* @return AbstractResultParser
*/
protected function createParserInstance($className)
{
return GeneralUtility::makeInstance($className);
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping\GroupItem;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
/**
* Solr document class that should be used in the frontend in the search context.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
class SearchResult extends Document
{
/**
* The variant field value
*
* Value of Solr collapse field, which is defined via
* TypoScript variable "variants.variantField"
*
* @var string
*/
protected $variantFieldValue = '';
/**
* Number of variants found
*
* May differ from documents in variants as
* returned variants are limited by expand.rows
*
* @var int
*/
protected $variantsNumFound = 0;
/**
* @var SearchResult[]
*/
protected $variants = [];
/**
* Indicates if an instance of this document is a variant (a sub document of another).
*
* @var bool
*/
protected $isVariant = false;
/**
* References the parent document of the document is a variant.
*
* @var SearchResult|null
*/
protected $variantParent = null;
/**
* @var GroupItem
*/
protected $groupItem = null;
/**
* @return GroupItem
*/
public function getGroupItem(): GroupItem
{
return $this->groupItem;
}
/**
* @return bool
*/
public function getHasGroupItem()
{
return $this->groupItem !== null;
}
/**
* @param GroupItem $group
*/
public function setGroupItem(GroupItem $group)
{
$this->groupItem = $group;
}
/**
* @return string
*/
public function getVariantFieldValue(): string
{
return $this->variantFieldValue;
}
/**
* @param string $variantFieldValue
*/
public function setVariantFieldValue(string $variantFieldValue)
{
$this->variantFieldValue = $variantFieldValue;
}
/**
* @return int
*/
public function getVariantsNumFound(): int
{
return $this->variantsNumFound;
}
/**
* @param int $numFound
*/
public function setVariantsNumFound(int $numFound)
{
$this->variantsNumFound = $numFound;
}
/**
* @return SearchResult[]
*/
public function getVariants()
{
return $this->variants;
}
/**
* @param SearchResult $expandedResult
*/
public function addVariant(SearchResult $expandedResult)
{
$this->variants[] = $expandedResult;
}
/**
* @return bool
*/
public function getIsVariant()
{
return $this->isVariant;
}
/**
* @param bool $isVariant
*/
public function setIsVariant($isVariant)
{
$this->isVariant = $isVariant;
}
/**
* @return SearchResult
*/
public function getVariantParent()
{
return $this->variantParent;
}
/**
* @param SearchResult $variantParent
*/
public function setVariantParent(SearchResult $variantParent)
{
$this->variantParent = $variantParent;
}
/**
* @return string
*/
public function getContent()
{
return $this->fields['content'];
}
/**
* @return boolean
*/
public function getIsElevated()
{
return $this->fields['isElevated'];
}
/**
* @return string
*/
public function getType()
{
return $this->fields['type'];
}
/**
* @return integer
*/
public function getId()
{
return $this->fields['id'];
}
/**
* @return float
*/
public function getScore()
{
return $this->fields['score'];
}
/**
* @return string
*/
public function getUrl()
{
return $this->fields['url'];
}
/**
* @return string
*/
public function getTitle()
{
return $this->fields['title'];
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* The SearchResultBuilder is responsible to build a SearchResult object from an \WapplerSystems\Meilisearch\System\Solr\Document\Document
* and should use a different class as SearchResult if configured.
*/
class SearchResultBuilder {
/**
* This method is used to wrap the original solr document instance in an instance of the configured SearchResult
* class.
*
* @param Document $originalDocument
* @throws \InvalidArgumentException
* @return SearchResult
*/
public function fromApacheSolrDocument(Document $originalDocument)
{
$searchResultClassName = $this->getResultClassName();
$result = GeneralUtility::makeInstance($searchResultClassName, /** @scrutinizer ignore-type */ $originalDocument->getFields() ?? []);
if (!$result instanceof SearchResult) {
throw new \InvalidArgumentException('Could not create result object with class: ' . (string)$searchResultClassName, 1470037679);
}
return $result;
}
/**
* @return string
*/
protected function getResultClassName()
{
return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultClassName ']) ?
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultClassName '] : SearchResult::class;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result;
/***************************************************************
* Copyright notice
*
* (c) 2017 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Grouping\GroupCollection;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* The SearchResultCollection contains the SearchResult object and related objects. e.g groups.
*/
class SearchResultCollection extends AbstractCollection {
/**
* @var GroupCollection
*/
protected $groups = null;
/**
* SearchResultCollection constructor.
* @param array $data
*/
public function __construct(array $data = [])
{
parent::__construct($data);
$this->groups = new GroupCollection();
}
/**
* @return GroupCollection
*/
public function getGroups(): GroupCollection
{
return $this->groups;
}
/**
* @param GroupCollection $groups
*/
public function setGroups(GroupCollection $groups)
{
$this->groups = $groups;
}
/**
* @return bool
*/
public function getHasGroups(): bool
{
return $this->groups->getCount() > 0;
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetRegistry;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RequirementsService;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking\Suggestion;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* This processor is used to transform the solr response into a
* domain object hierarchy that can be used in the application (controller and view).
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class ResultSetReconstitutionProcessor implements SearchResultSetProcessor
{
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @return ObjectManagerInterface
*/
public function getObjectManager()
{
if ($this->objectManager === null) {
$this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
}
return $this->objectManager;
}
/**
* @param ObjectManagerInterface $objectManager
*/
public function setObjectManager($objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @return FacetRegistry
*/
protected function getFacetRegistry()
{
// @extensionScannerIgnoreLine
return $this->getObjectManager()->get(FacetRegistry::class);
}
/**
* The implementation can be used to influence a SearchResultSet that is
* created and processed in the SearchResultSetService.
*
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
public function process(SearchResultSet $resultSet)
{
if (!$resultSet instanceof SearchResultSet) {
return $resultSet;
}
$resultSet = $this->parseSpellCheckingResponseIntoObjects($resultSet);
$resultSet = $this->parseSortingIntoObjects($resultSet);
// here we can reconstitute other domain objects from the solr response
$resultSet = $this->parseFacetsIntoObjects($resultSet);
return $resultSet;
}
/**
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
protected function parseSortingIntoObjects(SearchResultSet $resultSet)
{
$configuration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration();
$hasSorting = $resultSet->getUsedSearchRequest()->getHasSorting();
$activeSortingName = $resultSet->getUsedSearchRequest()->getSortingName();
$activeSortingDirection = $resultSet->getUsedSearchRequest()->getSortingDirection();
// no configuration available
if (!isset($configuration)) {
return $resultSet;
}
// no sorting enabled
if (!$configuration->getSearchSorting()) {
return $resultSet;
}
foreach ($configuration->getSearchSortingOptionsConfiguration() as $sortingKeyName => $sortingOptions) {
$sortingName = rtrim($sortingKeyName, '.');
$selected = false;
$direction = $configuration->getSearchSortingDefaultOrderBySortOptionName($sortingName);
// when we have an active sorting in the request we compare the sortingName and mark is as active and
// use the direction from the request
if ($hasSorting && $activeSortingName == $sortingName) {
$selected = true;
$direction = $activeSortingDirection;
}
$field = $sortingOptions['field'];
$label = $sortingOptions['label'];
$isResetOption = $field === 'relevance';
// Allow stdWrap on label:
$labelHasSubConfiguration = is_array($sortingOptions['label.']);
if ($labelHasSubConfiguration) {
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$label = $cObj->stdWrap($label, $sortingOptions['label.']);
}
$sorting = $this->getObjectManager()->get(Sorting::class, $resultSet, $sortingName, $field, $direction, $label, $selected, $isResetOption);
$resultSet->addSorting($sorting);
}
return $resultSet;
}
/**
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
private function parseSpellCheckingResponseIntoObjects(SearchResultSet $resultSet)
{
//read the response
$response = $resultSet->getResponse();
if (!is_array($response->spellcheck->suggestions)) {
return $resultSet;
}
$misspelledTerm = '';
foreach ($response->spellcheck->suggestions as $key => $suggestionData) {
if (is_string($suggestionData)) {
$misspelledTerm = $key;
continue;
}
if ($misspelledTerm === '') {
throw new \UnexpectedValueException('No missspelled term before suggestion');
}
if (!is_object($suggestionData) && !is_array($suggestionData->suggestion)) {
continue;
}
foreach ($suggestionData->suggestion as $suggestedTerm) {
$suggestion = $this->createSuggestionFromResponseFragment($suggestionData, $suggestedTerm, $misspelledTerm);
//add it to the resultSet
$resultSet->addSpellCheckingSuggestion($suggestion);
}
}
return $resultSet;
}
/**
* @param \stdClass $suggestionData
* @param string $suggestedTerm
* @param string $misspelledTerm
* @return Suggestion
*/
private function createSuggestionFromResponseFragment($suggestionData, $suggestedTerm, $misspelledTerm)
{
$numFound = isset($suggestionData->numFound) ? $suggestionData->numFound : 0;
$startOffset = isset($suggestionData->startOffset) ? $suggestionData->startOffset : 0;
$endOffset = isset($suggestionData->endOffset) ? $suggestionData->endOffset : 0;
// by now we avoid to use GeneralUtility::makeInstance, since we only create a value object
// and the usage might be a overhead.
$suggestion = new Suggestion($suggestedTerm, $misspelledTerm, $numFound, $startOffset, $endOffset);
return $suggestion;
}
/**
* Parse available facets into objects
*
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
private function parseFacetsIntoObjects(SearchResultSet $resultSet)
{
// Make sure we can access the facet configuration
if (!$resultSet->getUsedSearchRequest() || !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()) {
return $resultSet;
}
// Read the response
$response = $resultSet->getResponse();
if (!is_object($response->facet_counts) && !is_object($response->facets)) {
return $resultSet;
}
/** @var FacetRegistry $facetRegistry */
$facetRegistry = $this->getFacetRegistry();
$facetsConfiguration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingFacets();
foreach ($facetsConfiguration as $name => $options) {
if (!is_array($options)) {
continue;
}
$facetName = rtrim($name, '.');
$type = !empty($options['type']) ? $options['type'] : '';
$parser = $facetRegistry->getPackage($type)->getParser();
$facet = $parser->parse($resultSet, $facetName, $options);
if ($facet !== null) {
$resultSet->addFacet($facet);
}
}
$this->applyRequirements($resultSet);
return $resultSet;
}
/**
* @param SearchResultSet $resultSet
*/
protected function applyRequirements(SearchResultSet $resultSet)
{
$requirementsService = $this->getRequirementsService();
$facets = $resultSet->getFacets();
foreach ($facets as $facet) {
/** @var $facet AbstractFacet */
$requirementsMet = $requirementsService->getAllRequirementsMet($facet);
$facet->setAllRequirementsMet($requirementsMet);
}
}
/**
* @return RequirementsService
*/
protected function getRequirementsService()
{
// @extensionScannerIgnoreLine
return $this->getObjectManager()->get(RequirementsService::class);
}
}

View File

@@ -0,0 +1,445 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\Query;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResult;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting\SortingCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking\Suggestion;
use WapplerSystems\Meilisearch\Domain\Search\SearchRequest;
use WapplerSystems\Meilisearch\Search;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
/**
* The SearchResultSet is used to provided access to the \Apache_Solr_Response and
* other relevant information, like the used Query and Request objects.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
class SearchResultSet
{
/**
* @var Query
*/
protected $usedQuery = null;
/**
* @var SearchRequest
*/
protected $usedSearchRequest = null;
/**
* @var Search
*/
protected $usedSearch;
/**
* @var ResponseAdapter
*/
protected $response = null;
/**
* @var int
*/
protected $usedPage = 0;
/**
* @var int
*/
protected $usedResultsPerPage = 0;
/**
* @var array
*/
protected $usedAdditionalFilters = [];
/**
* @var SearchResultCollection
*/
protected $searchResults = null;
/**
* @var int
*/
protected $allResultCount = 0;
/**
* @var float
*/
protected $maximumScore = 0.0;
/**
* @var Suggestion[]
*/
protected $spellCheckingSuggestions = [];
/**
* @var FacetCollection
*/
protected $facets = null;
/**
* @var SortingCollection
*/
protected $sortings = null;
/**
* @var bool
*/
protected $isAutoCorrected = false;
/**
* @var string
*/
protected $initialQueryString = '';
/**
* @var string
*/
protected $correctedQueryString = '';
/**
* @var bool
*/
protected $hasSearched = false;
/**
* @return \WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet
*/
public function __construct()
{
$this->facets = new FacetCollection();
$this->sortings = new SortingCollection();
$this->searchResults = new SearchResultCollection();
}
/**
* @param int $allResultCount
*/
public function setAllResultCount($allResultCount)
{
$this->allResultCount = $allResultCount;
}
/**
* @return int
*/
public function getAllResultCount()
{
return $this->allResultCount;
}
/**
* @param Suggestion $suggestion
*/
public function addSpellCheckingSuggestion(Suggestion $suggestion)
{
$this->spellCheckingSuggestions[$suggestion->getSuggestion()] = $suggestion;
}
/**
* @return bool
*/
public function getHasSpellCheckingSuggestions()
{
return count($this->spellCheckingSuggestions) > 0;
}
/**
* @param \WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking\Suggestion[] $spellCheckingSuggestions
*/
public function setSpellCheckingSuggestions($spellCheckingSuggestions)
{
$this->spellCheckingSuggestions = $spellCheckingSuggestions;
}
/**
* @return \WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking\Suggestion[]
*/
public function getSpellCheckingSuggestions()
{
return $this->spellCheckingSuggestions;
}
/**
* @return FacetCollection
*/
public function getFacets()
{
return $this->facets;
}
/**
* @param AbstractFacet $facet
*/
public function addFacet(AbstractFacet $facet)
{
$this->facets->addFacet($facet);
}
/**
* @return float
*/
public function getMaximumScore()
{
return $this->maximumScore;
}
/**
* @param float $maximumScore
*/
public function setMaximumScore($maximumScore)
{
$this->maximumScore = $maximumScore;
}
/**
* @param Sorting $sorting
*/
public function addSorting(Sorting $sorting)
{
$this->sortings->addSorting($sorting);
}
/**
* @return SortingCollection
*/
public function getSortings()
{
return $this->sortings;
}
/**
* @param ResponseAdapter $response
*/
public function setResponse($response)
{
$this->response = $response;
}
/**
* @return ResponseAdapter
*/
public function getResponse()
{
return $this->response;
}
/**
* @param array $usedAdditionalFilters
*/
public function setUsedAdditionalFilters($usedAdditionalFilters)
{
$this->usedAdditionalFilters = $usedAdditionalFilters;
}
/**
* @return array
*/
public function getUsedAdditionalFilters()
{
return $this->usedAdditionalFilters;
}
/**
* @param Query $usedQuery
*/
public function setUsedQuery($usedQuery)
{
$this->usedQuery = $usedQuery;
}
/**
* Retrieves the query object that has been used to build this result set.
*
* @return Query
*/
public function getUsedQuery()
{
return $this->usedQuery;
}
/**
* @param int $page
*/
public function setUsedPage($page)
{
$this->usedPage = $page;
}
/**
* Retrieve the page argument that has been used to build this SearchResultSet.
*
* @return int
*/
public function getUsedPage()
{
return $this->usedPage;
}
/**
* @param \WapplerSystems\Meilisearch\Domain\Search\SearchRequest $usedSearchRequest
*/
public function setUsedSearchRequest($usedSearchRequest)
{
$this->usedSearchRequest = $usedSearchRequest;
}
/**
* Retrieves the SearchRequest that has been used to build this SearchResultSet.
*
* @return \WapplerSystems\Meilisearch\Domain\Search\SearchRequest
*/
public function getUsedSearchRequest()
{
return $this->usedSearchRequest;
}
/**
* @param \WapplerSystems\Meilisearch\Search $usedSearch
*/
public function setUsedSearch($usedSearch)
{
$this->usedSearch = $usedSearch;
}
/**
* @return \WapplerSystems\Meilisearch\Search
*/
public function getUsedSearch()
{
return $this->usedSearch;
}
/**
* @param int $usedResultsPerPage
*/
public function setUsedResultsPerPage($usedResultsPerPage)
{
$this->usedResultsPerPage = $usedResultsPerPage;
}
/**
* @return int
*/
public function getUsedResultsPerPage()
{
return $this->usedResultsPerPage;
}
/**
* @return SearchResultCollection
*/
public function getSearchResults()
{
return $this->searchResults;
}
/**
* @param SearchResultCollection $searchResults
*/
public function setSearchResults($searchResults)
{
$this->searchResults = $searchResults;
}
/**
* @param SearchResult $searchResult
*/
public function addSearchResult(SearchResult $searchResult)
{
$this->searchResults[] = $searchResult;
}
/**
* @return boolean
*/
public function getIsAutoCorrected()
{
return $this->isAutoCorrected;
}
/**
* @param boolean $wasAutoCorrected
*/
public function setIsAutoCorrected($wasAutoCorrected)
{
$this->isAutoCorrected = $wasAutoCorrected;
}
/**
* @return string
*/
public function getInitialQueryString()
{
return $this->initialQueryString;
}
/**
* @param string $initialQueryString
*/
public function setInitialQueryString($initialQueryString)
{
$this->initialQueryString = $initialQueryString;
}
/**
* @return string
*/
public function getCorrectedQueryString()
{
return $this->correctedQueryString;
}
/**
* @param string $correctedQueryString
*/
public function setCorrectedQueryString($correctedQueryString)
{
$this->correctedQueryString = $correctedQueryString;
}
/**
* @return boolean
*/
public function getHasSearched(): bool
{
return $this->hasSearched;
}
/**
* @param boolean $hasSearched
*/
public function setHasSearched(bool $hasSearched)
{
$this->hasSearched = $hasSearched;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* The implementation can be used to influence a SearchResultSet that is
* created and processed in the SearchResultSetService.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
interface SearchResultSetProcessor
{
/**
* The implementation can be used to influence a SearchResultSet that is
* created and processed in the SearchResultSetService.
*
* @param SearchResultSet $resultSet
* @return mixed
*/
public function process(SearchResultSet $resultSet);
}

View File

@@ -0,0 +1,499 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet;
/***************************************************************
* Copyright notice
*
* (c) 2015-2016 Timo Schmidt <timo.schmidt@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\Query\ParameterBuilder\QueryFields;
use WapplerSystems\Meilisearch\Domain\Search\Query\QueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\Query\Query;
use WapplerSystems\Meilisearch\Domain\Search\Query\SearchQuery;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResult;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection;
use WapplerSystems\Meilisearch\Domain\Search\SearchRequest;
use WapplerSystems\Meilisearch\Domain\Search\SearchRequestAware;
use WapplerSystems\Meilisearch\Domain\Variants\VariantsProcessor;
use WapplerSystems\Meilisearch\Query\Modifier\Modifier;
use WapplerSystems\Meilisearch\Search;
use WapplerSystems\Meilisearch\Search\QueryAware;
use WapplerSystems\Meilisearch\Search\SearchAware;
use WapplerSystems\Meilisearch\Search\SearchComponentManager;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
use WapplerSystems\Meilisearch\System\Logging\SolrLogManager;
use WapplerSystems\Meilisearch\System\Solr\Document\Document;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
use WapplerSystems\Meilisearch\System\Solr\SolrIncompleteResponseException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultBuilder;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
/**
* The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
* It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
*
* @author Timo Schmidt <timo.schmidt@dkd.de>
*/
class SearchResultSetService
{
/**
* Track, if the number of results per page has been changed by the current request
*
* @var bool
*/
protected $resultsPerPageChanged = false;
/**
* @var Search
*/
protected $search;
/**
* @var SearchResultSet
*/
protected $lastResultSet = null;
/**
* @var boolean
*/
protected $isSolrAvailable = false;
/**
* @var TypoScriptConfiguration
*/
protected $typoScriptConfiguration;
/**
* @var SolrLogManager
*/
protected $logger = null;
/**
* @var SearchResultBuilder
*/
protected $searchResultBuilder;
/**
* @var QueryBuilder
*/
protected $queryBuilder;
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* @param TypoScriptConfiguration $configuration
* @param Search $search
* @param SolrLogManager $solrLogManager
* @param SearchResultBuilder $resultBuilder
* @param QueryBuilder $queryBuilder
*/
public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
{
$this->search = $search;
$this->typoScriptConfiguration = $configuration;
$this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
$this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
$this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
}
/**
* @param ObjectManagerInterface $objectManager
*/
public function injectObjectManager(ObjectManagerInterface $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* @param bool $useCache
* @return bool
*/
public function getIsSolrAvailable($useCache = true)
{
$this->isSolrAvailable = $this->search->ping($useCache);
return $this->isSolrAvailable;
}
/**
* Retrieves the used search instance.
*
* @return Search
*/
public function getSearch()
{
return $this->search;
}
/**
* @param Query $query
* @param SearchRequest $searchRequest
*/
protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
{
$searchComponents = $this->getRegisteredSearchComponents();
foreach ($searchComponents as $searchComponent) {
/** @var Search\SearchComponent $searchComponent */
$searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
if ($searchComponent instanceof QueryAware) {
$searchComponent->setQuery($query);
}
if ($searchComponent instanceof SearchRequestAware) {
$searchComponent->setSearchRequest($searchRequest);
}
$searchComponent->initializeSearchComponent();
}
}
/**
* @return string
*/
protected function getResultSetClassName()
{
return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
}
/**
* Performs a search and returns a SearchResultSet.
*
* @param SearchRequest $searchRequest
* @return SearchResultSet
*/
public function search(SearchRequest $searchRequest)
{
$resultSet = $this->getInitializedSearchResultSet($searchRequest);
$this->lastResultSet = $resultSet;
$resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
$resultSet->setHasSearched(false);
return $resultSet;
}
$query = $this->queryBuilder->buildSearchQuery($searchRequest->getRawUserQuery(), (int)$searchRequest->getResultsPerPage(), $searchRequest->getAdditionalFilters());
$this->initializeRegisteredSearchComponents($query, $searchRequest);
$resultSet->setUsedQuery($query);
// performing the actual search, sending the query to the Solr server
$query = $this->modifyQuery($query, $searchRequest, $this->search);
$response = $this->doASearch($query, $searchRequest);
if ((int)$searchRequest->getResultsPerPage() === 0) {
// when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
// when results for the initial search should not be shown.
// @extensionScannerIgnoreLine
$response->response->numFound = 0;
}
$resultSet->setHasSearched(true);
$resultSet->setResponse($response);
$this->getParsedSearchResults($resultSet);
$resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
/** @var $variantsProcessor VariantsProcessor */
$variantsProcessor = GeneralUtility::makeInstance(
VariantsProcessor::class,
/** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
/** @scrutinizer ignore-type */ $this->searchResultBuilder
);
$variantsProcessor->process($resultSet);
/** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
$searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
$searchResultReconstitutionProcessor->process($resultSet);
$resultSet = $this->getAutoCorrection($resultSet);
return $this->handleSearchHook('afterSearch', $resultSet);
}
/**
* Uses the configured parser and retrieves the parsed search resutls.
*
* @param SearchResultSet $resultSet
*/
protected function getParsedSearchResults($resultSet)
{
/** @var ResultParserRegistry $parserRegistry */
$parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
$useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_meilisearch.features.useRawDocuments', false);
$parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
}
/**
* Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
* SearchResultSet should be returned.
*
* @param SearchRequest $searchRequest
* @return bool
*/
protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest)
{
if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
// when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
return true;
}
if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
// the user entered an empty query string "" or " " and empty querystring is not allowed
return true;
}
return false;
}
/**
* Initializes the SearchResultSet from the SearchRequest
*
* @param SearchRequest $searchRequest
* @return SearchResultSet
*/
protected function getInitializedSearchResultSet(SearchRequest $searchRequest):SearchResultSet
{
/** @var $resultSet SearchResultSet */
$resultSetClass = $this->getResultSetClassName();
$resultSet = $this->objectManager->get($resultSetClass);
$resultSet->setUsedSearchRequest($searchRequest);
$resultSet->setUsedPage((int)$searchRequest->getPage());
$resultSet->setUsedResultsPerPage((int)$searchRequest->getResultsPerPage());
$resultSet->setUsedSearch($this->search);
return $resultSet;
}
/**
* Executes the search and builds a fake response for a current bug in Apache Solr 6.3
*
* @param Query $query
* @param SearchRequest $searchRequest
* @return ResponseAdapter
*/
protected function doASearch($query, $searchRequest): ResponseAdapter
{
// the offset mulitplier is page - 1 but not less then zero
$offsetMultiplier = max(0, $searchRequest->getPage() - 1);
$offSet = $offsetMultiplier * (int)$searchRequest->getResultsPerPage();
$response = $this->search->search($query, $offSet, null);
if($response === null) {
throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
}
return $response;
}
/**
* @param SearchResultSet $searchResultSet
* @return SearchResultSet
*/
protected function getAutoCorrection(SearchResultSet $searchResultSet)
{
// no secondary search configured
if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
return $searchResultSet;
}
// more then zero results
if ($searchResultSet->getAllResultCount() > 0) {
return $searchResultSet;
}
// no corrections present
if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
return $searchResultSet;
}
$searchResultSet = $this->peformAutoCorrection($searchResultSet);
return $searchResultSet;
}
/**
* @param SearchResultSet $searchResultSet
* @return SearchResultSet
*/
protected function peformAutoCorrection(SearchResultSet $searchResultSet)
{
$searchRequest = $searchResultSet->getUsedSearchRequest();
$suggestions = $searchResultSet->getSpellCheckingSuggestions();
$maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
$runs = 0;
foreach ($suggestions as $suggestion) {
$runs++;
$correction = $suggestion->getSuggestion();
$initialQuery = $searchRequest->getRawUserQuery();
$searchRequest->setRawQueryString($correction);
$searchResultSet = $this->search($searchRequest);
if ($searchResultSet->getAllResultCount() > 0) {
$searchResultSet->setIsAutoCorrected(true);
$searchResultSet->setCorrectedQueryString($correction);
$searchResultSet->setInitialQueryString($initialQuery);
break;
}
if ($runs > $maximumRuns) {
break;
}
}
return $searchResultSet;
}
/**
* Allows to modify a query before eventually handing it over to Solr.
*
* @param Query $query The current query before it's being handed over to Solr.
* @param SearchRequest $searchRequest The searchRequest, relevant in the current context
* @param Search $search The search, relevant in the current context
* @throws \UnexpectedValueException
* @return Query The modified query that is actually going to be given to Solr.
*/
protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
{
// hook to modify the search query
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
$queryModifier = $this->objectManager->get($classReference);
if ($queryModifier instanceof Modifier) {
if ($queryModifier instanceof SearchAware) {
$queryModifier->setSearch($search);
}
if ($queryModifier instanceof SearchRequestAware) {
$queryModifier->setSearchRequest($searchRequest);
}
$query = $queryModifier->modifyQuery($query);
} else {
throw new \UnexpectedValueException(
get_class($queryModifier) . ' must implement interface ' . Modifier::class,
1310387414
);
}
}
}
return $query;
}
/**
* Retrieves a single document from solr by document id.
*
* @param string $documentId
* @return SearchResult
*/
public function getDocumentById($documentId)
{
/* @var $query SearchQuery */
$query = $this->queryBuilder->newSearchQuery($documentId)->useQueryFields(QueryFields::fromString('id'))->getQuery();
$response = $this->search->search($query, 0, 1);
$parsedData = $response->getParsedData();
// @extensionScannerIgnoreLine
$resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
if (!$resultDocument instanceof Document) {
throw new \UnexpectedValueException("Response did not contain a valid Document object");
}
return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
}
/**
* This method is used to call the registered hooks during the search execution.
*
* @param string $eventName
* @param SearchResultSet $resultSet
* @return SearchResultSet
*/
private function handleSearchHook($eventName, SearchResultSet $resultSet)
{
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
return $resultSet;
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
$afterSearchProcessor = $this->objectManager->get($classReference);
if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
$afterSearchProcessor->process($resultSet);
}
}
return $resultSet;
}
/**
* @return SearchResultSet
*/
public function getLastResultSet()
{
return $this->lastResultSet;
}
/**
* This method returns true when the last search was executed with an empty query
* string or whitespaces only. When no search was triggered it will return false.
*
* @return bool
*/
public function getLastSearchWasExecutedWithEmptyQueryString()
{
$wasEmptyQueryString = false;
if ($this->lastResultSet != null) {
$wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
}
return $wasEmptyQueryString;
}
/**
* @return bool
*/
protected function getInitialSearchIsConfigured()
{
return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
}
/**
* @return mixed
*/
protected function getRegisteredSearchComponents()
{
return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class Sorting
*/
class Sorting
{
const DIRECTION_DESC = 'desc';
const DIRECTION_ASC = 'asc';
/**
* @var array
*/
protected static $validDirections = [self::DIRECTION_DESC, self::DIRECTION_ASC];
/**
* @var string
*/
protected $name = '';
/**
* @var string
*/
protected $field = '';
/**
* @var string
*/
protected $direction = self::DIRECTION_ASC;
/**
* @var string
*/
protected $label = '';
/**
* @var bool
*/
protected $selected = false;
/**
* @var bool
*/
protected $isResetOption = false;
/**
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $direction
* @param string $label
* @param boolean $selected
* @param boolean $isResetOption
* @throws \InvalidArgumentException
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $direction, $label, $selected, $isResetOption)
{
if (!self::getIsValidDirection($direction)) {
throw new \InvalidArgumentException("Invalid sorting direction");
}
$this->name = $name;
$this->direction = $direction;
$this->field = $field;
$this->label = $label;
$this->selected = $selected;
$this->isResetOption = $isResetOption;
}
/**
* @return string
*/
public function getDirection()
{
return $this->direction;
}
/**
* @return bool
*/
public function getIsAscDirection()
{
return $this->direction === self::DIRECTION_ASC;
}
/**
* @return bool
*/
public function getIsDescDirection()
{
return $this->direction === self::DIRECTION_DESC;
}
/**
* Returns the opposite direction of the current assigned direction.
*
* @return string
*/
public function getOppositeDirection()
{
return self::getOppositeDirectionFromDirection($this->direction);
}
/**
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* @return string
*/
public function getLabel()
{
return $this->label;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return boolean
*/
public function getSelected()
{
return $this->selected;
}
/**
* @param string $direction
* @return bool
*/
public static function getIsValidDirection($direction)
{
return in_array($direction, self::$validDirections);
}
/**
* @param string $direction
* @return string
*/
public static function getOppositeDirectionFromDirection($direction)
{
if ($direction === self::DIRECTION_ASC) {
return self::DIRECTION_DESC;
} else {
return self::DIRECTION_ASC;
}
}
/**
* @return boolean
*/
public function getIsResetOption()
{
return $this->isResetOption;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting;
use WapplerSystems\Meilisearch\System\Data\AbstractCollection;
/**
* Class SortingCollection
*/
class SortingCollection extends AbstractCollection
{
/**
* @var Sorting
*/
protected $selected;
/**
* @param Sorting $sorting
*/
public function addSorting(Sorting $sorting)
{
if ($sorting->getSelected()) {
$this->selected = $sorting;
}
$this->data[$sorting->getName()] = $sorting;
}
/**
* @param array $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @param Sorting $selected
*/
protected function setSelected(Sorting $selected)
{
$this->selected = $selected;
}
/**
* @return Sorting
*/
public function getSelected()
{
return $this->selected;
}
/**
* @return bool
*/
public function getHasSelected()
{
return $this->selected !== null;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting;
/***************************************************************
* Copyright notice
*
* (c) 2015-2018 Timo Hund <timo.hund@dkd.de>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project 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 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script 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.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Class SortingHelper
*/
class SortingHelper {
/**
* @var array
*/
protected $configuration = [];
/**
* Constructor
*
* @param array $sortingConfiguration Raw configuration from plugin.tx_meilisearch.search.sorting.options
*/
public function __construct(array $sortingConfiguration)
{
$this->configuration = $sortingConfiguration;
}
/**
* Takes the tx_meilisearch[sort] URL parameter containing the option names and
* directions to sort by and resolves it to the actual sort fields and
* directions as configured through TypoScript. Makes sure that only
* configured sorting options get applied to the query.
*
* @param string $urlParameters tx_meilisearch[sort] URL parameter.
* @return string The actual index field configured to sort by for the given sort option name
* @throws \InvalidArgumentException if the given sort option is not configured
*/
public function getSortFieldFromUrlParameter($urlParameters)
{
$sortFields = [];
$sortParameters = GeneralUtility::trimExplode(',', $urlParameters);
$removeTsKeyDot = function($sortingKey) { return trim($sortingKey, '.'); };
$configuredSortingName = array_map($removeTsKeyDot, array_keys($this->configuration));
foreach ($sortParameters as $sortParameter) {
list($sortOption, $sortDirection) = explode(' ', $sortParameter);
if (!in_array($sortOption, $configuredSortingName)) {
throw new \InvalidArgumentException('No sorting configuration found for option name ' . $sortOption, 1316187644);
}
$sortField = $this->configuration[$sortOption . '.']['field'];
$sortFields[] = $sortField . ' ' . $sortDirection;
}
return implode(', ', $sortFields);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Value object that represent a spellchecking suggestion.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Suggestion
{
/**
* @var string
*/
protected $suggestion = '';
/**
* @var string
*/
protected $missSpelled = '';
/**
* @var int
*/
protected $numFound = 1;
/**
* @var int
*/
protected $startOffset = 0;
/**
* @var int
*/
protected $endOffset = 0;
/**
* @param string $suggestion the suggested term
* @param string $missSpelled the misspelled original term
* @param int $numFound
* @param int $startOffset
* @param int $endOffset
*/
public function __construct($suggestion = '', $missSpelled = '', $numFound = 1, $startOffset = 0, $endOffset = 0)
{
$this->suggestion = $suggestion;
$this->missSpelled = $missSpelled;
$this->numFound = $numFound;
$this->startOffset = $startOffset;
$this->endOffset = $endOffset;
}
/**
* @return int
*/
public function getEndOffset()
{
return $this->endOffset;
}
/**
* @return int
*/
public function getNumFound()
{
return $this->numFound;
}
/**
* @return int
*/
public function getStartOffset()
{
return $this->startOffset;
}
/**
* @return string
*/
public function getSuggestion()
{
return $this->suggestion;
}
/**
* @return string
*/
public function getMissSpelled()
{
return $this->missSpelled;
}
}