453 lines
15 KiB
PHP
453 lines
15 KiB
PHP
|
<?php
|
||
|
namespace App\Library\Util;
|
||
|
|
||
|
/**
|
||
|
* Implements API for describing search queries.
|
||
|
*
|
||
|
* @note This part of Opus search API differs from Solr in terminology in that
|
||
|
* all requests for searching documents are considered "queries" with a
|
||
|
* "filter" used to describe conditions matching documents has to met.
|
||
|
* In opposition to Solr's "filter queries" this API supports "subfilters"
|
||
|
* to reduce confusions on differences between filters, queries and
|
||
|
* filter queries. Thus wording is mapped like this
|
||
|
*
|
||
|
* Solr --> Opus
|
||
|
* "request" --> "query"
|
||
|
* "query" --> "filter"
|
||
|
* "filter query" --> "subfilter"
|
||
|
*
|
||
|
* @method int getStart( int $default = null )
|
||
|
* @method int getRows( int $default = null )
|
||
|
* @method string[] getFields( array $default = null )
|
||
|
* @method array getSort( array $default = null )
|
||
|
* @method bool getUnion( bool $default = null )
|
||
|
* @method Opus_Search_Filter_Base getFilter( Opus_Search_Filter_Base $default = null )
|
||
|
* @method Opus_Search_Facet_Set getFacet( Opus_Search_Facet_Set $default = null )
|
||
|
* @method $this setStart( int $offset )
|
||
|
* @method $this setRows( int $count )
|
||
|
* @method $this setFields( $fields )
|
||
|
* @method $this setSort( $sorting )
|
||
|
* @method $this setUnion( bool $isUnion )
|
||
|
* @method $this setFilter( Opus_Search_Filter_Base $filter ) assigns condition to be met by resulting documents
|
||
|
* @method $this setFacet( Opus_Search_Facet_Set $facet )
|
||
|
* @method $this addFields( string $fields )
|
||
|
* @method $this addSort( $sorting )
|
||
|
*/
|
||
|
class SearchParameter
|
||
|
{
|
||
|
protected $_data;
|
||
|
|
||
|
public function reset()
|
||
|
{
|
||
|
$this->_data = array(
|
||
|
'start' => null,
|
||
|
'rows' => null,
|
||
|
'fields' => null,
|
||
|
'sort' => null,
|
||
|
'union' => null,
|
||
|
'filter' => null,
|
||
|
'facet' => null,
|
||
|
'subfilters' => null,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
public function __construct()
|
||
|
{
|
||
|
$this->reset();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Tests if provided name is actually name of known parameter normalizing it
|
||
|
* on return.
|
||
|
*
|
||
|
* @throws InvalidArgumentException unless providing name of existing parameter
|
||
|
* @param string $name name of parameter to access
|
||
|
* @return string normalized name of existing parameter
|
||
|
*/
|
||
|
protected function isValidParameter($name)
|
||
|
{
|
||
|
if (!array_key_exists(strtolower(trim($name)), $this->_data)) {
|
||
|
throw new InvalidArgumentException('invalid query parameter: ' . $name);
|
||
|
}
|
||
|
|
||
|
return strtolower(trim($name));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes one or more field names or set of comma-separated field names
|
||
|
* into set of field names.
|
||
|
*
|
||
|
* @param string|string[] $input one or more field names or comma-separated lists of fields' names
|
||
|
* @return string[] list of field names
|
||
|
*/
|
||
|
protected function normalizeFields($input)
|
||
|
{
|
||
|
if (!is_array($input)) {
|
||
|
$input = array($input);
|
||
|
}
|
||
|
$output = array();
|
||
|
|
||
|
foreach ($input as $field) {
|
||
|
if (!is_string($field)) {
|
||
|
throw new InvalidArgumentException('invalid type of field selector');
|
||
|
}
|
||
|
|
||
|
$fieldNames = preg_split('/[\s,]+/', $field, null, PREG_SPLIT_NO_EMPTY);
|
||
|
foreach ($fieldNames as $name) {
|
||
|
if (!preg_match('/^(?:\*|[a-z_][a-z0-9_]*)$/i', $name)) {
|
||
|
throw new InvalidArgumentException('malformed field selector: ' . $name);
|
||
|
}
|
||
|
$output[] = $name;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!count($input)) {
|
||
|
throw new InvalidArgumentException('missing field selector');
|
||
|
}
|
||
|
return $output;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses provided parameter for describing some sorting direction.
|
||
|
*
|
||
|
* @param string|bool $ascending one out of true, false, "asc" or "desc"
|
||
|
* @return bool true if parameter is considered requesting to sort in ascending order
|
||
|
*/
|
||
|
protected function normalizeDirection($ascending)
|
||
|
{
|
||
|
if (!strcasecmp($ascending, 'asc')) {
|
||
|
$ascending = true;
|
||
|
} elseif (!strcasecmp($ascending, 'desc')) {
|
||
|
$ascending = false;
|
||
|
} elseif ($ascending !== false && $ascending !== true) {
|
||
|
throw new InvalidArgumentException('invalid sorting direction selector');
|
||
|
}
|
||
|
return $ascending;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves value of selected query parameter.
|
||
|
*
|
||
|
* @param string $name name of parameter to read
|
||
|
* @param mixed $defaultValue value to retrieve if parameter hasn't been set internally
|
||
|
* @return mixed value of selected parameter, default if missing internally
|
||
|
*/
|
||
|
public function get($name, $defaultValue = null)
|
||
|
{
|
||
|
$name = $this->isValidParameter($name);
|
||
|
|
||
|
return is_null($this->_data[$name]) ? $defaultValue : $this->_data[$name];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets value of selected query parameter.
|
||
|
*
|
||
|
* @throws InvalidArgumentException in case of invalid arguments (e.g. on trying to add value to single-value param)
|
||
|
* @param string $name name of query parameter to adjust
|
||
|
* @param string[]|array|string|int $value value of query parameter to write
|
||
|
* @param bool $adding true for adding given parameter to any existing one
|
||
|
* @return $this
|
||
|
*/
|
||
|
public function set($name, $value, $adding = false) //filter, "aa", false
|
||
|
{
|
||
|
$name = $this->isValidParameter($name);
|
||
|
|
||
|
switch ($name) {
|
||
|
case 'start':
|
||
|
case 'rows':
|
||
|
if ($adding) {
|
||
|
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||
|
}
|
||
|
|
||
|
if (!is_scalar($value) || !ctype_digit(trim($value))) {
|
||
|
throw new InvalidArgumentException('invalid parameter value on ' . $name);
|
||
|
}
|
||
|
|
||
|
$this->_data[$name] = intval($value);
|
||
|
break;
|
||
|
|
||
|
case 'fields':
|
||
|
$fields = $this->normalizeFields($value);
|
||
|
|
||
|
if ($adding && is_null($this->_data['fields'])) {
|
||
|
$adding = false;
|
||
|
}
|
||
|
|
||
|
if ($adding) {
|
||
|
$this->_data['fields'] = array_merge($this->_data['fields'], $fields);
|
||
|
} else {
|
||
|
if (!count($fields)) {
|
||
|
throw new InvalidArgumentException('setting empty set of fields rejected');
|
||
|
}
|
||
|
$this->_data['fields'] = $fields;
|
||
|
}
|
||
|
|
||
|
$this->_data['fields'] = array_unique($this->_data['fields']);
|
||
|
break;
|
||
|
|
||
|
case 'sort':
|
||
|
if (!is_array($value)) {
|
||
|
$value = array($value, true);
|
||
|
}
|
||
|
|
||
|
switch (count($value)) {
|
||
|
case 2:
|
||
|
$fields = array_shift($value);
|
||
|
$ascending = array_shift($value);
|
||
|
break;
|
||
|
case 1:
|
||
|
$fields = array_shift($value);
|
||
|
$ascending = true;
|
||
|
break;
|
||
|
default:
|
||
|
throw new InvalidArgumentException('invalid sorting selector');
|
||
|
}
|
||
|
|
||
|
$this->addSorting($fields, $ascending, !$adding);
|
||
|
break;
|
||
|
|
||
|
case 'union':
|
||
|
if ($adding) {
|
||
|
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||
|
}
|
||
|
$this->_data[$name] = !!$value;
|
||
|
break;
|
||
|
|
||
|
case 'filter':
|
||
|
if ($adding) {
|
||
|
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||
|
}
|
||
|
|
||
|
// if ( !( $value instanceof Opus_Search_Filter_Base ) ) {
|
||
|
// throw new InvalidArgumentException( 'invalid filter' );
|
||
|
// }
|
||
|
|
||
|
$this->_data[$name] = $value;
|
||
|
break;
|
||
|
|
||
|
case 'facet':
|
||
|
if ($adding) {
|
||
|
throw new InvalidArgumentException('invalid parameter access on ' . $name);
|
||
|
}
|
||
|
|
||
|
if (!($value instanceof Opus_Search_Facet_Set)) {
|
||
|
throw new InvalidArgumentException('invalid facet options');
|
||
|
}
|
||
|
$this->_data[$name] = $value;
|
||
|
break;
|
||
|
|
||
|
case 'subfilters':
|
||
|
throw new RuntimeException('invalid access on sub filters');
|
||
|
}
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function __get($name)
|
||
|
{
|
||
|
return $this->get($name);
|
||
|
}
|
||
|
|
||
|
public function __isset($name)
|
||
|
{
|
||
|
return !is_null($this->get($name));
|
||
|
}
|
||
|
|
||
|
public function __set($name, $value)
|
||
|
{
|
||
|
$this->set($name, $value, false);
|
||
|
}
|
||
|
|
||
|
public function __call($method, $arguments)
|
||
|
{
|
||
|
if (preg_match('/^(get|set|add)([a-z]+)$/i', $method, $matches)) {
|
||
|
$property = $this->isValidParameter($matches[2]);
|
||
|
switch (strtolower($matches[1])) {
|
||
|
case 'get':
|
||
|
return $this->get($property, @$arguments[0]);
|
||
|
|
||
|
case 'set':
|
||
|
$this->set($property, @$arguments[0], false);
|
||
|
return $this;
|
||
|
|
||
|
case 'add':
|
||
|
$this->set($property, @$arguments[0], true);
|
||
|
return $this;
|
||
|
}
|
||
|
}
|
||
|
throw new RuntimeException('invalid method: ' . $method);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds request for sorting by some field in desired order.
|
||
|
*
|
||
|
* @param string|string[] $field one or more field names to add sorting (as array and/or comma-separated string)
|
||
|
* @param bool $ascending true or "asc" for ascending by all given fields
|
||
|
* @param bool $reset true for dropping previously declared sorting
|
||
|
* @return $this fluent interface
|
||
|
*/
|
||
|
public function addSorting($field, $ascending = true, $reset = false)
|
||
|
{
|
||
|
$fields = $this->normalizeFields($field);
|
||
|
$ascending = $this->normalizeDirection($ascending);
|
||
|
|
||
|
if (!count($fields)) {
|
||
|
throw new InvalidArgumentException('missing field for sorting result');
|
||
|
}
|
||
|
|
||
|
if ($reset || !is_array($this->_data['sort'])) {
|
||
|
$this->_data['sort'] = array();
|
||
|
}
|
||
|
|
||
|
foreach ($fields as $field) {
|
||
|
if ($field === '*') {
|
||
|
throw new InvalidArgumentException('invalid request for sorting by all fields (*)');
|
||
|
}
|
||
|
$this->_data['sort'][$field] = $ascending ? 'asc' : 'desc';
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Declares some subfilter.
|
||
|
*
|
||
|
* @note In Solr a search includes a "query" and optionally one or more
|
||
|
* "filter query". This API intends different terminology for the
|
||
|
* whole search request is considered a "query" with a "filter" used
|
||
|
* to select actually desired documents by matching conditions. In
|
||
|
* context with this terminology "subfilter" was used to describe what
|
||
|
* is "filter query" in Solr world: some named query to be included on
|
||
|
* selecting documents in database with some benefits regarding
|
||
|
* performance, server-side result caching and non-affecting score.
|
||
|
*
|
||
|
* @see http://wiki.apache.org/solr/CommonQueryParameters#fq
|
||
|
*
|
||
|
* @param string $name name of query (used for server-side caching)
|
||
|
* @param Opus_Search_Filter_Base $subFilter filter to be satisfied by all matching documents in addition
|
||
|
* @return $this fluent interface
|
||
|
*/
|
||
|
public function setSubFilter($name, Opus_Search_Filter_Base $subFilter)
|
||
|
{
|
||
|
if (!is_string($name) || !$name) {
|
||
|
throw new InvalidArgumentException('invalid sub filter name');
|
||
|
}
|
||
|
|
||
|
if (!is_array($this->_data['subfilters'])) {
|
||
|
$this->_data['subfilters'] = array($name => $subFilter);
|
||
|
} else {
|
||
|
$this->_data['subfilters'][$name] = $subFilter;
|
||
|
}
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes some previously defined subfilter from current query again.
|
||
|
*
|
||
|
* @note This isn't affecting server-side caching of selected filter but
|
||
|
* reverting some parts of query compiled on client-side.
|
||
|
*
|
||
|
* @see Opus_Search_Query::setSubFilter()
|
||
|
*
|
||
|
* @param string $name name of filter to remove from query again
|
||
|
* @return $this fluent interface
|
||
|
*/
|
||
|
public function removeSubFilter($name)
|
||
|
{
|
||
|
if (!is_string($name) || !$name) {
|
||
|
throw new InvalidArgumentException('invalid sub filter name');
|
||
|
}
|
||
|
|
||
|
if (is_array($this->_data['subfilters'])) {
|
||
|
if (array_key_exists($name, $this->_data['subfilters'])) {
|
||
|
unset($this->_data['subfilters'][$name]);
|
||
|
}
|
||
|
if (!count($this->_data['subfilters'])) {
|
||
|
$this->_data['subfilters'] = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves named map of subfilters to include on querying search engine.
|
||
|
*
|
||
|
* @return Opus_Search_Filter_Base[]
|
||
|
*/
|
||
|
public function getSubFilters()
|
||
|
{
|
||
|
return $this->_data['subfilters'];
|
||
|
}
|
||
|
|
||
|
public static function getParameterDefault($name, $fallbackIfMissing, $oldName = null)
|
||
|
{
|
||
|
$config = Opus_Search_Config::getDomainConfiguration();
|
||
|
$defaults = $config->parameterDefaults;
|
||
|
|
||
|
if ($defaults instanceof Zend_Config) {
|
||
|
return $defaults->get($name, $fallbackIfMissing);
|
||
|
}
|
||
|
if ($oldName) {
|
||
|
return $config->get($oldName, $fallbackIfMissing);
|
||
|
}
|
||
|
|
||
|
return $fallbackIfMissing;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves configured default offset for paging results.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public static function getDefaultStart()
|
||
|
{
|
||
|
return static::getParameterDefault('start', 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves configured default number of rows to show (per page).
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public static function getDefaultRows()
|
||
|
{
|
||
|
return static::getParameterDefault('rows', 10, 'numberOfDefaultSearchResults');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves configured default sorting.
|
||
|
*
|
||
|
* @return string[]
|
||
|
*/
|
||
|
public static function getDefaultSorting()
|
||
|
{
|
||
|
$sorting = static::getParameterDefault('sortField', 'score desc');
|
||
|
|
||
|
$parts = preg_split('/[\s,]+/', trim($sorting), null, PREG_SPLIT_NO_EMPTY);
|
||
|
|
||
|
$sorting = array(array_shift($parts));
|
||
|
|
||
|
if (!count($parts)) {
|
||
|
$sorting[] = 'desc';
|
||
|
} else {
|
||
|
$dir = array_shift($parts);
|
||
|
if (strcasecmp($dir, 'asc') || strcasecmp($dir, 'desc')) {
|
||
|
$dir = 'desc';
|
||
|
}
|
||
|
$sorting[] = strtolower($dir);
|
||
|
}
|
||
|
return $sorting;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves configured name of field to use for sorting results by default.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public static function getDefaultSortingField()
|
||
|
{
|
||
|
$sorting = static::getDefaultSorting();
|
||
|
return $sorting[0];
|
||
|
}
|
||
|
}
|