1422 lines
46 KiB
PHP
1422 lines
46 KiB
PHP
|
<?php namespace ProcessWire;
|
|||
|
|
|||
|
/**
|
|||
|
* ProcessWire Selector base type and implementation for various Selector types
|
|||
|
*
|
|||
|
* Selectors hold a field, operator and value and are used in finding things
|
|||
|
*
|
|||
|
* This file provides the base implementation for a Selector, as well as implementation
|
|||
|
* for several actual Selector types under the main Selector class.
|
|||
|
*
|
|||
|
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
|||
|
* https://processwire.com
|
|||
|
*
|
|||
|
* #pw-summary Selector maintains a single selector consisting of field name, operator, and value.
|
|||
|
*
|
|||
|
* #pw-body =
|
|||
|
* - Serves as the base class for the different Selector types (`SelectorEqual`, `SelectorNotEqual`, `SelectorLessThan`, etc.)
|
|||
|
* - The constructor requires `$field` and `$value` properties which may either be an array or string.
|
|||
|
* An array indicates multiple items in an OR condition. Multiple items may also be specified by
|
|||
|
* pipe “|” separated strings.
|
|||
|
* - Operator is determined by the Selector class name, and thus may not be changed without replacing
|
|||
|
* the entire Selector.
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* // very basic usage example
|
|||
|
* // constructor takes ($field, $value) which can be strings or arrays
|
|||
|
* $s = new SelectorEqual('title', 'About Us');
|
|||
|
* // $page can be any kind of Wire-derived object
|
|||
|
* if($s->matches($page)) {
|
|||
|
* // $page has title "About Us"
|
|||
|
* }
|
|||
|
* ~~~~~
|
|||
|
* ~~~~~
|
|||
|
* // another usage example
|
|||
|
* $s = new SelectorContains('title|body|summary', 'foo|bar');
|
|||
|
* if($s->matches($page)) {
|
|||
|
* // the title, body or summary properties of $page contain either the text "foo" or "bar"
|
|||
|
* }
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* ### List of core selector-derived classes
|
|||
|
*
|
|||
|
* - `SelectorEqual`
|
|||
|
* - `SelectorNotEqual`
|
|||
|
* - `SelectorGreaterThan`
|
|||
|
* - `SelectorLessThan`
|
|||
|
* - `SelectorGreaterThanEqual`
|
|||
|
* - `SelectorLessThanEqual`
|
|||
|
* - `SelectorContains`
|
|||
|
* - `SelectorContainsLike`
|
|||
|
* - `SelectorContainsWords`
|
|||
|
* - `SelectorContainsWordsPartial` (3.0.160+)
|
|||
|
* - `SelectorContainsWordsLive` (3.0.160)
|
|||
|
* - `SelectorContainsWordsLike` (3.0.160)
|
|||
|
* - `SelectorContainsWordsExpand` (3.0.160)
|
|||
|
* - `SelectorContainsAnyWords` (3.0.160)
|
|||
|
* - `SelectorContainsAnyWordsPartial` (3.0.160)
|
|||
|
* - `SelectorContainsAnyWordsLike` (3.0.160)
|
|||
|
* - `SelectorContainsExpand` (3.0.160)
|
|||
|
* - `SelectorContainsMatch` (3.0.160)
|
|||
|
* - `SelectorContainsMatchExpand` (3.0.160)
|
|||
|
* - `SelectorContainsAdvanced` (3.0.160)
|
|||
|
* - `SelectorStarts`
|
|||
|
* - `SelectorStartsLike`
|
|||
|
* - `SelectorEnds`
|
|||
|
* - `SelectorEndsLike`
|
|||
|
* - `SelectorBitwiseAnd`
|
|||
|
*
|
|||
|
*
|
|||
|
* #pw-body
|
|||
|
*
|
|||
|
* @property array $fields Fields that were present in selector (same as $field, but always an array).
|
|||
|
* @property string|array $field Field or fields present in the selector (string if single, or array of strings if multiple). Preferable to use $fields property instead.
|
|||
|
* @property-read string $operator Operator used by the selector.
|
|||
|
* @property array $values Values that were present in selector (same as $value, but always array).
|
|||
|
* @property string|array $value Value or values present in the selector (string if single, or array of strings if multiple). Preferable to use $values property instead.
|
|||
|
* @property bool $not Is this a NOT selector? Indicates the selector returns the opposite if what it would otherwise. #pw-group-properties
|
|||
|
* @property string|null $group Group name for this selector (if field was prepended with a "group_name@"). #pw-group-properties
|
|||
|
* @property string $quote Type of quotes value was in, or blank if it was not quoted. One of: '"[{( #pw-group-properties
|
|||
|
* @property-read string $str String value of selector, i.e. “a=b”. #pw-group-properties
|
|||
|
* @property null|bool $forceMatch When boolean, forces match (true) or force non-match (false). (default=null) #pw-group-properties
|
|||
|
* @property array $altOperators Alternate operators to use when primary fails match, supported only by compareTypeFind. Since 3.0.161 (default=[]) #pw-group-properties
|
|||
|
*
|
|||
|
*/
|
|||
|
abstract class Selector extends WireData {
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Exact (value equals this or value does not equal this)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeExact = 1;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Sort (matches based on how it would sort among given value)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeSort = 2;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Find (text value is found within another text value)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeFind = 4;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Like (text value is like another, combined with compareTypeFind)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeLike = 8;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Bitwise
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeBitwise = 16;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Expand (value can be expanded to include other results when supported)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeExpand = 32;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Command (value can contain additional commands interpreted by the Selector)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeCommand = 64;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Database (Selector is only applicable for database-driven comparisons)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeDatabase = 128;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Fulltext index required when used with database queries
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeFulltext = 256;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Perform phrase match (1+ words in order)
|
|||
|
*/
|
|||
|
const compareTypePhrase = 512;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Match as words independent of order (opposite of phrase)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeWords = 1024;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Partial matches allowed, such as partial words or phrases
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypePartial = 2048;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: If multiple items in query, ANY of them may match
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeAny = 4096;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: If multiple items in query, ALL of them may match
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeAll = 8192;
|
|||
|
|
|||
|
/**
|
|||
|
* Comparison type: Matches at boundary (start or end)
|
|||
|
*
|
|||
|
*/
|
|||
|
const compareTypeBoundary = 16384;
|
|||
|
|
|||
|
/**
|
|||
|
* Given a field name and value, construct the Selector.
|
|||
|
*
|
|||
|
* If the provided $field is an array or pipe "|" separated string, Selector may match any of them (OR field condition)
|
|||
|
* If the provided $value is an array of pipe "|" separated string, Selector may match any one of them (OR value condition).
|
|||
|
*
|
|||
|
* If only one field is provided as a string, and that field is prepended by an exclamation point, i.e. !field=something
|
|||
|
* then the condition is reversed.
|
|||
|
*
|
|||
|
* @param string|array $field
|
|||
|
* @param string|int|array $value
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __construct($field, $value) {
|
|||
|
$this->set('not', false);
|
|||
|
$this->set('group', null); // group name identified with 'group_name@' before a field name
|
|||
|
$this->set('quote', ''); // if $value in quotes, this contains either: ', ", [, {, or (, indicating quote type (set by Selectors class)
|
|||
|
$this->set('forceMatch', null); // boolean true to force match, false to force non-match
|
|||
|
parent::set('altOperators', array()); // optional alternate operators
|
|||
|
$this->setField($field);
|
|||
|
$this->setValue($value);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the operator used by this Selector
|
|||
|
*
|
|||
|
* @return string
|
|||
|
* @since 3.0.42 Prior versions just supported the 'operator' property.
|
|||
|
*
|
|||
|
*/
|
|||
|
public function operator() {
|
|||
|
return static::getOperator();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get the field(s) of this Selector
|
|||
|
*
|
|||
|
* Note that if calling this as a property (rather than a method) it can return either a string or an array.
|
|||
|
*
|
|||
|
* @param bool|int $forceString Specify one of the following:
|
|||
|
* - `true` (bool): to only return a string, where multiple-fields will be split by pipe "|". (default)
|
|||
|
* - `false` (bool): to return string if 1 field, or array of multiple fields (same behavior as field property).
|
|||
|
* - `1` (int): to return only the first value (string).
|
|||
|
* @return string|array|null
|
|||
|
* @since 3.0.42 Prior versions only supported the 'field' property.
|
|||
|
* @see Selector::fields()
|
|||
|
*
|
|||
|
*/
|
|||
|
public function field($forceString = true) {
|
|||
|
$field = parent::get('field');
|
|||
|
if($forceString && is_array($field)) {
|
|||
|
if($forceString === 1) {
|
|||
|
$field = reset($field);
|
|||
|
} else {
|
|||
|
$field = implode('|', $field);
|
|||
|
}
|
|||
|
}
|
|||
|
return $field;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return array of field(s) for this Selector
|
|||
|
*
|
|||
|
* @return array
|
|||
|
* @see Selector::field()
|
|||
|
* @since 3.0.42 Prior versions just supported the 'fields' property.
|
|||
|
*
|
|||
|
*/
|
|||
|
public function fields() {
|
|||
|
$field = parent::get('field');
|
|||
|
if(is_array($field)) return $field;
|
|||
|
if(!strlen($field)) return array();
|
|||
|
return array($field);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get the value(s) of this Selector
|
|||
|
*
|
|||
|
* Note that if calling this as a property (rather than a method) it can return either a string or an array.
|
|||
|
*
|
|||
|
* @param bool|int $forceString Specify one of the following:
|
|||
|
* - `true` (bool): to only return a string, where multiple-values will be split by pipe "|". (default)
|
|||
|
* - `false` (bool): to return string if 1 value, or array of multiple values (same behavior as value property).
|
|||
|
* - `1` (int): to return only the first value (string).
|
|||
|
* @return string|array|null
|
|||
|
* @since 3.0.42 Prior versions only supported the 'value' property.
|
|||
|
* @see Selector::values()
|
|||
|
*
|
|||
|
*/
|
|||
|
public function value($forceString = true) {
|
|||
|
$value = parent::get('value');
|
|||
|
if($forceString && is_array($value)) {
|
|||
|
if($forceString === 1) {
|
|||
|
$value = reset($value);
|
|||
|
} else {
|
|||
|
$value = $this->wire('sanitizer')->selectorValue($value);
|
|||
|
}
|
|||
|
}
|
|||
|
return $value;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return array of value(s) for this Selector
|
|||
|
*
|
|||
|
* @param bool $nonEmpty If empty array will be returned, forces it to return array with one blank item instead (default=false).
|
|||
|
* @return array
|
|||
|
* @see Selector::value()
|
|||
|
* @since 3.0.42 Prior versions just supported the 'values' property.
|
|||
|
*
|
|||
|
*/
|
|||
|
public function values($nonEmpty = false) {
|
|||
|
$values = parent::get('value');
|
|||
|
if(is_array($values)) {
|
|||
|
// ok
|
|||
|
} else if(is_string($values)) {
|
|||
|
$values = strlen($values) ? array($values) : array();
|
|||
|
} else if(is_object($values)) {
|
|||
|
$values = $values instanceof WireArray ? $values->getArray() : array($values);
|
|||
|
} else if($values) {
|
|||
|
$values = array($values);
|
|||
|
} else {
|
|||
|
$values = array();
|
|||
|
}
|
|||
|
if($nonEmpty && !count($values)) $values = array('');
|
|||
|
return $values;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get a property
|
|||
|
*
|
|||
|
* @param string $key Property name
|
|||
|
* @return array|mixed|null|string Property value
|
|||
|
*
|
|||
|
*/
|
|||
|
public function get($key) {
|
|||
|
if($key === 'operator') return $this->operator();
|
|||
|
if($key === 'str') return $this->__toString();
|
|||
|
if($key === 'values') return $this->values();
|
|||
|
if($key === 'fields') return $this->fields();
|
|||
|
if($key === 'label') return $this->getLabel();
|
|||
|
return parent::get($key);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the selector field(s), optionally forcing as string or array
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param string $type Omit for automatic, or specify 'string' or 'array' to force return in that type
|
|||
|
* @return string|array
|
|||
|
* @throws WireException if given invalid type
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getField($type = '') {
|
|||
|
$field = $this->field;
|
|||
|
if($type == 'string') {
|
|||
|
if(is_array($field)) $field = implode('|', $field);
|
|||
|
} else if($type == 'array') {
|
|||
|
if(!is_array($field)) $field = array($field);
|
|||
|
} else if($type) {
|
|||
|
throw new WireException("Unknown type '$type' specified to getField()");
|
|||
|
}
|
|||
|
return $field;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set field or fields
|
|||
|
*
|
|||
|
* @param string|array $field
|
|||
|
* @return self
|
|||
|
* @since 3.0.160
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setField($field) {
|
|||
|
if(is_array($field)) $field = implode('|', $field);
|
|||
|
$field = (string) $field;
|
|||
|
$not = strpos($field, '!') === 0;
|
|||
|
if($not) $field = ltrim($field, '!');
|
|||
|
if(strpos($field, '|') !== false) $field = explode('|', $field);
|
|||
|
parent::set('field', $field);
|
|||
|
parent::set('not', $not);
|
|||
|
return $this;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Returns the selector value(s) with additional processing and forced type options
|
|||
|
*
|
|||
|
* When the $type argument is not specified, this method may return a string, array or Selectors object.
|
|||
|
* A Selectors object is only returned if the value happens to contain an embedded selector.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param string $type Omit for automatic, or specify 'string' or 'array' to force return in that type
|
|||
|
* @return string|array|Selectors
|
|||
|
* @throws WireException if given invalid type
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getValue($type = '') {
|
|||
|
$value = $this->value;
|
|||
|
if($type == 'string') {
|
|||
|
if(is_array($value)) $value = $this->wire('sanitizer')->selectorValue($value);
|
|||
|
} else if($type == 'array') {
|
|||
|
if(!is_array($value)) $value = array($value);
|
|||
|
} else if($this->quote == '[') {
|
|||
|
if(is_string($value) && Selectors::stringHasSelector($value)) {
|
|||
|
$value = $this->wire(new Selectors($value));
|
|||
|
} else if($value instanceof Selectors) {
|
|||
|
// okay
|
|||
|
}
|
|||
|
} else if($type) {
|
|||
|
throw new WireException("Unknown type '$type' specified to getValue()");
|
|||
|
}
|
|||
|
return $value;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set selector value(s)
|
|||
|
*
|
|||
|
* @param string|int|array|mixed $value
|
|||
|
* @return self
|
|||
|
* @since 3.0.160
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setValue($value) {
|
|||
|
parent::set('value', $value);
|
|||
|
return $this;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set a property of the Selector
|
|||
|
*
|
|||
|
* @param string $key
|
|||
|
* @param mixed $value
|
|||
|
* @return Selector|WireData
|
|||
|
*
|
|||
|
*/
|
|||
|
public function set($key, $value) {
|
|||
|
if($key === 'fields' || $key === 'field') return $this->setField($value);
|
|||
|
if($key === 'values' || $key === 'value') return $this->setValue($value);
|
|||
|
if($key === 'operator') {
|
|||
|
$this->error("You cannot set the operator on a Selector: $this");
|
|||
|
return $this;
|
|||
|
}
|
|||
|
if($key === 'altOperators') {
|
|||
|
if(!is_array($value)) $value = array();
|
|||
|
$operator = $this->operator();
|
|||
|
foreach($value as $k => $v) {
|
|||
|
// don’t allow current operator to be an altOperator
|
|||
|
if($operator === $v) unset($value[$k]);
|
|||
|
}
|
|||
|
}
|
|||
|
return parent::set($key, $value);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the operator used by this Selector
|
|||
|
*
|
|||
|
* Strict standards don't let us make static abstract methods, so this one throws an exception if it's not reimplemented.
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @return string
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function getOperator() {
|
|||
|
throw new WireException("This getOperator method must be implemented");
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* What type of comparson does Selector perform?
|
|||
|
*
|
|||
|
* @return int Returns a Selector::compareType* constant or 0 if not defined
|
|||
|
* @since 3.0.154
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function getCompareType() {
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
/*
|
|||
|
public static function getCompareTypeArray() {
|
|||
|
$types = array(
|
|||
|
self::compareTypeExact => 'exact',
|
|||
|
self::compareTypeSort => 'sort',
|
|||
|
self::compareTypeFind => 'find',
|
|||
|
self::compareTypeLike => 'like',
|
|||
|
self::compareTypeBitwise => 'bitwise',
|
|||
|
self::compareTypeExpand => 'expand',
|
|||
|
self::compareTypeCommand => 'command',
|
|||
|
self::compareTypeDatabase => 'database',
|
|||
|
self::compareTypeFulltext => 'fulltext',
|
|||
|
self::compareTypePhrase => 'phrase',
|
|||
|
self::compareTypeWords => 'words',
|
|||
|
self::compareTypePartial => 'partial',
|
|||
|
self::compareTypeAny => 'any',
|
|||
|
self::compareTypeAll => 'all',
|
|||
|
self::compareTypeBoundary => 'boundary',
|
|||
|
);
|
|||
|
$compareType = self::getCompareType();
|
|||
|
$compareTypes = array();
|
|||
|
foreach($types as $flag => $type) {
|
|||
|
if($compareType & $flag;
|
|||
|
}
|
|||
|
}
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Get short label that describes this Selector
|
|||
|
*
|
|||
|
* @return string
|
|||
|
* @since 3.0.160
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function getLabel() {
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get longer description that describes this Selector
|
|||
|
*
|
|||
|
* @return string
|
|||
|
* @since 3.0.160
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function getDescription() {
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Does $value1 match $value2?
|
|||
|
*
|
|||
|
* @param mixed $value1 Dynamic comparison value
|
|||
|
* @param string $value2 User-supplied value to compare against
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
abstract protected function match($value1, $value2);
|
|||
|
|
|||
|
/**
|
|||
|
* Does this Selector match the given value?
|
|||
|
*
|
|||
|
* If the value held by this Selector is an array of values, it will check if any one of them matches the value supplied here.
|
|||
|
*
|
|||
|
* @param string|int|Wire|array $value If given a Wire, then matches will also operate on OR field=value type selectors, where present
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
public function matches($value) {
|
|||
|
|
|||
|
$forceMatch = $this->get('forceMatch');
|
|||
|
if(is_bool($forceMatch)) return $forceMatch;
|
|||
|
|
|||
|
$matches = false;
|
|||
|
$values1 = is_array($this->value) ? $this->value : array($this->value);
|
|||
|
$field = $this->field;
|
|||
|
$operator = $this->operator();
|
|||
|
|
|||
|
// prepare the value we are comparing
|
|||
|
if(is_object($value)) {
|
|||
|
if($this->wire('languages') && $value instanceof LanguagesValueInterface) $value = (string) $value;
|
|||
|
else if($value instanceof WireData) $value = $value->get($field);
|
|||
|
else if($value instanceof WireArray && is_string($field) && !strpos($field, '.')) $value = (string) $value; // 123|456|789, etc.
|
|||
|
else if($value instanceof Wire) $value = $value->$field;
|
|||
|
$value = (string) $value;
|
|||
|
}
|
|||
|
|
|||
|
if(is_string($value) && strpos($value, '|') !== false) $value = explode('|', $value);
|
|||
|
if(!is_array($value)) $value = array($value);
|
|||
|
$values2 = $value;
|
|||
|
unset($value);
|
|||
|
|
|||
|
// now we're just dealing with 2 arrays: $values1 and $values2
|
|||
|
// $values1 is the value stored by the selector
|
|||
|
// $values2 is the value passed into the matches() function
|
|||
|
|
|||
|
$numMatches = 0;
|
|||
|
$numMatchesRequired = 1;
|
|||
|
if(($operator === '!=' && !$this->not) || ($this->not && $operator !== '!=')) {
|
|||
|
$numMatchesRequired = count($values1) * count($values2);
|
|||
|
}
|
|||
|
|
|||
|
$fields = is_array($field) ? $field : array($field);
|
|||
|
|
|||
|
foreach($fields as $field) {
|
|||
|
|
|||
|
foreach($values1 as $v1) {
|
|||
|
|
|||
|
if(is_object($v1)) {
|
|||
|
if($v1 instanceof WireData) $v1 = $v1->get($field);
|
|||
|
else if($v1 instanceof Wire) $v1 = $v1->$field;
|
|||
|
}
|
|||
|
|
|||
|
foreach($values2 as $v2) {
|
|||
|
if(empty($v2) && empty($v1)) {
|
|||
|
// normalize empty values so that they will match if both considered "empty"
|
|||
|
$v2 = '';
|
|||
|
$v1 = '';
|
|||
|
}
|
|||
|
if($this->match($v2, $v1)) {
|
|||
|
$numMatches++;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($numMatches >= $numMatchesRequired) {
|
|||
|
$matches = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if($matches) break;
|
|||
|
}
|
|||
|
|
|||
|
return $matches;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Provides the opportunity to override or NOT the condition
|
|||
|
*
|
|||
|
* Selectors should include a call to this in their matches function
|
|||
|
*
|
|||
|
* @param bool $matches
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function evaluate($matches) {
|
|||
|
$forceMatch = $this->get('forceMatch');
|
|||
|
if(is_bool($forceMatch)) $matches = $forceMatch;
|
|||
|
if($this->not) return !$matches;
|
|||
|
return $matches;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Copy all data from this selector to another
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @param Selector $selector
|
|||
|
* @since 3.0.161
|
|||
|
*
|
|||
|
*/
|
|||
|
public function copyTo(Selector $selector) {
|
|||
|
$selector->setField($this->field);
|
|||
|
$selector->setValue($this->value);
|
|||
|
$selector->not = $this->not;
|
|||
|
if($this->group) $selector->group = $this->group;
|
|||
|
if($this->quote) $selector->quote = $this->quote;
|
|||
|
if(is_bool($this->forceMatch)) $selector->forceMatch = $this->forceMatch;
|
|||
|
if(count($this->altOperators)) $selector->altOperators = $this->altOperators;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Sanitize field name
|
|||
|
*
|
|||
|
* @param string|array $fieldName
|
|||
|
* @return string|array
|
|||
|
* @todo This needs testing and then to be used by this class
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function sanitizeFieldName($fieldName) {
|
|||
|
if(strpos($fieldName, '|') !== false) {
|
|||
|
$fieldName = explode('|', $fieldName);
|
|||
|
}
|
|||
|
if(is_array($fieldName)) {
|
|||
|
$fieldNames = array();
|
|||
|
foreach($fieldName as $name) {
|
|||
|
$name = $this->sanitizeFieldName($name);
|
|||
|
if($name !== '') $fieldNames[] = $name;
|
|||
|
}
|
|||
|
return $fieldNames;
|
|||
|
}
|
|||
|
$fieldName = trim($fieldName, '. ');
|
|||
|
if($fieldName === '') return $fieldName;
|
|||
|
if(ctype_alnum($fieldName)) return $fieldName;
|
|||
|
if(ctype_alnum(str_replace(array('.', '_'), '', $fieldName))) return $fieldName;
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* The string value of Selector is always the selector string that it originated from
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __toString() {
|
|||
|
|
|||
|
$openingQuote = $this->quote;
|
|||
|
$closingQuote = $openingQuote;
|
|||
|
|
|||
|
if($openingQuote) {
|
|||
|
if($openingQuote == '[') $closingQuote = ']';
|
|||
|
else if($openingQuote == '{') $closingQuote = '}';
|
|||
|
else if($openingQuote == '(') $closingQuote = ')';
|
|||
|
}
|
|||
|
|
|||
|
$value = $this->value();
|
|||
|
if($openingQuote) $value = trim($value, $openingQuote . $closingQuote);
|
|||
|
$value = $openingQuote . $value . $closingQuote;
|
|||
|
|
|||
|
$str =
|
|||
|
($this->not ? '!' : '') .
|
|||
|
(is_null($this->group) ? '' : $this->group . '@') .
|
|||
|
(is_array($this->field) ? implode('|', $this->field) : $this->field) .
|
|||
|
$this->operator() . $value;
|
|||
|
|
|||
|
return $str;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Debug info
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
* @return array
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __debugInfo() {
|
|||
|
$info = array(
|
|||
|
'field' => $this->field,
|
|||
|
'operator' => $this->operator,
|
|||
|
'value' => $this->value,
|
|||
|
);
|
|||
|
if($this->not) $info['not'] = true;
|
|||
|
if($this->forceMatch) $info['forceMatch'] = true;
|
|||
|
if($this->group) $info['group'] = $this->group;
|
|||
|
if($this->quote) $info['quote'] = $this->quote;
|
|||
|
$info['string'] = $this->__toString();
|
|||
|
return $info;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Add all individual selector types to the runtime Selectors
|
|||
|
*
|
|||
|
* #pw-internal
|
|||
|
*
|
|||
|
*/
|
|||
|
static public function loadSelectorTypes() {
|
|||
|
$types = array(
|
|||
|
'Equal',
|
|||
|
'NotEqual',
|
|||
|
'GreaterThan',
|
|||
|
'LessThan',
|
|||
|
'GreaterThanEqual',
|
|||
|
'LessThanEqual',
|
|||
|
'Contains',
|
|||
|
'ContainsLike',
|
|||
|
'ContainsWords',
|
|||
|
'ContainsWordsPartial',
|
|||
|
'ContainsWordsLive',
|
|||
|
'ContainsWordsLike',
|
|||
|
'ContainsWordsExpand',
|
|||
|
'ContainsAnyWords',
|
|||
|
'ContainsAnyWordsPartial',
|
|||
|
'ContainsAnyWordsLike',
|
|||
|
'ContainsAnyWordsExpand',
|
|||
|
'ContainsExpand',
|
|||
|
'ContainsMatch',
|
|||
|
'ContainsMatchExpand',
|
|||
|
'ContainsAdvanced',
|
|||
|
'Starts',
|
|||
|
'StartsLike',
|
|||
|
'Ends',
|
|||
|
'EndsLike',
|
|||
|
'BitwiseAnd',
|
|||
|
);
|
|||
|
foreach($types as $type) {
|
|||
|
$class = "Selector$type";
|
|||
|
/** @var Selector $className */
|
|||
|
$className = __NAMESPACE__ . "\\$class";
|
|||
|
$operator = $className::getOperator();
|
|||
|
Selectors::addType($operator, $class);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches equality between two values
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorEqual extends Selector {
|
|||
|
public static function getOperator() { return '='; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeExact; }
|
|||
|
public static function getLabel() { return __('Equals', __FILE__); }
|
|||
|
public static function getDescription() { return __('Given value is the same as value compared to.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 == $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches two values that aren't equal
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorNotEqual extends Selector {
|
|||
|
public static function getOperator() { return '!='; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeExact; }
|
|||
|
public static function getLabel() { return __('Not equals', __FILE__); }
|
|||
|
public static function getDescription() { return __('Given value is not the same as value compared to.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 != $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one value greater than another
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorGreaterThan extends Selector {
|
|||
|
public static function getOperator() { return '>'; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeSort; }
|
|||
|
public static function getLabel() { return __('Greater than', __FILE__); }
|
|||
|
public static function getDescription() { return __('Compared value is greater than given value.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 > $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one value less than another
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorLessThan extends Selector {
|
|||
|
public static function getOperator() { return '<'; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeSort; }
|
|||
|
public static function getLabel() { return __('Less than', __FILE__); }
|
|||
|
public static function getDescription() { return __('Compared value is less than given value.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 < $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one value greater than or equal to another
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorGreaterThanEqual extends Selector {
|
|||
|
public static function getOperator() { return '>='; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeSort; }
|
|||
|
public static function getLabel() { return __('Greater than or equal', __FILE__); }
|
|||
|
public static function getDescription() { return __('Compared value is greater than or equal to given value.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 >= $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one value less than or equal to another
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorLessThanEqual extends Selector {
|
|||
|
public static function getOperator() { return '<='; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeSort; }
|
|||
|
public static function getLabel() { return __('Less than or equal', __FILE__); }
|
|||
|
public static function getDescription() { return __('Compared value is less than or equal to given value.', __FILE__); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate($value1 <= $value2); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one string value (phrase) that happens to be present in another string value
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContains extends Selector {
|
|||
|
public static function getOperator() { return '*='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains phrase', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$matches = stripos($value1, $value2) !== false && preg_match('/\b' . preg_quote($value2) . '/i', $value1);
|
|||
|
return $this->evaluate($matches);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Build description from predefined keys for SelectorContains* classes
|
|||
|
*
|
|||
|
* @param array|string $keys
|
|||
|
* @return string
|
|||
|
*
|
|||
|
*/
|
|||
|
public static function buildDescription($keys) {
|
|||
|
$a = array();
|
|||
|
if(!is_array($keys)) $keys = explode(' ', $keys);
|
|||
|
foreach($keys as $key) {
|
|||
|
switch($key) {
|
|||
|
case 'text': $a[] = __('Given text appears in value compared to.', __FILE__); break;
|
|||
|
case 'phrase': $a[] = __('Given phrase or word appears in value compared to.', __FILE__); break;
|
|||
|
case 'phrase-start': $a[] = __('Given word or phrase appears at beginning of compared value.', __FILE__); break;
|
|||
|
case 'phrase-end': $a[] = __('Given word or phrase appears at end of compared value.', __FILE__); break;
|
|||
|
case 'expand': $a[] = __('Expand to include potentially related terms and word variations.', __FILE__); break;
|
|||
|
case 'words-all': $a[] = __('All given words appear in compared value, in any order.', __FILE__); break;
|
|||
|
case 'words-any': $a[] = __('Any given words appear in compared value, in any order.', __FILE__); break;
|
|||
|
case 'words-match': $a[] = __('Any given words match against compared value.', __FILE__); break;
|
|||
|
case 'words-whole': $a[] = __('Matches whole words.', __FILE__); break;
|
|||
|
case 'words-partial': $a[] = __('Matches whole or partial words.', __FILE__); break;
|
|||
|
case 'words-partial-any': $a[] = __('Partial matches anywhere within words.', __FILE__); break;
|
|||
|
case 'words-partial-begin': $a[] = __('Partial matches from beginning of words.', __FILE__); break;
|
|||
|
case 'words-partial-last': $a[] = __('Partial matches last word in given value.', __FILE__); break;
|
|||
|
case 'fulltext': $a[] = __('Uses “fulltext” index.', __FILE__); break;
|
|||
|
case 'like': $a[] = __('Matches using “like”.', __FILE__); break;
|
|||
|
case 'like-words': $a[] = __('Matches without regard to word boundaries (using “like”).', __FILE__); break;
|
|||
|
default: $a[] = "UNKNOWN:$key";
|
|||
|
}
|
|||
|
}
|
|||
|
return implode(' ', $a);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Same as SelectorContains but query expansion when used for database searching
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsExpand extends SelectorContains {
|
|||
|
public static function getOperator() { return '*+='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeExpand |
|
|||
|
Selector::compareTypeDatabase |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains phrase expand', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase expand fulltext'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Same as SelectorContains but serves as operator placeholder for SQL LIKE operations
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsLike extends SelectorContains {
|
|||
|
public static function getOperator() { return '%='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeLike;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains text like', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase like'); }
|
|||
|
protected function match($value1, $value2) { return $this->evaluate(stripos($value1, $value2) !== false); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches one string value that happens to have all of its words present in another string value (regardless of individual word location)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsWords extends Selector {
|
|||
|
public static function getOperator() { return '~='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains all words', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-all words-whole fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAll = true;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) if(!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
|
|||
|
$hasAll = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
return $this->evaluate($hasAll);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches all given words in whole or in part starting with
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsWordsPartial extends Selector {
|
|||
|
public static function getOperator() { return '~*='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains all partial words', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-all words-partial words-partial-begin fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAll = true;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(!preg_match('/\b' . preg_quote($word) . '/i', $value1)) {
|
|||
|
$hasAll = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
return $this->evaluate($hasAll);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches partial words at either beginning or ending
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsWordsLike extends Selector {
|
|||
|
public static function getOperator() { return '~%='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeLike;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains all words like', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-all words-partial words-partial-any like'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAll = true;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(stripos($value1, $word) === false) {
|
|||
|
$hasAll = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
return $this->evaluate($hasAll);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches entire words except for last word, which must start with
|
|||
|
*
|
|||
|
* Useful in matching "live" search results where someone is typing and last word may be partial.
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsWordsLive extends Selector {
|
|||
|
public static function getOperator() { return '~~='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains all words live', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-all words-partial-last fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAll = true;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
$lastWord = array_pop($words);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
|
|||
|
// full-word match
|
|||
|
$hasAll = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
// last word only needs to match beginning of word
|
|||
|
$hasAll = $hasAll && preg_match('\b' . preg_quote($lastWord) . '/i', $value1);
|
|||
|
return $this->evaluate($hasAll);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches all words with query expansion
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsWordsExpand extends SelectorContainsWords {
|
|||
|
public static function getOperator() { return '~+='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeExpand |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains all words expand', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-all words-whole expand fulltext'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that has any of the given whole words (only 1 needs to match)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsAnyWords extends Selector {
|
|||
|
public static function getOperator() { return '~|='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains any words', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-any words-whole fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAny = false;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(stripos($value1, $word) !== false) {
|
|||
|
if(preg_match('!\b' . preg_quote($word) . '\b!i', $value1)) {
|
|||
|
$hasAny = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return $this->evaluate($hasAny);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that has any of the given partial words (starting with, only 1 needs to match)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsAnyWordsPartial extends Selector {
|
|||
|
public static function getOperator() { return '~|*='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains any partial words', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-any words-partial words-partial-begin fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAny = false;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(stripos($value1, $word) !== false) {
|
|||
|
if(preg_match('!\b' . preg_quote($word) . '!i', $value1)) {
|
|||
|
$hasAny = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return $this->evaluate($hasAny);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that has any words like any of those given (only 1 needs to match)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsAnyWordsLike extends Selector {
|
|||
|
public static function getOperator() { return '~|%='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypePartial |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeLike;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains any words like', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-any words-partial words-partial-any like'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAny = false;
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $key => $word) {
|
|||
|
if(stripos($value1, $word) !== false) {
|
|||
|
$hasAny = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
return $this->evaluate($hasAny);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches any words with query expansion
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsAnyWordsExpand extends SelectorContainsAnyWords {
|
|||
|
public static function getOperator() { return '~|+='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeExpand |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains any words expand', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-any expand fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$hasAny = false;
|
|||
|
$textTools = $this->wire()->sanitizer->getTextTools();
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value2);
|
|||
|
foreach($words as $word) {
|
|||
|
if(stripos($value1, $word) !== false && preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
|
|||
|
$hasAny = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
$alternates = $textTools->getWordAlternates($word);
|
|||
|
foreach($alternates as $alternate) {
|
|||
|
if(stripos($value1, $alternate) && preg_match('/\b' . preg_quote($alternate) . '\b/i', $value1)) {
|
|||
|
$hasAny = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if($hasAny) break;
|
|||
|
}
|
|||
|
return $this->evaluate($hasAny);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that uses standard MySQL MATCH/AGAINST behavior with implied DB-score sorting
|
|||
|
*
|
|||
|
* This selector is only useful for database $pages->find() queries.
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsMatch extends SelectorContainsAnyWords {
|
|||
|
public static function getOperator() { return '**='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeDatabase |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains match', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-match words-whole fulltext'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that uses standard MySQL MATCH/AGAINST behavior with implied DB-score sorting
|
|||
|
*
|
|||
|
* This selector is only useful for database $pages->find() queries.
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsMatchExpand extends SelectorContainsMatch {
|
|||
|
public static function getOperator() { return '**+='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypeExpand |
|
|||
|
Selector::compareTypeDatabase |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Contains match expand', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('words-match words-whole expand fulltext'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector for advanced text searches that interprets specific search commands
|
|||
|
*
|
|||
|
* - `foo` Optional word has no prefix.
|
|||
|
* - `+foo` Required word has a "+" prefix.
|
|||
|
* - `+foo*` Required words starting with "foo" (i.e. "fool", "foobar", etc.) has "+" prefix and "*" wildcard suffix.
|
|||
|
* - `-bar` Disallowed word has a "-" prefix.
|
|||
|
* - `-bar*` Disallowed words starting with "bar" (i.e. "barn", "barbell", etc.) has "-" prefix and "*" wildcard suffix.
|
|||
|
* - `"foo bar baz"` Optional phrase surrounded in quotes.
|
|||
|
* - `+"foo bar baz"` Required phrase with "+" prefix followed by double-quoted value.
|
|||
|
* - `-"foo bar baz"` Disallowed phrase with "-" prefix followed by double-quoted value.
|
|||
|
*
|
|||
|
* Note that to designate a phrase, it must be in "double quotes" (not 'single quotes').
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorContainsAdvanced extends SelectorContains {
|
|||
|
public static function getOperator() { return '#='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAny |
|
|||
|
Selector::compareTypeWords |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeCommand |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Advanced text search', __FILE__); }
|
|||
|
public static function getDescription() {
|
|||
|
return
|
|||
|
__('Match values with commands: +Word MUST appear, -Word MUST NOT appear, and unprefixed Word may appear.', __FILE__) . ' ' .
|
|||
|
__('Add asterisk for partial match: Bar* or +Bar* matches bar, barn, barge; while -Bar* prevents matching them.') . ' ' .
|
|||
|
__('Use quotes to match phrases: +(Must Match), -(Must Not Match), or (May Match).');
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return array of advanced search commands from given value
|
|||
|
*
|
|||
|
* @param string $value
|
|||
|
* @return array
|
|||
|
*
|
|||
|
*/
|
|||
|
public function valueToCommands($value) {
|
|||
|
$commands = array();
|
|||
|
$hasQuotes = strrpos($value, '"') || strrpos($value, '”') || strrpos($value, ')') || strrpos($value, '}');
|
|||
|
$substr = function_exists('\\mb_substr') ? '\\mb_substr' : '\\substr';
|
|||
|
$re = '/[-+]?("[^"]+"|\([^)]+\))\*?/';
|
|||
|
if($hasQuotes && preg_match_all($re, $value, $matches)) {
|
|||
|
// find all quoted phrases
|
|||
|
foreach($matches[0] as $key => $fullMatch) {
|
|||
|
$type = substr($fullMatch, 0, 1);
|
|||
|
$partial = substr($fullMatch, -1) === '*';
|
|||
|
if($type !== '+' && $type !== '-') $type = '';
|
|||
|
$phrase = $matches[1][$key];
|
|||
|
$phrase = trim($phrase, $substr($phrase, 0, 1) . $substr($phrase, -1)); // remove quotes
|
|||
|
$phrase = str_replace('+', '', trim($phrase, '-'));
|
|||
|
if(strpos($phrase, '-') !== false) $phrase = preg_replace('/([^\w\d])-(.)/', '$1 $2', $phrase);
|
|||
|
$value = str_replace($fullMatch, ' ', $value);
|
|||
|
while(strpos($phrase, ' ') !== false) $phrase = str_replace(' ', ' ', $phrase);
|
|||
|
if(!strlen($phrase)) continue;
|
|||
|
$phrase = str_replace('"', '', $phrase);
|
|||
|
$query = $type . '"' . $phrase . '"' . ($partial ? '*' : '');
|
|||
|
$a = array('type' => $type, 'value' => $phrase, 'query' => $query, 'partial' => $partial, 'phrase' => true);
|
|||
|
$commands[] = $a;
|
|||
|
}
|
|||
|
}
|
|||
|
$words = $this->wire()->sanitizer->wordsArray($value, array(
|
|||
|
'keepChars' => array('+', '-', '*')
|
|||
|
));
|
|||
|
foreach($words as $key => $word) {
|
|||
|
$type = substr($word, 0, 1);
|
|||
|
$partial = substr($word, -1) === '*';
|
|||
|
if($type !== '+' && $type !== '-') $type = '';
|
|||
|
$word = trim($word, '+-*');
|
|||
|
$query = $type . $word . ($partial ? '*' : '');
|
|||
|
$a = array('type' => $type, 'value' => $word, 'query' => $query, 'partial' => $partial, 'phrase' => false);
|
|||
|
$commands[] = $a;
|
|||
|
}
|
|||
|
return $commands;
|
|||
|
}
|
|||
|
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$fail = false;
|
|||
|
$numMatch = 0;
|
|||
|
$numOptional = 0;
|
|||
|
$commands = $this->valueToCommands($value2);
|
|||
|
foreach($commands as $command) {
|
|||
|
$re = '/\b' . preg_quote($command['value']) . ($command['partial'] ? '' : '\b') . '/i';
|
|||
|
$match = preg_match($re, $value1);
|
|||
|
if($command['type'] === '+') {
|
|||
|
// value must be present (+)
|
|||
|
if(!$match) $fail = true;
|
|||
|
if(!$fail) $numMatch++;
|
|||
|
} else if($command['type'] === '-') {
|
|||
|
// value must not be present (-)
|
|||
|
if($match) $fail = true;
|
|||
|
if(!$fail) $numMatch++;
|
|||
|
} else {
|
|||
|
// value may be present (blank type)
|
|||
|
if($match) $numMatch++;
|
|||
|
$numOptional++;
|
|||
|
}
|
|||
|
if($fail) break;
|
|||
|
}
|
|||
|
if(!$fail && $numOptional && !$numMatch) $fail = true;
|
|||
|
return $this->evaluate(!$fail);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches if the value exists at the beginning of another value
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorStarts extends Selector {
|
|||
|
public static function getOperator() { return '^='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeBoundary |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Starts with', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase-start fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
return $this->evaluate(stripos(trim($value1), $value2) === 0);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches if the value exists at the beginning of another value (specific to SQL LIKE)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorStartsLike extends SelectorStarts {
|
|||
|
public static function getOperator() { return '%^='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeBoundary |
|
|||
|
Selector::compareTypeLike;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Starts like', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase-start like'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches if the value exists at the end of another value
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorEnds extends Selector {
|
|||
|
public static function getOperator() { return '$='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeBoundary |
|
|||
|
Selector::compareTypeFulltext;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Ends with', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase-end fulltext'); }
|
|||
|
protected function match($value1, $value2) {
|
|||
|
$value2 = trim($value2);
|
|||
|
$value1 = substr($value1, -1 * strlen($value2));
|
|||
|
return $this->evaluate(strcasecmp($value1, $value2) == 0);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches if the value exists at the end of another value (specific to SQL LIKE)
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorEndsLike extends SelectorEnds {
|
|||
|
public static function getOperator() { return '%$='; }
|
|||
|
public static function getCompareType() {
|
|||
|
return
|
|||
|
Selector::compareTypeFind |
|
|||
|
Selector::compareTypeAll |
|
|||
|
Selector::compareTypePhrase |
|
|||
|
Selector::compareTypeBoundary |
|
|||
|
Selector::compareTypeLike;
|
|||
|
}
|
|||
|
public static function getLabel() { return __('Ends like', __FILE__); }
|
|||
|
public static function getDescription() { return SelectorContains::buildDescription('phrase-end like'); }
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Selector that matches a bitwise AND '&'
|
|||
|
*
|
|||
|
*/
|
|||
|
class SelectorBitwiseAnd extends Selector {
|
|||
|
public static function getOperator() { return '&'; }
|
|||
|
public static function getCompareType() { return Selector::compareTypeBitwise; }
|
|||
|
public static function getLabel() { return __('Bitwise AND', __FILE__); }
|
|||
|
public static function getDescription() {
|
|||
|
return __('Given integer value matches bitwise AND with compared integer value.', __FILE__);
|
|||
|
}
|
|||
|
protected function match($value1, $value2) { return $this->evaluate(((int) $value1) & ((int) $value2)); }
|
|||
|
}
|
|||
|
|