PageRenderTime 28ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/public_html/wire/core/Selectors.php

https://bitbucket.org/thomas1151/mats
PHP | 1277 lines | 677 code | 165 blank | 435 comment | 237 complexity | fd32aa8676414371a9786cc0fa06a8db MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, LGPL-2.1, MPL-2.0-no-copyleft-exception
  1. <?php namespace ProcessWire;
  2. require_once(PROCESSWIRE_CORE_PATH . "Selector.php");
  3. /**
  4. * ProcessWire Selectors
  5. *
  6. * #pw-summary Processes a selector string into a WireArray of Selector objects.
  7. * #pw-summary-static-helpers Static helper methods useful in analyzing selector strings outside of this class.
  8. * #pw-body =
  9. * This Selectors class is used internally by ProcessWire to provide selector string (and array) matching throughout the core.
  10. *
  11. * ~~~~~
  12. * $selectors = new Selectors("sale_price|retail_price>100, currency=USD|EUR");
  13. * if($selectors->matches($page)) {
  14. * // selector string matches the given $page (which can be any Wire-derived item)
  15. * }
  16. * ~~~~~
  17. * ~~~~~
  18. * // iterate and display what's in this Selectors object
  19. * foreach($selectors as $selector) {
  20. * echo "<p>";
  21. * echo "Field(s): " . implode('|', $selector->fields) . "<br>";
  22. * echo "Operator: " . $selector->operator . "<br>";
  23. * echo "Value(s): " . implode('|', $selector->values) . "<br>";
  24. * echo "</p>";
  25. * }
  26. * ~~~~~
  27. * #pw-body
  28. *
  29. * @link https://processwire.com/api/selectors/ Official Selectors Documentation
  30. * @method Selector[] getIterator()
  31. *
  32. * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
  33. * https://processwire.com
  34. *
  35. */
  36. class Selectors extends WireArray {
  37. /**
  38. * Maximum length for a selector operator
  39. *
  40. */
  41. const maxOperatorLength = 10;
  42. /**
  43. * Static array of Selector types of $operator => $className
  44. *
  45. */
  46. static $selectorTypes = array();
  47. /**
  48. * Array of all individual characters used by operators
  49. *
  50. */
  51. static $operatorChars = array();
  52. /**
  53. * Original saved selector string, used for debugging purposes
  54. *
  55. */
  56. protected $selectorStr = '';
  57. /**
  58. * Whether or not variables like [user.id] should be converted to actual value
  59. *
  60. * In most cases this should be true.
  61. *
  62. * @var bool
  63. *
  64. */
  65. protected $parseVars = true;
  66. /**
  67. * API variable names that are allowed to be parsed
  68. *
  69. * @var array
  70. *
  71. */
  72. protected $allowedParseVars = array(
  73. 'session',
  74. 'page',
  75. 'user',
  76. );
  77. /**
  78. * Types of quotes selector values may be surrounded in
  79. *
  80. */
  81. protected $quotes = array(
  82. // opening => closing
  83. '"' => '"',
  84. "'" => "'",
  85. '[' => ']',
  86. '{' => '}',
  87. '(' => ')',
  88. );
  89. /**
  90. * Given a selector string, extract it into one or more corresponding Selector objects, iterable in this object.
  91. *
  92. * @param string|null|array $selector Selector string or array. If not provided here, please follow-up with a setSelectorString($str) call.
  93. *
  94. */
  95. public function __construct($selector = null) {
  96. if(!is_null($selector)) $this->init($selector);
  97. }
  98. /**
  99. * Set the selector string or array (if not set already from the constructor)
  100. *
  101. * ~~~~~
  102. * $selectors = new Selectors();
  103. * $selectors->init("sale_price|retail_price>100, currency=USD|EUR");
  104. * ~~~~~
  105. *
  106. * @param string|array $selector
  107. *
  108. */
  109. public function init($selector) {
  110. if(is_array($selector)) {
  111. $this->setSelectorArray($selector);
  112. } else if(is_object($selector) && $selector instanceof Selector) {
  113. $this->add($selector);
  114. } else {
  115. $this->setSelectorString($selector);
  116. }
  117. }
  118. /**
  119. * Set the selector string
  120. *
  121. * #pw-internal
  122. *
  123. * @param string $selectorStr
  124. *
  125. */
  126. public function setSelectorString($selectorStr) {
  127. $this->selectorStr = $selectorStr;
  128. $this->extractString(trim($selectorStr));
  129. }
  130. /**
  131. * Import items into this WireArray.
  132. *
  133. * #pw-internal
  134. *
  135. * @throws WireException
  136. * @param string|WireArray $items Items to import.
  137. * @return WireArray This instance.
  138. *
  139. */
  140. public function import($items) {
  141. if(is_string($items)) {
  142. $this->extractString($items);
  143. return $this;
  144. } else {
  145. return parent::import($items);
  146. }
  147. }
  148. /**
  149. * Per WireArray interface, return true if the item is a Selector instance
  150. *
  151. * #pw-internal
  152. *
  153. * @param Selector $item
  154. * @return bool
  155. *
  156. */
  157. public function isValidItem($item) {
  158. return is_object($item) && $item instanceof Selector;
  159. }
  160. /**
  161. * Per WireArray interface, return a blank Selector
  162. *
  163. * #pw-internal
  164. *
  165. */
  166. public function makeBlankItem() {
  167. return $this->wire(new SelectorEqual('',''));
  168. }
  169. /**
  170. * Add a Selector type that processes a specific operator
  171. *
  172. * Static since there may be multiple instances of this Selectors class at runtime.
  173. * See Selector.php
  174. *
  175. * #pw-internal
  176. *
  177. * @param string $operator
  178. * @param string $class
  179. *
  180. */
  181. static public function addType($operator, $class) {
  182. self::$selectorTypes[$operator] = $class;
  183. for($n = 0; $n < strlen($operator); $n++) {
  184. $c = $operator[$n];
  185. self::$operatorChars[$c] = $c;
  186. }
  187. }
  188. /**
  189. * Return array of all valid operator characters
  190. *
  191. * #pw-group-static-helpers
  192. *
  193. * @return array
  194. *
  195. */
  196. static public function getOperatorChars() {
  197. return self::$operatorChars;
  198. }
  199. /**
  200. * Does the given string have an operator in it?
  201. *
  202. * #pw-group-static-helpers
  203. *
  204. * @param string $str
  205. * @return bool
  206. *
  207. */
  208. static public function stringHasOperator($str) {
  209. static $letters = 'abcdefghijklmnopqrstuvwxyz';
  210. static $digits = '_0123456789';
  211. $has = false;
  212. foreach(self::$selectorTypes as $operator => $unused) {
  213. if($operator == '&') continue; // this operator is too common in other contexts
  214. $pos = strpos($str, $operator);
  215. if(!$pos) continue; // if pos is 0 or false, move onto the next
  216. // possible match: confirm that field name precedes an operator
  217. // if(preg_match('/\b[_a-zA-Z0-9]+' . preg_quote($operator) . '/', $str)) {
  218. $c = $str[$pos-1]; // letter before the operator
  219. if(stripos($letters, $c) !== false) {
  220. // if a letter appears as the character before operator, then we're good
  221. $has = true;
  222. } else if(strpos($digits, $c) !== false) {
  223. // if a digit appears as the character before operator, we need to confirm there is at least one letter
  224. // as there can't be a field named 123, for example, which would mean the operator is likely something
  225. // to do with math equations, which we would refuse as a valid selector operator
  226. $n = $pos-1;
  227. while($n > 0) {
  228. $c = $str[--$n];
  229. if(stripos($letters, $c) !== false) {
  230. // if found a letter, then we've got something valid
  231. $has = true;
  232. break;
  233. } else if(strpos($digits, $c) === false) {
  234. // if we've got a non-digit (and non-letter) then definitely not valid
  235. break;
  236. }
  237. }
  238. }
  239. if($has) break;
  240. }
  241. return $has;
  242. }
  243. /**
  244. * Is the give string a Selector string?
  245. *
  246. * #pw-group-static-helpers
  247. *
  248. * @param string $str String to check for selector(s)
  249. * @return bool
  250. *
  251. */
  252. static public function stringHasSelector($str) {
  253. if(!self::stringHasOperator($str)) return false;
  254. $has = false;
  255. $alphabet = 'abcdefghijklmnopqrstuvwxyz';
  256. // replace characters that are allowed but aren't useful here
  257. if(strpos($str, '=(') !== false) $str = str_replace('=(', '=1,', $str);
  258. $str = str_replace(array('!', '(', ')', '@', '.', '|', '_'), '', trim(strtolower($str)));
  259. // flatten sub-selectors
  260. $pos = strpos($str, '[');
  261. if($pos && strrpos($str, ']') > $pos) {
  262. $str = str_replace(array(']', '=[', '<[', '>['), array('', '=1,', '<2,', '>3,'), $str);
  263. }
  264. $str = rtrim($str, ", ");
  265. // first character must match alphabet
  266. if(strpos($alphabet, substr($str, 0, 1)) === false) return false;
  267. $operatorChars = implode('', self::getOperatorChars());
  268. if(strpos($str, ',')) {
  269. // split the string into all key=value components and check each individually
  270. $inQuote = '';
  271. $cLast = '';
  272. // replace comments in quoted values so that they aren't considered selector boundaries
  273. for($n = 0; $n < strlen($str); $n++) {
  274. $c = $str[$n];
  275. if($c === ',') {
  276. // commas in quoted values are replaced with semicolons
  277. if($inQuote) $str[$n] = ';';
  278. } else if(($c === '"' || $c === "'") && $cLast != "\\") {
  279. if($inQuote && $inQuote === $c) {
  280. $inQuote = ''; // end quote
  281. } else if(!$inQuote) {
  282. $inQuote = $c; // start quote
  283. }
  284. }
  285. $cLast = $c;
  286. }
  287. $parts = explode(',', $str);
  288. } else {
  289. // outside of verbose mode, only the first apparent selector is checked
  290. $parts = array($str);
  291. }
  292. // check each key=value component
  293. foreach($parts as $part) {
  294. $has = preg_match('/^[a-z][a-z0-9]*([' . $operatorChars . ']+)(.*)$/', trim($part), $matches);
  295. if($has) {
  296. $operator = $matches[1];
  297. $value = $matches[2];
  298. if(!isset(self::$selectorTypes[$operator])) {
  299. $has = false;
  300. } else if(self::stringHasOperator($value) && $value[0] != '"' && $value[0] != "'") {
  301. // operators not allowed in values unless quoted
  302. $has = false;
  303. }
  304. }
  305. if(!$has) break;
  306. }
  307. return $has;
  308. }
  309. /**
  310. * Create a new Selector object from a field name, operator, and value
  311. *
  312. * This is mostly for internal use, as the Selectors object already does this when you pass it
  313. * a selector string in the constructor or init() method.
  314. *
  315. * #pw-group-advanced
  316. *
  317. * @param string $field Field name or names (separated by a pipe)
  318. * @param string $operator Operator, i.e. "="
  319. * @param string $value Value or values (separated by a pipe)
  320. * @return Selector Returns the correct type of `Selector` object that corresponds to the given `$operator`.
  321. * @throws WireException
  322. *
  323. */
  324. public function create($field, $operator, $value) {
  325. $not = false;
  326. if(!isset(self::$selectorTypes[$operator])) {
  327. // unrecognized operator, see if it's an alternate placement for NOT "!" statement
  328. $op = ltrim($operator, '!');
  329. if(isset(self::$selectorTypes[$op])) {
  330. $operator = $op;
  331. $not = true;
  332. } else {
  333. if(is_array($value)) $value = implode('|', $value);
  334. $debug = $this->wire('config')->debug ? "field='$field', value='$value', selector: '$this->selectorStr'" : "";
  335. throw new WireException("Unknown Selector operator: '$operator' -- was your selector value properly escaped? $debug");
  336. }
  337. }
  338. $class = wireClassName(self::$selectorTypes[$operator], true);
  339. $selector = $this->wire(new $class($field, $value));
  340. if($not) $selector->not = true;
  341. return $selector;
  342. }
  343. /**
  344. * Given a selector string, populate to Selector objects in this Selectors instance
  345. *
  346. * @param string $str The string containing a selector (or multiple selectors, separated by commas)
  347. *
  348. */
  349. protected function extractString($str) {
  350. while(strlen($str)) {
  351. $not = false;
  352. $quote = '';
  353. if(strpos($str, '!') === 0) {
  354. $str = ltrim($str, '!');
  355. $not = true;
  356. }
  357. $group = $this->extractGroup($str);
  358. $field = $this->extractField($str);
  359. $operator = $this->extractOperator($str, self::getOperatorChars());
  360. $value = $this->extractValue($str, $quote);
  361. if($this->parseVars && $quote == '[' && $this->valueHasVar($value)) {
  362. // parse an API variable property to a string value
  363. $v = $this->parseValue($value);
  364. if($v !== null) {
  365. $value = $v;
  366. $quote = '';
  367. }
  368. }
  369. if($field || $value || strlen("$value")) {
  370. $selector = $this->create($field, $operator, $value);
  371. if(!is_null($group)) $selector->group = $group;
  372. if($quote) $selector->quote = $quote;
  373. if($not) $selector->not = true;
  374. $this->add($selector);
  375. }
  376. }
  377. }
  378. /**
  379. * Given a string like name@field=... or @field=... extract the part that comes before the @
  380. *
  381. * This part indicates the group name, which may also be blank to indicate grouping with other blank grouped items
  382. *
  383. * @param string $str
  384. * @return null|string
  385. *
  386. */
  387. protected function extractGroup(&$str) {
  388. $group = null;
  389. $pos = strpos($str, '@');
  390. if($pos === false) return $group;
  391. if($pos === 0) {
  392. $group = '';
  393. $str = substr($str, 1);
  394. } else if(preg_match('/^([-_a-zA-Z0-9]*)@(.*)/', $str, $matches)) {
  395. $group = $matches[1];
  396. $str = $matches[2];
  397. }
  398. return $group;
  399. }
  400. /**
  401. * Given a string starting with a field, return that field, and remove it from $str.
  402. *
  403. * @param string $str
  404. * @return string
  405. *
  406. */
  407. protected function extractField(&$str) {
  408. $field = '';
  409. if(strpos($str, '(') === 0) {
  410. // OR selector where specification of field name is optional and = operator is assumed
  411. $str = '=(' . substr($str, 1);
  412. return $field;
  413. }
  414. if(preg_match('/^(!?[_|.a-zA-Z0-9]+)(.*)/', $str, $matches)) {
  415. $field = trim($matches[1], '|');
  416. $str = $matches[2];
  417. if(strpos($field, '|')) {
  418. $field = explode('|', $field);
  419. }
  420. }
  421. return $field;
  422. }
  423. /**
  424. * Given a string starting with an operator, return that operator, and remove it from $str.
  425. *
  426. * @param string $str
  427. * @param array $operatorChars
  428. * @return string
  429. *
  430. */
  431. protected function extractOperator(&$str, array $operatorChars) {
  432. $n = 0;
  433. $operator = '';
  434. while(isset($str[$n]) && in_array($str[$n], $operatorChars) && $n < self::maxOperatorLength) {
  435. $operator .= $str[$n];
  436. $n++;
  437. }
  438. if($operator) $str = substr($str, $n);
  439. return $operator;
  440. }
  441. /**
  442. * Early-exit optimizations for extractValue
  443. *
  444. * @param string $str String to extract value from, $str will be modified if extraction successful
  445. * @param string $openingQuote Opening quote character, if string has them, blank string otherwise
  446. * @param string $closingQuote Closing quote character, if string has them, blank string otherwise
  447. * @return mixed Returns found value if successful, boolean false if not
  448. *
  449. */
  450. protected function extractValueQuick(&$str, $openingQuote, $closingQuote) {
  451. // determine where value ends
  452. $offset = 0;
  453. if($openingQuote) $offset++; // skip over leading quote
  454. $commaPos = strpos("$str,", $closingQuote . ',', $offset); // "$str," just in case value is last and no trailing comma
  455. if($commaPos === false && $closingQuote) {
  456. // if closing quote and comma didn't match, try to match just comma in case of "something"<space>,
  457. $str1 = substr($str, 1);
  458. $commaPos = strpos($str1, ',');
  459. if($commaPos !== false) {
  460. $closingQuotePos = strpos($str1, $closingQuote);
  461. if($closingQuotePos > $commaPos) {
  462. // comma is in quotes and thus not one we want to work with
  463. return false;
  464. } else {
  465. // increment by 1 since it was derived from a string at position 1 (rather than 0)
  466. $commaPos++;
  467. }
  468. }
  469. }
  470. if($commaPos === false) {
  471. // value is the last one in $str
  472. $commaPos = strlen($str);
  473. } else if($commaPos && $str[$commaPos-1] === '//') {
  474. // escaped comma or closing quote means no optimization possible here
  475. return false;
  476. }
  477. // extract the value for testing
  478. $value = substr($str, 0, $commaPos);
  479. // if there is an operator present, it might be a subselector or OR-group
  480. if(self::stringHasOperator($value)) return false;
  481. if($openingQuote) {
  482. // if there were quotes, trim them out
  483. $value = trim($value, $openingQuote . $closingQuote);
  484. }
  485. // determine if there are any embedded quotes in the value
  486. $hasEmbeddedQuotes = false;
  487. foreach($this->quotes as $open => $close) {
  488. if(strpos($value, $open)) $hasEmbeddedQuotes = true;
  489. }
  490. // if value contains quotes anywhere inside of it, abort optimization
  491. if($hasEmbeddedQuotes) return false;
  492. // does the value contain possible OR conditions?
  493. if(strpos($value, '|') !== false) {
  494. // if there is an escaped pipe, abort optimization attempt
  495. if(strpos($value, '\\' . '|') !== false) return false;
  496. // if value was surrounded in "quotes" or 'quotes' abort optimization attempt
  497. // as the pipe is a literal value rather than an OR
  498. if($openingQuote == '"' || $openingQuote == "'") return false;
  499. // we have valid OR conditions, so convert to an array
  500. $value = explode('|', $value);
  501. }
  502. // if we reach this point we have a successful extraction and can remove value from str
  503. // $str = $commaPos ? trim(substr($str, $commaPos+1)) : '';
  504. $str = trim(substr($str, $commaPos+1));
  505. // successful optimization
  506. return $value;
  507. }
  508. /**
  509. * Given a string starting with a value, return that value, and remove it from $str.
  510. *
  511. * @param string $str String to extract value from
  512. * @param string $quote Automatically populated with quote type, if found
  513. * @return array|string Found values or value (excluding quotes)
  514. *
  515. */
  516. protected function extractValue(&$str, &$quote) {
  517. $str = trim($str);
  518. if(!strlen($str)) return '';
  519. if(isset($this->quotes[$str[0]])) {
  520. $openingQuote = $str[0];
  521. $closingQuote = $this->quotes[$openingQuote];
  522. $quote = $openingQuote;
  523. $n = 1;
  524. } else {
  525. $openingQuote = '';
  526. $closingQuote = '';
  527. $n = 0;
  528. }
  529. $value = $this->extractValueQuick($str, $openingQuote, $closingQuote); // see if we can do a quick exit
  530. if($value !== false) return $value;
  531. $value = '';
  532. $lastc = '';
  533. $quoteDepth = 0;
  534. do {
  535. if(!isset($str[$n])) break;
  536. $c = $str[$n];
  537. if($openingQuote) {
  538. // we are in a quoted value string
  539. if($c == $closingQuote) { // reference closing quote
  540. if($lastc != '\\') {
  541. // same quote that opened, and not escaped
  542. // means the end of the value
  543. if($quoteDepth > 0) {
  544. // closing of an embedded quote
  545. $quoteDepth--;
  546. } else {
  547. $n++; // skip over quote
  548. $quote = $openingQuote;
  549. break;
  550. }
  551. } else {
  552. // this is an intentionally escaped quote
  553. // so remove the escape
  554. $value = rtrim($value, '\\');
  555. }
  556. } else if($c == $openingQuote && $openingQuote != $closingQuote) {
  557. // another opening quote of the same type encountered while already in a quote
  558. $quoteDepth++;
  559. }
  560. } else {
  561. // we are in an un-quoted value string
  562. if($c == ',' || $c == '|') {
  563. if($lastc != '\\') {
  564. // a non-quoted, non-escaped comma terminates the value
  565. break;
  566. } else {
  567. // an intentionally escaped comma
  568. // so remove the escape
  569. $value = rtrim($value, '\\');
  570. }
  571. }
  572. }
  573. $value .= $c;
  574. $lastc = $c;
  575. } while(++$n);
  576. $len = strlen("$value");
  577. if($len) {
  578. $str = substr($str, $n);
  579. // if($len > self::maxValueLength) $value = substr($value, 0, self::maxValueLength);
  580. }
  581. $str = ltrim($str, ' ,"\']})'); // should be executed even if blank value
  582. // check if a pipe character is present next, indicating an OR value may be provided
  583. if(strlen($str) > 1 && substr($str, 0, 1) == '|') {
  584. $str = substr($str, 1);
  585. // perform a recursive extract to account for all OR values
  586. $v = $this->extractValue($str, $quote);
  587. $quote = ''; // we don't support separately quoted OR values
  588. $value = array($value);
  589. if(is_array($v)) $value = array_merge($value, $v);
  590. else $value[] = $v;
  591. }
  592. return $value;
  593. }
  594. /**
  595. * Given a value string with an "api_var" or "api_var.property" return the string value of the property
  596. *
  597. * #pw-internal
  598. *
  599. * @param string $value var or var.property
  600. * @return null|string Returns null if it doesn't resolve to anything or a string of the value it resolves to
  601. *
  602. */
  603. public function parseValue($value) {
  604. if(!preg_match('/^\$?[_a-zA-Z0-9]+(?:\.[_a-zA-Z0-9]+)?$/', $value)) return null;
  605. $property = '';
  606. if(strpos($value, '.')) list($value, $property) = explode('.', $value);
  607. if(!in_array($value, $this->allowedParseVars)) return null;
  608. $value = $this->wire($value);
  609. if(is_null($value)) return null; // does not resolve to API var
  610. if(empty($property)) return (string) $value; // no property requested, just return string value
  611. if(!is_object($value)) return null; // property requested, but value is not an object
  612. return (string) $value->$property;
  613. }
  614. /**
  615. * Set whether or not vars should be parsed
  616. *
  617. * By default this is true, so only need to call this method to disable variable parsing.
  618. *
  619. * #pw-internal
  620. *
  621. * @param bool $parseVars
  622. *
  623. */
  624. public function setParseVars($parseVars) {
  625. $this->parseVars = $parseVars ? true : false;
  626. }
  627. /**
  628. * Does the given Selector value contain a parseable value?
  629. *
  630. * #pw-internal
  631. *
  632. * @param Selector $selector
  633. * @return bool
  634. *
  635. */
  636. public function selectorHasVar(Selector $selector) {
  637. if($selector->quote != '[') return false;
  638. $has = false;
  639. foreach($selector->values as $value) {
  640. if($this->valueHasVar($value)) {
  641. $has = true;
  642. break;
  643. }
  644. }
  645. return $has;
  646. }
  647. /**
  648. * Does the given value contain an API var reference?
  649. *
  650. * It is assumed the value was quoted in "[value]", and the quotes are not there now.
  651. *
  652. * #pw-internal
  653. *
  654. * @param string $value The value to evaluate
  655. * @return bool
  656. *
  657. */
  658. public function valueHasVar($value) {
  659. if(self::stringHasOperator($value)) return false;
  660. if(strpos($value, '.') !== false) {
  661. list($name, $subname) = explode('.', $value);
  662. } else {
  663. $name = $value;
  664. $subname = '';
  665. }
  666. if(!in_array($name, $this->allowedParseVars)) return false;
  667. if(strlen($subname) && $this->wire('sanitizer')->fieldName($subname) !== $subname) return false;
  668. return true;
  669. }
  670. /**
  671. * Return array of all field names referenced in all of the Selector objects here
  672. *
  673. * @param bool $subfields Default is to allow "field.subfield" fields, or specify false to convert them to just "field".
  674. * @return array Returned array has both keys and values as field names (same)
  675. *
  676. */
  677. public function getAllFields($subfields = true) {
  678. $fields = array();
  679. foreach($this as $selector) {
  680. $field = $selector->field;
  681. if(!is_array($field)) $field = array($field);
  682. foreach($field as $f) {
  683. if(!$subfields && strpos($f, '.')) {
  684. list($f, $subfield) = explode('.', $f, 2);
  685. if($subfield) {} // ignore
  686. }
  687. $fields[$f] = $f;
  688. }
  689. }
  690. return $fields;
  691. }
  692. /**
  693. * Return array of all values referenced in all Selector objects here
  694. *
  695. * @return array Returned array has both keys and values as field values (same)
  696. *
  697. */
  698. public function getAllValues() {
  699. $values = array();
  700. foreach($this as $selector) {
  701. $value = $selector->value;
  702. if(!is_array($value)) $value = array($value);
  703. foreach($value as $v) {
  704. $values[$v] = $v;
  705. }
  706. }
  707. return $values;
  708. }
  709. /**
  710. * Does the given Wire match these Selectors?
  711. *
  712. * @param Wire $item
  713. * @return bool
  714. *
  715. */
  716. public function matches(Wire $item) {
  717. // if item provides it's own matches function, then let it have control
  718. if($item instanceof WireMatchable) return $item->matches($this);
  719. $matches = true;
  720. foreach($this as $selector) {
  721. $value = array();
  722. foreach($selector->fields as $property) {
  723. if(strpos($property, '.') && $item instanceof WireData) {
  724. $value[] = $item->getDot($property);
  725. } else {
  726. $value[] = (string) $item->$property;
  727. }
  728. }
  729. if(!$selector->matches($value)) {
  730. $matches = false;
  731. break;
  732. }
  733. }
  734. return $matches;
  735. }
  736. public function __toString() {
  737. $str = '';
  738. foreach($this as $selector) {
  739. $str .= $selector->str . ", ";
  740. }
  741. return rtrim($str, ", ");
  742. }
  743. protected function getSelectorArrayType($data) {
  744. $dataType = '';
  745. if(is_int($data)) {
  746. $dataType = 'int';
  747. } else if(is_string($data)) {
  748. $dataType = 'string';
  749. } else if(is_array($data)) {
  750. $dataType = ctype_digit(implode('', array_keys($data))) ? 'array' : 'assoc';
  751. if($dataType == 'assoc' && isset($data['field'])) $dataType = 'verbose';
  752. }
  753. return $dataType;
  754. }
  755. protected function getOperatorFromField(&$field) {
  756. $operator = '=';
  757. $operators = array_keys(self::$selectorTypes);
  758. $operatorsStr = implode('', $operators);
  759. $op = substr($field, -1);
  760. if(strpos($operatorsStr, $op) !== false) {
  761. // extract operator from $field
  762. $field = substr($field, 0, -1);
  763. $op2 = substr($field, -1);
  764. if(strpos($operatorsStr, $op2) !== false) {
  765. $field = substr($field, 0, -1);
  766. $op = $op2 . $op;
  767. }
  768. $operator = $op;
  769. $field = trim($field);
  770. }
  771. return $operator;
  772. }
  773. /**
  774. * Create this Selectors object from an array
  775. *
  776. * #pw-internal
  777. *
  778. * @param array $a
  779. * @throws WireException
  780. *
  781. */
  782. public function setSelectorArray(array $a) {
  783. $groupCnt = 0;
  784. // fields that may only appear once in a selector
  785. $singles = array(
  786. 'start' => '',
  787. 'limit' => '',
  788. 'end' => '',
  789. );
  790. foreach($a as $key => $data) {
  791. $keyType = $this->getSelectorArrayType($key);
  792. $dataType = $this->getSelectorArrayType($data);
  793. if($keyType == 'int' && $dataType == 'assoc') {
  794. // OR-group
  795. $groupCnt++;
  796. foreach($data as $k => $v) {
  797. $s = $this->makeSelectorArrayItem($k, $v);
  798. $selector1 = $this->create($s['field'], $s['operator'], $s['value']);
  799. $selector2 = $this->create("or$groupCnt", "=", $selector1);
  800. $selector2->quote = '(';
  801. $this->add($selector2);
  802. }
  803. } else {
  804. $s = $this->makeSelectorArrayItem($key, $data, $dataType);
  805. $field = $s['field'];
  806. if(!is_array($field) && isset($singles[$field])) {
  807. if(empty($singles[$field])) {
  808. // mark it as present
  809. $singles[$field] = true;
  810. } else {
  811. // skip, because this 'single' field has already appeared
  812. continue;
  813. }
  814. }
  815. $selector = $this->create($field, $s['operator'], $s['value']);
  816. if($s['not']) $selector->not = true;
  817. if($s['group']) $selector->group = $s['group'];
  818. if($s['quote']) $selector->quote = $s['quote'];
  819. $this->add($selector);
  820. }
  821. }
  822. }
  823. /**
  824. * Return an array of an individual Selector info, for use by setSelectorArray() method
  825. *
  826. * @param string|int $key
  827. * @param array $data
  828. * @param string $dataType One of 'string', 'array', 'assoc', or 'verbose'
  829. * @return array
  830. * @throws WireException
  831. *
  832. */
  833. protected function makeSelectorArrayItem($key, $data, $dataType = '') {
  834. $sanitizer = $this->wire('sanitizer');
  835. $sanitize = 'selectorValue';
  836. $fields = array();
  837. $values = array();
  838. $operator = '=';
  839. $whitelist = null;
  840. $not = false;
  841. $group = '';
  842. $find = ''; // sub-selector
  843. $quote = '';
  844. if(empty($dataType)) $dataType = $this->getSelectorArrayType($data);
  845. if(is_int($key) && $dataType == 'verbose') {
  846. // Verbose selector with associative array of properties, in this expected format:
  847. //
  848. // $data = array(
  849. // 'field' => array|string, // field name, or field names
  850. // 'value' => array|string|number|object, // value or values, or omit if using 'find'
  851. // ---the following are optional---
  852. // 'operator' => '=>', // operator, '=' is the default
  853. // 'not' => false, // specify true to make this a NOT condition (default=false)
  854. // 'sanitize' => 'selectorValue', // sanitizer method to use on value(s), 'selectorValue' is default
  855. // 'find' => array(...), // sub-selector to use instead of 'value'
  856. // 'whitelist' => null|array, // whitelist of allowed values, NULL is default, which means ignore.
  857. // );
  858. if(isset($data['fields']) && !isset($data['field'])) $data['field'] = $data['fields']; // allow plural alternate
  859. if(!isset($data['field'])) {
  860. throw new WireException("Invalid selectors array, lacks 'field' property for index $key");
  861. }
  862. if(isset($data['values']) && !isset($data['value'])) $data['value'] = $data['values']; // allow plural alternate
  863. if(!isset($data['value']) && !isset($data['find'])) {
  864. throw new WireException("Invalid selectors array, lacks 'value' property for index $key");
  865. }
  866. if(isset($data['sanitizer']) && !isset($data['sanitize'])) $data['sanitize'] = $data['sanitizer']; // allow alternate
  867. if(isset($data['sanitize'])) $sanitize = $sanitizer->fieldName($data['sanitize']);
  868. if(!empty($data['operator'])) $operator = $data['operator'];
  869. if(!empty($data['not'])) $not = (bool) $data['not'];
  870. // may use either 'group' or 'or' to specify or-group
  871. if(!empty($data['group'])) {
  872. $group = $sanitizer->fieldName($data['group']);
  873. } else if(!empty($data['or'])) {
  874. $group = $sanitizer->fieldName($data['or']);
  875. }
  876. if(!empty($data['find'])) {
  877. if(isset($data['value'])) throw new WireException("You may not specify both 'value' and 'find' at the same time");
  878. // if(!is_array($data['find'])) throw new WireException("Selector 'find' property must be specified as array");
  879. $find = $data['find'];
  880. $data['value'] = array();
  881. }
  882. if(isset($data['whitelist']) && $data['whitelist'] !== null) {
  883. $whitelist = $data['whitelist'];
  884. if(is_object($whitelist) && $whitelist instanceof WireArray) $whitelist = explode('|', (string) $whitelist);
  885. if(!is_array($whitelist)) $whitelist = array($whitelist);
  886. }
  887. if($sanitize && $sanitize != 'selectorValue' && !method_exists($sanitizer, $sanitize)) {
  888. throw new WireException("Unrecognized sanitize method: " . $sanitizer->name($sanitize));
  889. }
  890. $_fields = is_array($data['field']) ? $data['field'] : array($data['field']);
  891. $_values = is_array($data['value']) ? $data['value'] : array($data['value']);
  892. } else if(is_string($key)) {
  893. // Non-verbose selector, where $key is the field name and $data is the value
  894. // The $key field name may have an optional operator appended to it
  895. $operator = $this->getOperatorFromField($key);
  896. $_fields = strpos($key, '|') ? explode('|', $key) : array($key);
  897. $_values = is_array($data) ? $data : array($data);
  898. } else if($dataType == 'array') {
  899. // selector in format: array('field', 'operator', 'value', 'sanitizer_method')
  900. // or array('field', 'operator', 'value', array('whitelist value1', 'whitelist value2', 'etc'))
  901. // or array('field', 'operator', 'value')
  902. // or array('field', 'value') where '=' is assumed operator
  903. $field = '';
  904. $value = array();
  905. if(count($data) == 4) {
  906. list($field, $operator, $value, $_sanitize) = $data;
  907. if(is_array($_sanitize)) {
  908. $whitelist = $_sanitize;
  909. } else {
  910. $sanitize = $sanitizer->name($_sanitize);
  911. }
  912. } else if(count($data) == 3) {
  913. list($field, $operator, $value) = $data;
  914. } else if(count($data) == 2) {
  915. list($field, $value) = $data;
  916. $operator = $this->getOperatorFromField($field);
  917. }
  918. if(is_array($field)) {
  919. $_fields = $field;
  920. } else {
  921. $_fields = strpos($field, '|') ? explode('|', $field) : array($field);
  922. }
  923. $_values = is_array($value) ? $value : array($value);
  924. } else {
  925. throw new WireException("Unable to resolve selector array");
  926. }
  927. // make sure operator is valid
  928. if(!isset(self::$selectorTypes[$operator])) {
  929. throw new WireException("Unrecognized selector operator '$operator'");
  930. }
  931. // determine field(s)
  932. foreach($_fields as $name) {
  933. if(strpos($name, '.') !== false) {
  934. // field name with multiple.named.parts, sanitize them separately
  935. $parts = explode('.', $name);
  936. foreach($parts as $n => $part) {
  937. $parts[$n] = $sanitizer->fieldName($part);
  938. }
  939. $_name = implode('.', $parts);
  940. } else {
  941. $_name = $sanitizer->fieldName($name);
  942. }
  943. if($_name !== $name) {
  944. throw new WireException("Invalid Selectors field name (sanitized value '$_name' did not match specified value)");
  945. }
  946. $fields[] = $_name;
  947. }
  948. // convert WireArray types to an array of $_values
  949. if(count($_values) === 1) {
  950. $value = reset($_values);
  951. if(is_object($value) && $value instanceof WireArray) {
  952. $_values = explode('|', (string) $value);
  953. }
  954. }
  955. // determine value(s)
  956. foreach($_values as $value) {
  957. $_sanitize = $sanitize;
  958. if(is_array($value)) $value = 'array'; // we don't allow arrays here
  959. if(is_object($value)) $value = (string) $value;
  960. if(is_int($value) || ctype_digit($value)) {
  961. $value = (int) $value;
  962. if($_sanitize == 'selectorValue') $_sanitize = ''; // no need to sanitize integer to string
  963. }
  964. if(is_array($whitelist) && !in_array($value, $whitelist)) {
  965. $fieldsStr = implode('|', $fields);
  966. throw new WireException("Value given for '$fieldsStr' is not in provided whitelist");
  967. }
  968. if($_sanitize === 'selectorValue') {
  969. $value = $sanitizer->selectorValue($value, array('useQuotes' => false));
  970. } else if($_sanitize) {
  971. $value = $sanitizer->$_sanitize($value);
  972. }
  973. $values[] = $value;
  974. }
  975. if($find) {
  976. // sub-selector find
  977. $quote = '[';
  978. $values = new Selectors($find);
  979. } else if($group) {
  980. // groups use quotes '()'
  981. $quote = '(';
  982. }
  983. return array(
  984. 'field' => count($fields) > 1 ? $fields : reset($fields),
  985. 'value' => count($values) > 1 ? $values : reset($values),
  986. 'operator' => $operator,
  987. 'not' => $not,
  988. 'group' => $group,
  989. 'quote' => $quote,
  990. );
  991. }
  992. /**
  993. * Simple "a=b, c=d" selector-style string conversion to associative array, for fast/simple needs
  994. *
  995. * - The only supported operator is "=".
  996. * - Each key=value statement should be separated by a comma.
  997. * - Do not use quoted values.
  998. * - If you need a literal comma, use a double comma ",,".
  999. * - If you need a literal equals, use a double equals "==".
  1000. *
  1001. * #pw-group-static-helpers
  1002. *
  1003. * @param string $s
  1004. * @return array
  1005. *
  1006. */
  1007. public static function keyValueStringToArray($s) {
  1008. if(strpos($s, '~~COMMA') !== false) $s = str_replace('~~COMMA', '', $s);
  1009. if(strpos($s, '~~EQUAL') !== false) $s = str_replace('~~EQUAL', '', $s);
  1010. $hasEscaped = false;
  1011. if(strpos($s, ',,') !== false) {
  1012. $s = str_replace(',,', '~~COMMA', $s);
  1013. $hasEscaped = true;
  1014. }
  1015. if(strpos($s, '==') !== false) {
  1016. $s = str_replace('==', '~~EQUAL', $s);
  1017. $hasEscaped = true;
  1018. }
  1019. $a = array();
  1020. $parts = explode(',', $s);
  1021. foreach($parts as $part) {
  1022. if(!strpos($part, '=')) continue;
  1023. list($key, $value) = explode('=', $part);
  1024. if($hasEscaped) $value = str_replace(array('~~COMMA', '~~EQUAL'), array(',', '='), $value);
  1025. $a[trim($key)] = trim($value);
  1026. }
  1027. return $a;
  1028. }
  1029. /**
  1030. * Given an assoc array, convert to a key=value selector-style string
  1031. *
  1032. * #pw-group-static-helpers
  1033. *
  1034. * @param $a
  1035. * @return string
  1036. *
  1037. */
  1038. public static function arrayToKeyValueString($a) {
  1039. $s = '';
  1040. foreach($a as $key => $value) {
  1041. if(strpos($value, ',') !== false) $value = str_replace(array(',,', ','), ',,', $value);
  1042. if(strpos($value, '=') !== false) $value = str_replace('=', '==', $value);
  1043. $s .= "$key=$value, ";
  1044. }
  1045. return rtrim($s, ", ");
  1046. }
  1047. /**
  1048. * Get the first selector that uses given field name
  1049. *
  1050. * This is useful for quickly retrieving values of reserved properties like "include", "limit", "start", etc.
  1051. *
  1052. * Using **$or:** By default this excludes selectors that have fields in an OR expression, like "a|b|c".
  1053. * So if you specified field "a" it would not be matched. If you wanted it to still match, specify true
  1054. * for the $or argument.
  1055. *
  1056. * Using **$all:** By default only the first matching selector is returned. If you want it to return all
  1057. * matching selectors in an array, then specify true for the $all argument. This changes the return value
  1058. * to always be an array of Selector objects, or a blank array if no match.
  1059. *
  1060. * @param string $fieldName Name of field to return value for (i.e. "include", "limit", etc.)
  1061. * @param bool $or Allow fields that appear in OR expressions? (default=false)
  1062. * @param bool $all Return an array of all matching Selector objects? (default=false)
  1063. * @return Selector|array|null Returns null if field not present in selectors (or blank array if $all mode)
  1064. *
  1065. */
  1066. public function getSelectorByField($fieldName, $or = false, $all = false) {
  1067. $selector = null;
  1068. $matches = array();
  1069. foreach($this as $sel) {
  1070. if($or) {
  1071. if(!in_array($fieldName, $sel->fields)) continue;
  1072. } else {
  1073. if($sel->field() !== $fieldName) continue;
  1074. }
  1075. if($all) {
  1076. $matches[] = $sel;
  1077. } else {
  1078. $selector = $sel;
  1079. break;
  1080. }
  1081. }
  1082. return $all ? $matches : $selector;
  1083. }
  1084. /**
  1085. * See if the given $selector specifies the given $field somewhere
  1086. *
  1087. * @param array|string|Selectors $selector
  1088. * @param string $field
  1089. * @return bool
  1090. *
  1091. public static function selectorHasField($selector, $field) {
  1092. if(is_object($selector)) $selector = (string) $selector;
  1093. if(is_array($selector)) {
  1094. if(array_key_exists($field, $selector)) return true;
  1095. $test = print_r($selector, true);
  1096. if(strpos($test, $field) === false) return false;
  1097. } else if(is_string($selector)) {
  1098. if(strpos($selector, $field) === false) return false; // quick exit
  1099. }
  1100. }
  1101. */
  1102. }
  1103. Selector::loadSelectorTypes();