From e53552d8c41ba0d1d5b3efcc8f3d0f4bed109046 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 27 Oct 2023 13:26:24 -0400 Subject: [PATCH] Upgrade Page matching in-memory selectors by adding support for OR-groups, sub-selectors, and same (1) item group matches. --- wire/core/PageComparison.php | 147 +++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 24 deletions(-) diff --git a/wire/core/PageComparison.php b/wire/core/PageComparison.php index 6dadf592..0db9ae09 100644 --- a/wire/core/PageComparison.php +++ b/wire/core/PageComparison.php @@ -214,7 +214,7 @@ class PageComparison { // exit early for simple path comparison return true; } else if($page->name === $s) { - // early exit for simple name atch + // early exit for simple name match return true; } else if(Selectors::stringHasOperator($s)) { // selectors string @@ -243,39 +243,138 @@ class PageComparison { if(!empty($options['useDatabase'])) { $selectors->add(new SelectorEqual('id', $page->id))->add(new SelectorEqual('include', 'all')); return $page->wire()->pages->count($selectors) > 0; - } + } - $matches = false; + $matchFail = false; + $groupSelectors = array(); // same (1) item match group selectors + $orGroupSelectors = array(); // OR-group selectors foreach($selectors as $selector) { - $property = $selector->field; - $subproperty = ''; - - if(is_array($property)) $property = reset($property); - if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2); - if(in_array($property, $this->matchesIgnores)) continue; - - $matches = true; - $value = $page->getUnformatted($property); - - if(is_object($value)) { - // convert object to array value(s) - $value = $this->getObjectValueArray($value, $subproperty); - } else if(is_array($value)) { - // ok: selector matches will accept an array - } else { - // convert to a string value, whatever it may be - $value = "$value"; + if($selector->quote === '(') { + // OR-groups are handled below this loop + $orGroup = $selector->field() . '.'; + if(!isset($orGroupSelectors[$orGroup])) $orGroupSelectors[$orGroup] = array(); + $orGroupSelectors[$orGroup][] = $selector; + continue; } - if(!$selector->matches($value)) { - $matches = false; + if(!$this->selectorMatches($page, $selector)) { + $matchFail = true; break; } + + if($selector->group !== null) { + // validate that same (1) item matches from these later + $groupName = $selector->group; + if(!isset($groupSelectors[$groupName])) $groupSelectors[$groupName] = array(); + $groupSelectors[$groupName][] = $selector; + } } + + // OR-groups + if(!$matchFail && count($orGroupSelectors)) { + foreach($orGroupSelectors as /* $orGroupName => */ $selectors) { + $orGroupMatches = false; + foreach($selectors as $selector) { + if($this->matches($page, $selector->value)) { + // OR-group selector matches + $orGroupMatches = true; + break; + } + } + if(!$orGroupMatches) { + $matchFail = true; + break; + } + } + } + + // same (1) item match groups + if(!$matchFail && count($groupSelectors)) { + foreach($groupSelectors as /* $groupName => */ $selectors) { + $matchGroupKeys = null; + foreach($selectors as $selector) { + $keys = $selector->get('_matchGroupKeys'); // populated by selectorMatchesProperty + if($matchGroupKeys === null) { + $matchGroupKeys = $keys; + } else { + $matchGroupKeys = array_intersect($matchGroupKeys, $keys); + } + } + if(empty($matchGroupKeys)) { + $matchFail = true; + break; + } + } + } + + return !$matchFail; + } - return $matches; + /** + * Return whether individual Selector object matches Page + * + * @param Page $page + * @param Selector $selector + * @return bool + * + */ + protected function selectorMatches(Page $page, Selector $selector) { + $match = false; + $properties = $selector->fields(); + foreach($properties as $property) { + $match = $this->selectorMatchesProperty($page, $selector, $property); + if($match) break; + } + return $match; + } + + /** + * Return whether single property from individual Selector matches Page + * + * @param Page $page + * @param Selector $selector + * @param string $property + * @return bool + * + */ + protected function selectorMatchesProperty(Page $page, Selector $selector, $property) { + + $subproperty = ''; + if(strpos($property, '.')) list($property, $subproperty) = explode('.', $property, 2); + if(in_array($property, $this->matchesIgnores)) return true; + + if($selector->quote === '[' && Selectors::stringHasOperator($selector->value())) { + $selector->value = $page->wire()->pages->findIDs($selector->value()); + } + + $value = $page->getUnformatted($property); + + if(is_object($value)) { + // convert object to array value(s) + $value = $this->getObjectValueArray($value, $subproperty); + + } else if(is_array($value)) { + // ok: selector matches will accept an array + + } else { + // convert to a string value, whatever it may be + $value = "$value"; + } + + if(!$selector->matches($value)) return false; + + if($selector->group !== null && is_array($value)) { + // see which individual values match and record their keys for later comparison + $matchGroupKeys = array(); + foreach($value as $key => $val) { + if($selector->matches($val)) $matchGroupKeys[] = $key; + } + $selector->setQuietly('_matchGroupKeys', $matchGroupKeys); // used by matches() method + } + + return true; } /**