diff --git a/wire/core/Selector.php b/wire/core/Selector.php index 169ee617..cf8aedfd 100644 --- a/wire/core/Selector.php +++ b/wire/core/Selector.php @@ -131,6 +131,12 @@ abstract class Selector extends WireData { * */ const compareTypeDatabase = 128; + + /** + * Comparison type: Fulltext index required when used with database queries + * + */ + const compareTypeFulltext = 256; /** * Given a field name and value, construct the Selector. @@ -658,6 +664,7 @@ abstract class Selector extends WireData { 'ContainsAnyWords', 'ContainsAnyWordsPartial', 'ContainsAnyWordsLike', + 'ContainsAnyWordsExpand', 'ContainsExpand', 'ContainsMatch', 'ContainsMatchExpand', @@ -686,9 +693,7 @@ 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__); - } + 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); } } @@ -700,9 +705,7 @@ 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__); - } + 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); } } @@ -714,9 +717,7 @@ 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__); - } + public static function getDescription() { return __('Compared value is greater than given value.', __FILE__); } protected function match($value1, $value2) { return $this->evaluate($value1 > $value2); } } @@ -728,9 +729,7 @@ 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__); - } + public static function getDescription() { return __('Compared value is less than given value.', __FILE__); } protected function match($value1, $value2) { return $this->evaluate($value1 < $value2); } } @@ -742,9 +741,7 @@ 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__); - } + 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); } } @@ -756,9 +753,7 @@ 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__); - } + 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); } } @@ -768,28 +763,58 @@ class SelectorLessThanEqual extends Selector { */ class SelectorContains extends Selector { public static function getOperator() { return '*='; } - public static function getCompareType() { return Selector::compareTypeFind; } + public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeFulltext; } public static function getLabel() { return __('Contains phrase', __FILE__); } - public static function getDescription() { - return __('Given phrase or word appears in value compared to.', __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::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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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'); } } /** @@ -800,9 +825,7 @@ class SelectorContainsLike extends SelectorContains { public static function getOperator() { return '%='; } public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeLike; } 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__); - } + public static function getDescription() { return SelectorContains::buildDescription('phrase like'); } protected function match($value1, $value2) { return $this->evaluate(stripos($value1, $value2) !== false); } } @@ -812,11 +835,9 @@ class SelectorContainsLike extends SelectorContains { */ 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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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); @@ -834,11 +855,9 @@ class SelectorContainsWords extends Selector { */ 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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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); @@ -852,6 +871,28 @@ class SelectorContainsWordsPartial extends Selector { } } +/** + * 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 all words like', __FILE__); } + public static function getDescription() { return SelectorContains::buildDescription('words-all words-partial words-partial-any like'); } + protected function match($value1, $value2) { + $hasAll = true; + $words = $this->wire()->sanitizer->wordsArray($value2); + foreach($words as $key => $word) { + if(stripos($value1, $word) === false) { + $hasAll = false; + break; + } + } + return $this->evaluate($hasAll); + } +} + /** * Selector that matches entire words except for last word, which must start with * @@ -860,11 +901,9 @@ class SelectorContainsWordsPartial extends Selector { */ 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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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); @@ -882,41 +921,15 @@ class SelectorContainsWordsLive extends Selector { } } -/** - * 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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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'); } } /** @@ -925,8 +938,9 @@ class SelectorContainsWordsExpand extends SelectorContainsWords { */ class SelectorContainsAnyWords extends Selector { public static function getOperator() { return '~|='; } - public static function getCompareType() { return Selector::compareTypeFind; } + public static function getCompareType() { return Selector::compareTypeFind | 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); @@ -940,9 +954,6 @@ class SelectorContainsAnyWords extends Selector { } return $this->evaluate($hasAny); } - public static function getDescription() { - return __('Any of the given whole words appear in compared value.', __FILE__); - } } /** @@ -951,8 +962,9 @@ class SelectorContainsAnyWords extends Selector { */ class SelectorContainsAnyWordsPartial extends Selector { public static function getOperator() { return '~|*='; } - public static function getCompareType() { return Selector::compareTypeFind; } + public static function getCompareType() { return Selector::compareTypeFind | 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); @@ -966,9 +978,6 @@ class SelectorContainsAnyWordsPartial extends Selector { } 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__); - } } /** @@ -976,13 +985,10 @@ class SelectorContainsAnyWordsPartial extends Selector { * */ 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__); - } + 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); @@ -996,6 +1002,43 @@ class SelectorContainsAnyWordsLike extends Selector { } } +/** + * 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::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; + } + $stem = $textTools->getWordStem($word); + if($stem && stripos($value1, $stem) !== false && preg_match('/\b' . preg_quote($stem) . '/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 * @@ -1004,11 +1047,9 @@ class SelectorContainsAnyWordsLike extends Selector { */ class SelectorContainsMatch extends SelectorContainsAnyWords { public static function getOperator() { return '**='; } - public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeDatabase; } + public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeDatabase | Selector::compareTypeFulltext; } 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__); - } + public static function getDescription() { return SelectorContains::buildDescription('words-match words-whole fulltext'); } } /** @@ -1019,11 +1060,9 @@ class SelectorContainsMatch extends SelectorContainsAnyWords { */ 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__); - } + public static function getCompareType() { return Selector::compareTypeFind | 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'); } } /** @@ -1043,7 +1082,7 @@ class SelectorContainsMatchExpand extends SelectorContainsMatch { */ class SelectorContainsAdvanced extends SelectorContains { public static function getOperator() { return '#='; } - public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeCommand; } + public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeCommand | Selector::compareTypeFulltext; } public static function getLabel() { return __('Advanced text search', __FILE__); } public static function getDescription() { return @@ -1132,11 +1171,9 @@ class SelectorContainsAdvanced extends SelectorContains { */ class SelectorStarts extends Selector { public static function getOperator() { return '^='; } - public static function getCompareType() { return Selector::compareTypeFind; } + public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeFulltext; } public static function getLabel() { return __('Starts with', __FILE__); } - public static function getDescription() { - return __('Given word or phrase appears at beginning of compared value.', __FILE__); - } + public static function getDescription() { return SelectorContains::buildDescription('phrase-start fulltext'); } protected function match($value1, $value2) { return $this->evaluate(stripos(trim($value1), $value2) === 0); } @@ -1150,9 +1187,7 @@ 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__); - } + public static function getDescription() { return SelectorContains::buildDescription('phrase-start like'); } } /** @@ -1161,11 +1196,9 @@ class SelectorStartsLike extends SelectorStarts { */ class SelectorEnds extends Selector { public static function getOperator() { return '$='; } - public static function getCompareType() { return Selector::compareTypeFind; } + public static function getCompareType() { return Selector::compareTypeFind | Selector::compareTypeFulltext; } public static function getLabel() { return __('Ends with', __FILE__); } - public static function getDescription() { - return __('Given word or phrase appears at end of compared value.', __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)); @@ -1181,9 +1214,7 @@ 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__); - } + public static function getDescription() { return SelectorContains::buildDescription('phrase-end like'); } } /** diff --git a/wire/core/Selectors.php b/wire/core/Selectors.php index 4389b4ba..d509793f 100644 --- a/wire/core/Selectors.php +++ b/wire/core/Selectors.php @@ -383,9 +383,17 @@ class Selectors extends WireArray { } if($operator && !isset($operators[$lastOperator])) { - if(self::isOperator($operator)) { - $operators[$operator] = $operator; - } else { + $fail = true; + if(!count($operators)) { + // check if operator has a typo we can fix + $op = self::isOperator($operator, true); + if($op) { + $operators[$op] = $op; + $str = substr($str, $n); + $fail = false; + } + } + if($fail) { throw new WireException("Unrecognized operator: $operator"); } } @@ -1235,7 +1243,7 @@ class Selectors extends WireArray { * @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') + * - `getIndexType` (string): Index type to use in returned array: 'operator', 'className', 'class', or 'none' (default='class') * - `getValueType` (string): Value type to use in returned array: 'operator', 'class', 'className', 'label', 'description', 'compareType', 'verbose' (default='operator'). * If 'verbose' option used then assoc array returned for each operator containing 'class', 'className', 'operator', 'compareType', 'label', 'description'. * @return array|string|int Returned array where both keys and values are operators (or values are requested 'valueType' option) @@ -1291,14 +1299,20 @@ class Selectors extends WireArray { } else { $value = $operator; } - if($indexType === 'class') { + if($indexType === 'none') { + $key = ''; + } else if($indexType === 'class') { $key = $typeName; } else if($indexType === 'className') { $key = $className; } else { $key = $operator; } - $operators[$key] = $value; + if($key === '') { + $operators[] = $value; + } else { + $operators[$key] = $value; + } } if(!empty($options['operator'])) return reset($operators); @@ -1400,12 +1414,18 @@ class Selectors extends WireArray { * #pw-group-static-helpers * * @param string $operator - * @return bool + * @param bool $returnOperator Return the operator rather than bool? When true, corrects minor typos, like mixed up + * order, returning correct found operator string if possible, false otherwise. Added 3.0.162. (default=false) + * @return bool|string * @since 3.0.108 * */ - static public function isOperator($operator) { - return self::getOperatorType($operator, true); + static public function isOperator($operator, $returnOperator = false) { + $is = self::getOperatorType($operator, true); + if(!$returnOperator || strlen($operator) < 3) return $is; + if($is) return $operator; + $op = strrev(trim($operator, '=')) . '='; + return self::getOperatorType($op, true) ? $op : false; } /**