1
0
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:
Ryan Cramer
2020-06-19 12:48:18 -04:00
parent 1f293cc4f4
commit c9c06f833a
2 changed files with 938 additions and 433 deletions

View File

@@ -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)); }
}

View File

@@ -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, ", ");
}
}