mirror of
https://github.com/processwire/processwire.git
synced 2025-08-15 03:05:26 +02:00
Add several new text-matching Selector classes and also some refactoring in main Selectors class
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* 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 2016 by Ryan Cramer
|
||||
* 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.
|
||||
@@ -49,12 +49,24 @@
|
||||
* - `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)
|
||||
* - `SelectorContainsAdvance3d` (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).
|
||||
@@ -101,6 +113,24 @@ abstract class Selector extends WireData {
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Given a field name and value, construct the Selector.
|
||||
*
|
||||
@@ -116,15 +146,8 @@ abstract class Selector extends WireData {
|
||||
*/
|
||||
public function __construct($field, $value) {
|
||||
|
||||
$not = false;
|
||||
if(!is_array($field) && isset($field[0]) && $field[0] == '!') {
|
||||
$not = true;
|
||||
$field = ltrim($field, '!');
|
||||
}
|
||||
|
||||
$this->set('field', $field);
|
||||
$this->set('value', $value);
|
||||
$this->set('not', $not);
|
||||
$this->setField($field);
|
||||
$this->setValue($value);
|
||||
$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
|
||||
@@ -242,10 +265,11 @@ abstract class Selector extends WireData {
|
||||
*
|
||||
*/
|
||||
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 === '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);
|
||||
}
|
||||
|
||||
@@ -271,6 +295,25 @@ abstract class Selector extends WireData {
|
||||
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
|
||||
*
|
||||
@@ -302,6 +345,19 @@ abstract class Selector extends WireData {
|
||||
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
|
||||
*
|
||||
@@ -311,9 +367,9 @@ abstract class Selector extends WireData {
|
||||
*
|
||||
*/
|
||||
public function set($key, $value) {
|
||||
if($key == 'fields') return parent::set('field', $value);
|
||||
if($key == 'values') return parent::set('value', $value);
|
||||
if($key == 'operator') {
|
||||
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;
|
||||
}
|
||||
@@ -346,6 +402,17 @@ abstract class Selector extends WireData {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get label that describes this Selector
|
||||
*
|
||||
* @return string
|
||||
* @since 3.0.160
|
||||
*
|
||||
*/
|
||||
public static function getLabel() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Does $value1 match $value2?
|
||||
*
|
||||
@@ -448,6 +515,33 @@ abstract class Selector extends WireData {
|
||||
return $matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -498,6 +592,7 @@ abstract class Selector extends WireData {
|
||||
return $info;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add all individual selector types to the runtime Selectors
|
||||
*
|
||||
@@ -505,20 +600,40 @@ abstract class Selector extends WireData {
|
||||
*
|
||||
*/
|
||||
static public function loadSelectorTypes() {
|
||||
Selectors::addType(SelectorEqual::getOperator(), 'SelectorEqual');
|
||||
Selectors::addType(SelectorNotEqual::getOperator(), 'SelectorNotEqual');
|
||||
Selectors::addType(SelectorGreaterThan::getOperator(), 'SelectorGreaterThan');
|
||||
Selectors::addType(SelectorLessThan::getOperator(), 'SelectorLessThan');
|
||||
Selectors::addType(SelectorGreaterThanEqual::getOperator(), 'SelectorGreaterThanEqual');
|
||||
Selectors::addType(SelectorLessThanEqual::getOperator(), 'SelectorLessThanEqual');
|
||||
Selectors::addType(SelectorContains::getOperator(), 'SelectorContains');
|
||||
Selectors::addType(SelectorContainsLike::getOperator(), 'SelectorContainsLike');
|
||||
Selectors::addType(SelectorContainsWords::getOperator(), 'SelectorContainsWords');
|
||||
Selectors::addType(SelectorStarts::getOperator(), 'SelectorStarts');
|
||||
Selectors::addType(SelectorStartsLike::getOperator(), 'SelectorStartsLike');
|
||||
Selectors::addType(SelectorEnds::getOperator(), 'SelectorEnds');
|
||||
Selectors::addType(SelectorEndsLike::getOperator(), 'SelectorEndsLike');
|
||||
Selectors::addType(SelectorBitwiseAnd::getOperator(), 'SelectorBitwiseAnd');
|
||||
$types = array(
|
||||
'Equal',
|
||||
'NotEqual',
|
||||
'GreaterThan',
|
||||
'LessThan',
|
||||
'GreaterThanEqual',
|
||||
'LessThanEqual',
|
||||
'Contains',
|
||||
'ContainsLike',
|
||||
'ContainsWords',
|
||||
'ContainsWordsPartial',
|
||||
'ContainsWordsLive',
|
||||
'ContainsWordsLike',
|
||||
'ContainsWordsExpand',
|
||||
'ContainsAnyWords',
|
||||
'ContainsAnyWordsPartial',
|
||||
'ContainsAnyWordsLike',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,6 +644,10 @@ abstract class Selector extends WireData {
|
||||
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 exactly the same as value compared to.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 == $value2); }
|
||||
}
|
||||
|
||||
@@ -539,6 +658,10 @@ class SelectorEqual extends Selector {
|
||||
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 exactly the same as value compared to.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 != $value2); }
|
||||
}
|
||||
|
||||
@@ -549,6 +672,10 @@ class SelectorNotEqual extends Selector {
|
||||
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 __('Given value is greater than compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 > $value2); }
|
||||
}
|
||||
|
||||
@@ -559,6 +686,10 @@ class SelectorGreaterThan extends Selector {
|
||||
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 __('Given value is less than compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 < $value2); }
|
||||
}
|
||||
|
||||
@@ -569,6 +700,10 @@ class SelectorLessThan extends Selector {
|
||||
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 __('Given value is greater than or equal to compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 >= $value2); }
|
||||
}
|
||||
|
||||
@@ -579,6 +714,10 @@ class SelectorGreaterThanEqual extends Selector {
|
||||
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 __('Given value is less than or equal to compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate($value1 <= $value2); }
|
||||
}
|
||||
|
||||
@@ -589,7 +728,27 @@ class SelectorLessThanEqual extends Selector {
|
||||
class SelectorContains extends Selector {
|
||||
public static function getOperator() { return '*='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind; }
|
||||
protected function match($value1, $value2) { return $this->evaluate(stripos($value1, $value2) !== false); }
|
||||
public static function getLabel() { return __('Contains phrase', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given phrase or word appears in value compared to.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) {
|
||||
$matches = stripos($value1, $value2) !== false && preg_match('/\b' . preg_quote($value2) . '/i', $value1);
|
||||
return $this->evaluate($matches);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::compareTypeExpand | Selector::compareTypeDatabase; }
|
||||
public static function getLabel() { return __('Contains phrase + expand', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given phrase, word or related terms appear in value compared to.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,18 +758,27 @@ class SelectorContains extends Selector {
|
||||
class SelectorContainsLike extends SelectorContains {
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; }
|
||||
public static function getOperator() { return '%='; }
|
||||
public static function getLabel() { return __('Contains text like', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given text appears in compared value, without regard to word boundaries.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) { return $this->evaluate(stripos($value1, $value2) !== false); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector that matches one string value that happens to have all of it's words present in another string value (regardless of individual word location)
|
||||
* 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; }
|
||||
public static function getLabel() { return __('Contains entire words', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('All given whole words appear in compared value, in any order.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) {
|
||||
$hasAll = true;
|
||||
$words = preg_split('/[-\s]/', $value2, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$words = $this->wire()->sanitizer->wordsArray($value2);
|
||||
foreach($words as $key => $word) if(!preg_match('/\b' . preg_quote($word) . '\b/i', $value1)) {
|
||||
$hasAll = false;
|
||||
break;
|
||||
@@ -619,6 +787,274 @@ class SelectorContainsWords extends Selector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; }
|
||||
public static function getLabel() { return __('Contains words partial', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('All given partial and whole words appear in compared value, in any order. Partial matches from beginning of words.', __FILE__);
|
||||
}
|
||||
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 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; }
|
||||
public static function getLabel() { return __('Contains words live', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('All given whole words—and at least partial last word—appear in compared value, in any order.', __FILE__);
|
||||
}
|
||||
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 partial words at either beginning or ending
|
||||
*
|
||||
*/
|
||||
class SelectorContainsWordsLike extends Selector {
|
||||
public static function getOperator() { return '~%='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; }
|
||||
public static function getLabel() { return __('Contains words like', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('All given partial or whole words appear in compared value (in any order) without regard to word boundaries.', __FILE__);
|
||||
}
|
||||
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 all words with query expansion
|
||||
*
|
||||
*/
|
||||
class SelectorContainsWordsExpand extends SelectorContainsWords {
|
||||
public static function getOperator() { return '~+='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeExpand; }
|
||||
public static function getLabel() { return __('Contains words + expand', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('All given whole words appear in compared value (in any order) and expand to match related values.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; }
|
||||
public static function getLabel() { return __('Contains any words', __FILE__); }
|
||||
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);
|
||||
}
|
||||
public static function getDescription() {
|
||||
return __('Any of the given whole words appear in compared value.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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; }
|
||||
public static function getLabel() { return __('Contains any partial words', __FILE__); }
|
||||
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);
|
||||
}
|
||||
public static function getDescription() {
|
||||
return __('Any of the given partial or whole words appear in compared value. Partial matches from beginning of words.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getOperator() { return '~|%='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; }
|
||||
public static function getLabel() { return __('Contains any words like', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Any of the given partial or whole words appear in compared value, without regard to word boundaries.', __FILE__);
|
||||
}
|
||||
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 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::compareTypeDatabase; }
|
||||
public static function getLabel() { return __('Contains match', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Any or all of the given words match compared value using default database logic and score.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::compareTypeExpand | Selector::compareTypeDatabase; }
|
||||
public static function getLabel() { return __('Contains match + expand', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return SelectorContainsMatch::getDescription() . ' ' . __('Plus, expands to include potentially related results.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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::compareTypeCommand; }
|
||||
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 (at least one matches).', __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".');
|
||||
}
|
||||
protected function match($value1, $value2) {
|
||||
$fail = false;
|
||||
$numOptionalMatch = 0;
|
||||
$commandMatches = array(
|
||||
'+' => array(),
|
||||
'-' => array(),
|
||||
'?' => array(),
|
||||
);
|
||||
if(strpos($value2, '"') !== false && preg_match_all('![-+]?"([^"]+)"!', $value2, $matches)) {
|
||||
// find all quoted phrases
|
||||
foreach($matches[0] as $key => $fullMatch) {
|
||||
$command = strpos($fullMatch, '"') === 0 ? '?' : substr($value2, 0, 1);
|
||||
$phrase = trim($matches[1][$key]);
|
||||
if(strlen($phrase)) $commandMatches[$command][] = $phrase;
|
||||
$value2 = str_replace($fullMatch, ' ', $value2);
|
||||
}
|
||||
}
|
||||
$words = $this->wire()->sanitizer->wordsArray($value2);
|
||||
foreach($words as $key => $word) {
|
||||
$command = substr($word, 0, 1);
|
||||
$word = ltrim($word, '-+');
|
||||
if($command !== '+' && $command !== '-') $command = '?';
|
||||
$commandMatches[$command][] = $word;
|
||||
}
|
||||
foreach($commandMatches as $command => $items) {
|
||||
foreach($items as $item) {
|
||||
$partial = substr($item, -1) === '*';
|
||||
if($partial) $item = rtrim($item, '*');
|
||||
$re = '!\b' . preg_quote($item) . ($partial ? '!i' : '\b!i');
|
||||
if($command === '+') {
|
||||
if(!preg_match($re, $value1)) $fail = true;
|
||||
} else if($command === '-') {
|
||||
if(preg_match($re, $value1)) $fail = true;
|
||||
} else {
|
||||
if(stripos($value1, $item) !== false) $numOptionalMatch++;
|
||||
}
|
||||
if($fail) break;
|
||||
}
|
||||
if($fail) break;
|
||||
}
|
||||
if(!$fail && count($commandMatches['?']) && !$numOptionalMatch) $fail = true;
|
||||
return $this->evaluate(!$fail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector that matches if the value exists at the beginning of another value
|
||||
*
|
||||
@@ -626,7 +1062,13 @@ class SelectorContainsWords extends Selector {
|
||||
class SelectorStarts extends Selector {
|
||||
public static function getOperator() { return '^='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind; }
|
||||
protected function match($value1, $value2) { return $this->evaluate(stripos(trim($value1), $value2) === 0); }
|
||||
public static function getLabel() { return __('Starts with', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given word or phrase appears at beginning of compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) {
|
||||
return $this->evaluate(stripos(trim($value1), $value2) === 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -636,6 +1078,10 @@ class SelectorStarts extends Selector {
|
||||
class SelectorStartsLike extends SelectorStarts {
|
||||
public static function getOperator() { return '%^='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; }
|
||||
public static function getLabel() { return __('Starts like', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given text appears at beginning of compared value, without regard for word boundaries.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -645,6 +1091,10 @@ class SelectorStartsLike extends SelectorStarts {
|
||||
class SelectorEnds extends Selector {
|
||||
public static function getOperator() { return '$='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind; }
|
||||
public static function getLabel() { return __('Ends with', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given word or phrase appears at end of compared value.', __FILE__);
|
||||
}
|
||||
protected function match($value1, $value2) {
|
||||
$value2 = trim($value2);
|
||||
$value1 = substr($value1, -1 * strlen($value2));
|
||||
@@ -659,6 +1109,10 @@ class SelectorEnds extends Selector {
|
||||
class SelectorEndsLike extends SelectorEnds {
|
||||
public static function getOperator() { return '%$='; }
|
||||
public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; }
|
||||
public static function getLabel() { return __('Ends like', __FILE__); }
|
||||
public static function getDescription() {
|
||||
return __('Given text appears at end of compared value, without regard for word boundaries.', __FILE__);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -668,6 +1122,10 @@ class SelectorEndsLike extends SelectorEnds {
|
||||
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)); }
|
||||
}
|
||||
|
||||
|
@@ -31,9 +31,12 @@ require_once(PROCESSWIRE_CORE_PATH . "Selector.php");
|
||||
* @link https://processwire.com/api/selectors/ Official Selectors Documentation
|
||||
* @method Selector[] getIterator()
|
||||
*
|
||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||
* https://processwire.com
|
||||
*
|
||||
* @todo Move static helper methods to dedicated API var/class so this class can be more focused
|
||||
* @todo Determine whether Selector array handling methods would be better in separate/descending class
|
||||
*
|
||||
*/
|
||||
|
||||
class Selectors extends WireArray {
|
||||
@@ -184,310 +187,6 @@ class Selectors extends WireArray {
|
||||
return $this->wire(new SelectorEqual('',''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Selector type that processes a specific operator
|
||||
*
|
||||
* Static since there may be multiple instances of this Selectors class at runtime.
|
||||
* See Selector.php
|
||||
*
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $operator
|
||||
* @param string $class
|
||||
*
|
||||
*/
|
||||
static public function addType($operator, $class) {
|
||||
self::$selectorTypes[$operator] = $class;
|
||||
for($n = 0; $n < strlen($operator); $n++) {
|
||||
$c = $operator[$n];
|
||||
self::$operatorChars[$c] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all operators allowed by selectors
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param array $options
|
||||
* - `operator` (string): Return info for only this operator. When specified, only value is returned (default='').
|
||||
* - `compareType` (int): Return only operators matching given `Selector::compareType*` constant (default=0).
|
||||
* - `getIndexType` (string): Index type to use in returned array: 'operator' or 'class' (default='class')
|
||||
* - `getValueType` (string): Value type to use in returned array: 'operator', 'class', 'compareType', 'verbose' (default='operator').
|
||||
* If 'verbose' option used then assoc array returned for each operator containing 'class', 'operator', 'compareType'.
|
||||
* @return array|string|int Returned array where both keys and values are operators (or values are requested 'valueType' option)
|
||||
* If 'operator' option specified, return value is string, int or array (requested 'valueType'), and there is no indexType.
|
||||
* @since 3.0.154
|
||||
*
|
||||
*/
|
||||
static public function getOperators(array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'operator' => '',
|
||||
'getIndexType' => 'class',
|
||||
'getValueType' => 'operator',
|
||||
'compareType' => 0,
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$operators = array();
|
||||
$compareType = (int) $options['compareType'];
|
||||
$indexType = $options['getIndexType'];
|
||||
$valueType = $options['getValueType'];
|
||||
$selectorTypes = self::$selectorTypes;
|
||||
|
||||
if(!empty($options['operator'])) {
|
||||
$selectorTypes = array($options['operator'] => $selectorTypes[$options['operator']]);
|
||||
}
|
||||
|
||||
foreach($selectorTypes as $operator => $typeName) {
|
||||
$className = __NAMESPACE__ . "\\$typeName";
|
||||
if($compareType) {
|
||||
/** @var Selector $className */
|
||||
if(!($className::getCompareType() & $options['compareType'])) continue;
|
||||
}
|
||||
if($valueType === 'class') {
|
||||
$value = $typeName;
|
||||
} else if($valueType === 'className') {
|
||||
$value = $className;
|
||||
} else if($valueType === 'compareType') {
|
||||
$value = $className::getCompareType();
|
||||
} else if($valueType === 'verbose') {
|
||||
$value = array(
|
||||
'operator' => $operator,
|
||||
'class' => $typeName,
|
||||
'className' => $className,
|
||||
'compareType' => $className::getCompareType(),
|
||||
);
|
||||
} else {
|
||||
$value = $operator;
|
||||
}
|
||||
if($indexType === 'class') {
|
||||
$key = $typeName;
|
||||
} else if($indexType === 'className') {
|
||||
$key = $className;
|
||||
} else {
|
||||
$key = $operator;
|
||||
}
|
||||
$operators[$key] = $value;
|
||||
}
|
||||
|
||||
if(!empty($options['operator'])) return reset($operators);
|
||||
|
||||
return $operators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of all valid operator characters
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
static public function getOperatorChars() {
|
||||
return self::$operatorChars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of other characters that have meaning in a selector outside of operators
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.156
|
||||
*
|
||||
*/
|
||||
static public function getReservedChars() {
|
||||
return array(
|
||||
'or' => '|', // title|body=foo, summary=bar|baz
|
||||
'not' => '!', // !body*=suchi tobiko
|
||||
'separator' => ',', // foo=bar, bar=baz
|
||||
'match-same-1' => '@', // @foo.bar=123, @foo.baz=456
|
||||
'quote-value' => '"', // foo="bar"
|
||||
'or-group-open' => '(', // id>0, (title=foo), (body=bar)
|
||||
'or-group-close' => ')',
|
||||
'sub-selector-open' => '[', // foo=[bar>0, baz%=text]
|
||||
'sub-selector-close' => ']',
|
||||
'api-var-open' => '[', // [page], [page.id], [user.id], etc.
|
||||
'api-var-close' => ']',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string indicating the type of operator that it is, or false if not an operator
|
||||
*
|
||||
* @param string $operator Operator to check
|
||||
* @param bool $is Change return value to just boolean true or false.
|
||||
* @return bool|string
|
||||
* @since 3.0.108
|
||||
*
|
||||
*/
|
||||
static public function getOperatorType($operator, $is = false) {
|
||||
if(!isset(self::$selectorTypes[$operator])) return false;
|
||||
$type = self::$selectorTypes[$operator];
|
||||
// now double check that we can map it back, in case PHP filters anything in the isset()
|
||||
$op = array_search($type, self::$selectorTypes);
|
||||
if($op === $operator) {
|
||||
if($is) return true;
|
||||
// Convert types like "SelectorEquals" to "Equals"
|
||||
if(strpos($type, 'Selector') === 0) list(,$type) = explode('Selector', $type, 2);
|
||||
return $type;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given string is a recognized operator, or false if not
|
||||
*
|
||||
* @param string $operator
|
||||
* @return bool
|
||||
* @since 3.0.108
|
||||
*
|
||||
*/
|
||||
static public function isOperator($operator) {
|
||||
return self::getOperatorType($operator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given string have an operator in it?
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $str String that might contain an operator
|
||||
* @param bool $getOperator Specify true to return the operator that was found, or false if not (since 3.0.108)
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
static public function stringHasOperator($str, $getOperator = false) {
|
||||
|
||||
static $letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
static $digits = '_0123456789';
|
||||
|
||||
$has = false;
|
||||
|
||||
foreach(self::$selectorTypes as $operator => $unused) {
|
||||
|
||||
if($operator == '&') continue; // this operator is too common in other contexts
|
||||
|
||||
$pos = strpos($str, $operator);
|
||||
if(!$pos) continue; // if pos is 0 or false, move onto the next
|
||||
|
||||
// possible match: confirm that field name precedes an operator
|
||||
// if(preg_match('/\b[_a-zA-Z0-9]+' . preg_quote($operator) . '/', $str)) {
|
||||
|
||||
$c = $str[$pos-1]; // letter before the operator
|
||||
|
||||
if(stripos($letters, $c) !== false) {
|
||||
// if a letter appears as the character before operator, then we're good
|
||||
$has = true;
|
||||
|
||||
} else if(strpos($digits, $c) !== false) {
|
||||
// if a digit appears as the character before operator, we need to confirm there is at least one letter
|
||||
// as there can't be a field named 123, for example, which would mean the operator is likely something
|
||||
// to do with math equations, which we would refuse as a valid selector operator
|
||||
$n = $pos-1;
|
||||
while($n > 0) {
|
||||
$c = $str[--$n];
|
||||
if(stripos($letters, $c) !== false) {
|
||||
// if found a letter, then we've got something valid
|
||||
$has = true;
|
||||
break;
|
||||
|
||||
} else if(strpos($digits, $c) === false) {
|
||||
// if we've got a non-digit (and non-letter) then definitely not valid
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($has) {
|
||||
if($getOperator) $getOperator = $operator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if($has && $getOperator) return $getOperator;
|
||||
|
||||
return $has;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the given string a Selector string?
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $str String to check for selector(s)
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
static public function stringHasSelector($str) {
|
||||
|
||||
if(!self::stringHasOperator($str)) return false;
|
||||
|
||||
$has = false;
|
||||
$alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
// replace characters that are allowed but aren't useful here
|
||||
if(strpos($str, '=(') !== false) $str = str_replace('=(', '=1,', $str);
|
||||
$str = str_replace(array('!', '(', ')', '@', '.', '|', '_'), '', trim(strtolower($str)));
|
||||
|
||||
// flatten sub-selectors
|
||||
$pos = strpos($str, '[');
|
||||
if($pos && strrpos($str, ']') > $pos) {
|
||||
$str = str_replace(array(']', '=[', '<[', '>['), array('', '=1,', '<2,', '>3,'), $str);
|
||||
}
|
||||
$str = rtrim($str, ", ");
|
||||
|
||||
// first character must match alphabet
|
||||
if(strpos($alphabet, substr($str, 0, 1)) === false) return false;
|
||||
|
||||
$operatorChars = implode('', self::getOperatorChars());
|
||||
|
||||
if(strpos($str, ',')) {
|
||||
// split the string into all key=value components and check each individually
|
||||
$inQuote = '';
|
||||
$cLast = '';
|
||||
// replace comments in quoted values so that they aren't considered selector boundaries
|
||||
for($n = 0; $n < strlen($str); $n++) {
|
||||
$c = $str[$n];
|
||||
if($c === ',') {
|
||||
// commas in quoted values are replaced with semicolons
|
||||
if($inQuote) $str[$n] = ';';
|
||||
} else if(($c === '"' || $c === "'") && $cLast != "\\") {
|
||||
if($inQuote && $inQuote === $c) {
|
||||
$inQuote = ''; // end quote
|
||||
} else if(!$inQuote) {
|
||||
$inQuote = $c; // start quote
|
||||
}
|
||||
}
|
||||
$cLast = $c;
|
||||
}
|
||||
$parts = explode(',', $str);
|
||||
} else {
|
||||
// outside of verbose mode, only the first apparent selector is checked
|
||||
$parts = array($str);
|
||||
}
|
||||
|
||||
// check each key=value component
|
||||
foreach($parts as $part) {
|
||||
$has = preg_match('/^[a-z][a-z0-9]*([' . $operatorChars . ']+)(.*)$/', trim($part), $matches);
|
||||
if($has) {
|
||||
$operator = $matches[1];
|
||||
$value = $matches[2];
|
||||
if(!isset(self::$selectorTypes[$operator])) {
|
||||
$has = false;
|
||||
} else if(self::stringHasOperator($value) && $value[0] != '"' && $value[0] != "'") {
|
||||
// operators not allowed in values unless quoted
|
||||
$has = false;
|
||||
}
|
||||
}
|
||||
if(!$has) break;
|
||||
}
|
||||
|
||||
return $has;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Selector object from a field name, operator, and value
|
||||
*
|
||||
@@ -524,7 +223,6 @@ class Selectors extends WireArray {
|
||||
return $selector;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a selector string, populate to Selector objects in this Selectors instance
|
||||
*
|
||||
@@ -1019,14 +717,13 @@ class Selectors extends WireArray {
|
||||
return $matches;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
$str = '';
|
||||
foreach($this as $selector) {
|
||||
$str .= $selector->str . ", ";
|
||||
}
|
||||
return rtrim($str, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string indicating given data type for use in selector arrays
|
||||
*
|
||||
* @param int|string|array $data
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function getSelectorArrayType($data) {
|
||||
$dataType = '';
|
||||
if(is_int($data)) {
|
||||
@@ -1040,6 +737,13 @@ class Selectors extends WireArray {
|
||||
return $dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and return operator from end of field name, as used by selector arrays
|
||||
*
|
||||
* @param string $field
|
||||
* @return bool|string
|
||||
*
|
||||
*/
|
||||
protected function getOperatorFromField(&$field) {
|
||||
$operator = '=';
|
||||
$operators = array_keys(self::$selectorTypes);
|
||||
@@ -1059,7 +763,6 @@ class Selectors extends WireArray {
|
||||
return $operator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create this Selectors object from an array
|
||||
*
|
||||
@@ -1324,68 +1027,6 @@ class Selectors extends WireArray {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple "a=b, c=d" selector-style string conversion to associative array, for fast/simple needs
|
||||
*
|
||||
* - The only supported operator is "=".
|
||||
* - Each key=value statement should be separated by a comma.
|
||||
* - Do not use quoted values.
|
||||
* - If you need a literal comma, use a double comma ",,".
|
||||
* - If you need a literal equals, use a double equals "==".
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $s
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public static function keyValueStringToArray($s) {
|
||||
|
||||
if(strpos($s, '~~COMMA') !== false) $s = str_replace('~~COMMA', '', $s);
|
||||
if(strpos($s, '~~EQUAL') !== false) $s = str_replace('~~EQUAL', '', $s);
|
||||
|
||||
$hasEscaped = false;
|
||||
|
||||
if(strpos($s, ',,') !== false) {
|
||||
$s = str_replace(',,', '~~COMMA', $s);
|
||||
$hasEscaped = true;
|
||||
}
|
||||
if(strpos($s, '==') !== false) {
|
||||
$s = str_replace('==', '~~EQUAL', $s);
|
||||
$hasEscaped = true;
|
||||
}
|
||||
|
||||
$a = array();
|
||||
$parts = explode(',', $s);
|
||||
foreach($parts as $part) {
|
||||
if(!strpos($part, '=')) continue;
|
||||
list($key, $value) = explode('=', $part);
|
||||
if($hasEscaped) $value = str_replace(array('~~COMMA', '~~EQUAL'), array(',', '='), $value);
|
||||
$a[trim($key)] = trim($value);
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an assoc array, convert to a key=value selector-style string
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param $a
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public static function arrayToKeyValueString($a) {
|
||||
$s = '';
|
||||
foreach($a as $key => $value) {
|
||||
if(strpos($value, ',') !== false) $value = str_replace(array(',,', ','), ',,', $value);
|
||||
if(strpos($value, '=') !== false) $value = str_replace('=', '==', $value);
|
||||
$s .= "$key=$value, ";
|
||||
}
|
||||
return rtrim($s, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first selector that uses given field name
|
||||
*
|
||||
@@ -1466,39 +1107,445 @@ class Selectors extends WireArray {
|
||||
return count($matches) ? $matches[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value when typecast as string
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function __toString() {
|
||||
$str = '';
|
||||
foreach($this as $selector) {
|
||||
$str .= $selector->str . ", ";
|
||||
}
|
||||
return rtrim($str, ", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug info
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public function __debugInfo() {
|
||||
$info = parent::__debugInfo();
|
||||
$info['string'] = $this->__toString();
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug info for Selector item
|
||||
*
|
||||
* @param Selector|mixed $item
|
||||
* @return array|mixed|null|string
|
||||
*
|
||||
*/
|
||||
public function debugInfoItem($item) {
|
||||
if($item instanceof Selector) return $item->__debugInfo();
|
||||
return parent::debugInfoItem($item);
|
||||
}
|
||||
|
||||
/*** STATIC HELPERS *******************************************************************************/
|
||||
|
||||
/**
|
||||
* See if the given $selector specifies the given $field somewhere
|
||||
* Add a Selector type that processes a specific operator
|
||||
*
|
||||
* @param array|string|Selectors $selector
|
||||
* @param string $field
|
||||
* @return bool
|
||||
* Static since there may be multiple instances of this Selectors class at runtime.
|
||||
* See Selector.php
|
||||
*
|
||||
public static function selectorHasField($selector, $field) {
|
||||
|
||||
if(is_object($selector)) $selector = (string) $selector;
|
||||
|
||||
if(is_array($selector)) {
|
||||
if(array_key_exists($field, $selector)) return true;
|
||||
$test = print_r($selector, true);
|
||||
if(strpos($test, $field) === false) return false;
|
||||
|
||||
|
||||
} else if(is_string($selector)) {
|
||||
if(strpos($selector, $field) === false) return false; // quick exit
|
||||
* #pw-internal
|
||||
*
|
||||
* @param string $operator
|
||||
* @param string $class
|
||||
*
|
||||
*/
|
||||
static public function addType($operator, $class) {
|
||||
self::$selectorTypes[$operator] = $class;
|
||||
for($n = 0; $n < strlen($operator); $n++) {
|
||||
$c = $operator[$n];
|
||||
self::$operatorChars[$c] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all operators allowed by selectors
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param array $options
|
||||
* - `operator` (string): Return info for only this operator. When specified, only value is returned (default='').
|
||||
* - `compareType` (int): Return only operators matching given `Selector::compareType*` constant (default=0).
|
||||
* - `getIndexType` (string): Index type to use in returned array: 'operator', 'className' or 'class' (default='class')
|
||||
* - `getValueType` (string): Value type to use in returned array: 'operator', 'class', 'className', 'label', 'compareType', 'verbose' (default='operator').
|
||||
* If 'verbose' option used then assoc array returned for each operator containing 'class', 'className', 'operator', 'compareType', 'label'.
|
||||
* @return array|string|int Returned array where both keys and values are operators (or values are requested 'valueType' option)
|
||||
* If 'operator' option specified, return value is string, int or array (requested 'valueType'), and there is no indexType.
|
||||
* @since 3.0.154
|
||||
*
|
||||
*/
|
||||
static public function getOperators(array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'operator' => '',
|
||||
'getIndexType' => 'class',
|
||||
'getValueType' => 'operator',
|
||||
'compareType' => 0,
|
||||
);
|
||||
|
||||
$options = array_merge($defaults, $options);
|
||||
$operators = array();
|
||||
$compareType = (int) $options['compareType'];
|
||||
$indexType = $options['getIndexType'];
|
||||
$valueType = $options['getValueType'];
|
||||
$selectorTypes = self::$selectorTypes;
|
||||
|
||||
if(!empty($options['operator'])) {
|
||||
$selectorTypes = array($options['operator'] => $selectorTypes[$options['operator']]);
|
||||
}
|
||||
|
||||
foreach($selectorTypes as $operator => $typeName) {
|
||||
$className = __NAMESPACE__ . "\\$typeName";
|
||||
if($compareType) {
|
||||
/** @var Selector $className */
|
||||
if(!($className::getCompareType() & $options['compareType'])) continue;
|
||||
}
|
||||
if($valueType === 'class') {
|
||||
$value = $typeName;
|
||||
} else if($valueType === 'className') {
|
||||
$value = $className;
|
||||
} else if($valueType === 'label') {
|
||||
$value = $className::getLabel();
|
||||
} else if($valueType === 'compareType') {
|
||||
$value = $className::getCompareType();
|
||||
} else if($valueType === 'verbose') {
|
||||
$value = array(
|
||||
'operator' => $operator,
|
||||
'class' => $typeName,
|
||||
'className' => $className,
|
||||
'compareType' => $className::getCompareType(),
|
||||
'label' => $className::getLabel(),
|
||||
);
|
||||
} else {
|
||||
$value = $operator;
|
||||
}
|
||||
if($indexType === 'class') {
|
||||
$key = $typeName;
|
||||
} else if($indexType === 'className') {
|
||||
$key = $className;
|
||||
} else {
|
||||
$key = $operator;
|
||||
}
|
||||
$operators[$key] = $value;
|
||||
}
|
||||
|
||||
if(!empty($options['operator'])) return reset($operators);
|
||||
|
||||
return $operators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of all valid operator characters
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
static public function getOperatorChars() {
|
||||
return self::$operatorChars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of other characters that have meaning in a selector outside of operators
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @return array
|
||||
* @since 3.0.156
|
||||
*
|
||||
*/
|
||||
static public function getReservedChars() {
|
||||
return array(
|
||||
'or' => '|', // title|body=foo, summary=bar|baz
|
||||
'not' => '!', // !body*=suchi tobiko
|
||||
'separator' => ',', // foo=bar, bar=baz
|
||||
'match-same-1' => '@', // @foo.bar=123, @foo.baz=456
|
||||
'quote-value' => '"', // foo="bar"
|
||||
'or-group-open' => '(', // id>0, (title=foo), (body=bar)
|
||||
'or-group-close' => ')',
|
||||
'sub-selector-open' => '[', // foo=[bar>0, baz%=text]
|
||||
'sub-selector-close' => ']',
|
||||
'api-var-open' => '[', // [page], [page.id], [user.id], etc.
|
||||
'api-var-close' => ']',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string indicating the type of operator that it is, or false if not an operator
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $operator Operator to check
|
||||
* @param bool $is Change return value to just boolean true or false.
|
||||
* @return bool|string
|
||||
* @since 3.0.108
|
||||
*
|
||||
*/
|
||||
static public function getOperatorType($operator, $is = false) {
|
||||
if(!isset(self::$selectorTypes[$operator])) return false;
|
||||
$type = self::$selectorTypes[$operator];
|
||||
// now double check that we can map it back, in case PHP filters anything in the isset()
|
||||
$op = array_search($type, self::$selectorTypes);
|
||||
if($op === $operator) {
|
||||
if($is) return true;
|
||||
// Convert types like "SelectorEquals" to "Equals"
|
||||
if(strpos($type, 'Selector') === 0) list(,$type) = explode('Selector', $type, 2);
|
||||
return $type;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an operator, return Selector instance (or other requested Selector property)
|
||||
*
|
||||
* When getting a Selector instance, be sure to populate its `field` and `value` properties after retrieving it.
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $operator Operator to get Selector instance for
|
||||
* @param string $property One of 'instance,', 'label', 'compareType', 'class', 'className' (default='instance')
|
||||
* @return Selector|int|string|false Returns false if operator or property not recognized
|
||||
* @since 3.0.160
|
||||
*
|
||||
*/
|
||||
static public function getSelectorByOperator($operator, $property = 'instance') {
|
||||
if(!isset(self::$selectorTypes[$operator])) return false;
|
||||
$typeName = self::$selectorTypes[$operator];
|
||||
/** @var Selector $className */
|
||||
$className = __NAMESPACE__ . "\\$typeName";
|
||||
if($property === 'instance' || $property === '') return new $className('', null);
|
||||
if($property === 'compareType') return $className::getCompareType();
|
||||
if($property === 'className') return $className;
|
||||
if($property === 'label') return $className::getLabel();
|
||||
if($property === 'class') return $typeName;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given string is a recognized operator, or false if not
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $operator
|
||||
* @return bool
|
||||
* @since 3.0.108
|
||||
*
|
||||
*/
|
||||
static public function isOperator($operator) {
|
||||
return self::getOperatorType($operator, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given string have an operator in it?
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $str String that might contain an operator
|
||||
* @param bool $getOperator Specify true to return the operator that was found, or false if not (since 3.0.108)
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
static public function stringHasOperator($str, $getOperator = false) {
|
||||
|
||||
static $letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
static $digits = '_0123456789';
|
||||
|
||||
$has = false;
|
||||
|
||||
foreach(self::$selectorTypes as $operator => $unused) {
|
||||
|
||||
if($operator == '&') continue; // this operator is too common in other contexts
|
||||
|
||||
$pos = strpos($str, $operator);
|
||||
if(!$pos) continue; // if pos is 0 or false, move onto the next
|
||||
|
||||
// possible match: confirm that field name precedes an operator
|
||||
// if(preg_match('/\b[_a-zA-Z0-9]+' . preg_quote($operator) . '/', $str)) {
|
||||
|
||||
$c = $str[$pos-1]; // letter before the operator
|
||||
|
||||
if(stripos($letters, $c) !== false) {
|
||||
// if a letter appears as the character before operator, then we're good
|
||||
$has = true;
|
||||
|
||||
} else if(strpos($digits, $c) !== false) {
|
||||
// if a digit appears as the character before operator, we need to confirm there is at least one letter
|
||||
// as there can't be a field named 123, for example, which would mean the operator is likely something
|
||||
// to do with math equations, which we would refuse as a valid selector operator
|
||||
$n = $pos-1;
|
||||
while($n > 0) {
|
||||
$c = $str[--$n];
|
||||
if(stripos($letters, $c) !== false) {
|
||||
// if found a letter, then we've got something valid
|
||||
$has = true;
|
||||
break;
|
||||
|
||||
} else if(strpos($digits, $c) === false) {
|
||||
// if we've got a non-digit (and non-letter) then definitely not valid
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($has) {
|
||||
if($getOperator) $getOperator = $operator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if($has && $getOperator) return $getOperator;
|
||||
|
||||
return $has;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the given string a Selector string?
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $str String to check for selector(s)
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
static public function stringHasSelector($str) {
|
||||
|
||||
if(!self::stringHasOperator($str)) return false;
|
||||
|
||||
$has = false;
|
||||
$alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
// replace characters that are allowed but aren't useful here
|
||||
if(strpos($str, '=(') !== false) $str = str_replace('=(', '=1,', $str);
|
||||
$str = str_replace(array('!', '(', ')', '@', '.', '|', '_'), '', trim(strtolower($str)));
|
||||
|
||||
// flatten sub-selectors
|
||||
$pos = strpos($str, '[');
|
||||
if($pos && strrpos($str, ']') > $pos) {
|
||||
$str = str_replace(array(']', '=[', '<[', '>['), array('', '=1,', '<2,', '>3,'), $str);
|
||||
}
|
||||
$str = rtrim($str, ", ");
|
||||
|
||||
// first character must match alphabet
|
||||
if(strpos($alphabet, substr($str, 0, 1)) === false) return false;
|
||||
|
||||
$operatorChars = implode('', self::getOperatorChars());
|
||||
|
||||
if(strpos($str, ',')) {
|
||||
// split the string into all key=value components and check each individually
|
||||
$inQuote = '';
|
||||
$cLast = '';
|
||||
// replace comments in quoted values so that they aren't considered selector boundaries
|
||||
for($n = 0; $n < strlen($str); $n++) {
|
||||
$c = $str[$n];
|
||||
if($c === ',') {
|
||||
// commas in quoted values are replaced with semicolons
|
||||
if($inQuote) $str[$n] = ';';
|
||||
} else if(($c === '"' || $c === "'") && $cLast != "\\") {
|
||||
if($inQuote && $inQuote === $c) {
|
||||
$inQuote = ''; // end quote
|
||||
} else if(!$inQuote) {
|
||||
$inQuote = $c; // start quote
|
||||
}
|
||||
}
|
||||
$cLast = $c;
|
||||
}
|
||||
$parts = explode(',', $str);
|
||||
} else {
|
||||
// outside of verbose mode, only the first apparent selector is checked
|
||||
$parts = array($str);
|
||||
}
|
||||
|
||||
// check each key=value component
|
||||
foreach($parts as $part) {
|
||||
$has = preg_match('/^[a-z][a-z0-9]*([' . $operatorChars . ']+)(.*)$/', trim($part), $matches);
|
||||
if($has) {
|
||||
$operator = $matches[1];
|
||||
$value = $matches[2];
|
||||
if(!isset(self::$selectorTypes[$operator])) {
|
||||
$has = false;
|
||||
} else if(self::stringHasOperator($value) && $value[0] != '"' && $value[0] != "'") {
|
||||
// operators not allowed in values unless quoted
|
||||
$has = false;
|
||||
}
|
||||
}
|
||||
if(!$has) break;
|
||||
}
|
||||
|
||||
return $has;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple "a=b, c=d" selector-style string conversion to associative array, for fast/simple needs
|
||||
*
|
||||
* - The only supported operator is "=".
|
||||
* - Each key=value statement should be separated by a comma.
|
||||
* - Do not use quoted values.
|
||||
* - If you need a literal comma, use a double comma ",,".
|
||||
* - If you need a literal equals, use a double equals "==".
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param string $s
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
public static function keyValueStringToArray($s) {
|
||||
|
||||
if(strpos($s, '~~COMMA') !== false) $s = str_replace('~~COMMA', '', $s);
|
||||
if(strpos($s, '~~EQUAL') !== false) $s = str_replace('~~EQUAL', '', $s);
|
||||
|
||||
$hasEscaped = false;
|
||||
|
||||
if(strpos($s, ',,') !== false) {
|
||||
$s = str_replace(',,', '~~COMMA', $s);
|
||||
$hasEscaped = true;
|
||||
}
|
||||
if(strpos($s, '==') !== false) {
|
||||
$s = str_replace('==', '~~EQUAL', $s);
|
||||
$hasEscaped = true;
|
||||
}
|
||||
|
||||
$a = array();
|
||||
$parts = explode(',', $s);
|
||||
foreach($parts as $part) {
|
||||
if(!strpos($part, '=')) continue;
|
||||
list($key, $value) = explode('=', $part);
|
||||
if($hasEscaped) $value = str_replace(array('~~COMMA', '~~EQUAL'), array(',', '='), $value);
|
||||
$a[trim($key)] = trim($value);
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an assoc array, convert to a key=value selector-style string
|
||||
*
|
||||
* #pw-group-static-helpers
|
||||
*
|
||||
* @param array $a
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public static function arrayToKeyValueString($a) {
|
||||
$s = '';
|
||||
foreach($a as $key => $value) {
|
||||
if(strpos($value, ',') !== false) $value = str_replace(array(',,', ','), ',,', $value);
|
||||
if(strpos($value, '=') !== false) $value = str_replace('=', '==', $value);
|
||||
$s .= "$key=$value, ";
|
||||
}
|
||||
return rtrim($s, ", ");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user