1427 lines
46 KiB
PHP
1427 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) {
|
||
parent::__construct();
|
||
$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 $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 $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 $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 $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 $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 $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 $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 $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)); }
|
||
}
|
||
|