diff --git a/wire/core/WireHooks.php b/wire/core/WireHooks.php index dfa5a654..1c773d49 100644 --- a/wire/core/WireHooks.php +++ b/wire/core/WireHooks.php @@ -47,6 +47,7 @@ class WireHooks { * - fromClass: the name of the class containing the hooked method, if not the object where addHook was executed. Set automatically, but you may still use in some instances. * - argMatch: array of Selectors objects where the indexed argument (n) to the hooked method must match, order to execute hook. * - objMatch: Selectors object that the current object must match in order to execute hook + * - retMatch: Selectors object that must match the return value, or a match string to match return value * - public: auto-assigned to true or false by addHook() as to whether the method is public or private/protected. * */ @@ -58,7 +59,10 @@ class WireHooks { 'allInstances' => false, 'fromClass' => '', 'argMatch' => null, + 'argMatchType' => [], 'objMatch' => null, + 'retMatch' => null, + 'retMatchType' => '', ); /** @@ -631,8 +635,21 @@ class WireHooks { $options['fromClass'] = $fromClass; } + $retMatch = ''; $argOpen = strpos($method, '('); - if($argOpen) { + + if($argOpen) { + if(strpos($method, ':(')) { + list($method, $retMatch) = explode(':(', $method, 2); + $retMatch = rtrim($retMatch, ') '); + } else if(strpos($method, ':<') && substr(trim($method), -1) === '>') { + list($method, $retMatch) = explode(':<', $method, 2); + $retMatch = "<$retMatch"; + } + $argOpen = strpos($method, '('); + } + + if($argOpen) { // arguments to match may be specified in method name $argClose = strpos($method, ')'); if($argClose === $argOpen+1) { @@ -659,18 +676,31 @@ class WireHooks { // just single argument specified, so argument 0 is assumed } if(is_string($argMatch)) $argMatch = array(0 => $argMatch); + $argMatchType = []; foreach($argMatch as $argKey => $argVal) { - if(Selectors::stringHasSelector($argVal)) { - /** @var Selectors $selectors */ - $selectors = $this->wire->wire(new Selectors()); - $selectors->init($argVal); - $argMatch[$argKey] = $selectors; - } + list($argVal, $argValType) = $this->prepareArgMatch($argVal); + $argMatch[$argKey] = $argVal; + $argMatchType[$argKey] = $argValType; + } + if(count($argMatch)) { + $options['argMatch'] = $argMatch; + $options['argMatchType'] = $argMatchType; } - if(count($argMatch)) $options['argMatch'] = $argMatch; } + } else if(strpos($method, ':')) { + list($method, $retMatch) = explode(':', $method, 2); } + if($retMatch) { + // match return value + if($options['before'] && !$options['after']) { + throw new WireException('You cannot match return values with “before” hooks'); + } + list($retMatch, $retMatchType) = $this->prepareArgMatch($retMatch); + $options['retMatch'] = $retMatch; + $options['retMatchType'] = $retMatchType; + } + $localHooks = $object->getLocalHooks(); if($options['allInstances'] || $options['fromClass']) { @@ -996,50 +1026,25 @@ class WireHooks { if($type == 'method' && !empty($hook['options']['argMatch'])) { // argument comparison to determine at runtime whether to execute the hook $argMatches = $hook['options']['argMatch']; + $argMatchTypes = $hook['options']['argMatchType']; $matches = true; foreach($argMatches as $argKey => $argMatch) { /** @var Selectors $argMatch */ + $argMatchType = isset($argMatchTypes[$argKey]) ? $argMatchTypes[$argKey] : ''; $argVal = isset($arguments[$argKey]) ? $arguments[$argKey] : null; - if(is_object($argMatch)) { - // Selectors object - if(is_object($argVal)) { - $matches = $argMatch->matches($argVal); - } else { - // we don't work with non-object here - $matches = false; - } - } else if(is_string($argMatch) && strpos($argMatch, '<') === 0 && substr($argMatch, -1) === '>') { - // i.e. , , , , , etc. - $argMatch = trim($argMatch, '<>'); - if(strpos($argMatch, '|')) { - // i.e. or etc. - $argMatches = explode('|', str_replace(array('<', '>'), '', $argMatch)); - } else { - $argMatches = array($argMatch); - } - foreach($argMatches as $argMatchType) { - if(isset($this->argMatchTypes[$argMatchType])) { - $argMatchFunc = $this->argMatchTypes[$argMatchType]; - $matches = $argMatchFunc($argVal); - } else { - $matches = wireInstanceOf($argVal, $argMatchType); - } - if($matches) break; - } - } else { - if(is_array($argVal)) { - // match any array element - $matches = in_array($argMatch, $argVal); - } else { - // exact string match - $matches = $argMatch == $argVal; - } - } + $matches = $this->conditionalArgMatch($argMatch, $argVal, $argMatchType); if(!$matches) break; } if(!$matches) continue; // don't run hook } + if($type === 'method' && $when === 'after' && !empty($hook['options']['retMatch'])) { + if(!$this->conditionalArgMatch( + $hook['options']['retMatch'], + $result['return'], + $hook['options']['retMatchType'])) continue; + } + if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) { $allowRunPathHook = $this->allowRunPathHook($hook['id'], $arguments); $this->removeHook($object, $hook['id']); // once only @@ -1135,9 +1140,104 @@ class WireHooks { } /** - * Allow given path hook to run? + * Prepare argument match * - * This checks if the hook’s path matches the request path, allowing for both + * @param string $argMatch + * @return array + * @since 3.0.247 + * + */ + protected function prepareArgMatch($argMatch) { + $argMatch = trim($argMatch, '()'); + $argMatchType = ''; + + list($c1, $c2, $c3) = [ substr($argMatch, 0, 1), substr($argMatch, -1), substr($argMatch, 0, 2) ]; + + if($c1 === '<' && $c2 === '>') { + // i.e. or + $argMatchType = 'instanceof'; + $argMatch = trim($argMatch, '<>'); + + } else if($c1 === '=' || $c1 === '<' || $c1 === '>' || Selectors::isOperator($c3)) { + // selector that starts with operator and translates to "argVal matches argMatch" + $argMatch = "___val$argMatch"; // i.e. ___val=something + $argMatchType = 'selector'; + } + + if($argMatchType === 'instanceof') { + // ok + $argMatch = strpos($argMatch, '|') ? explode('|', $argMatch) : [ $argMatch ]; + } else if(Selectors::stringHasSelector($argMatch)) { + /** @var Selectors $selectors */ + $selectors = $this->wire->wire(new Selectors()); + $selectors->init($argMatch); + $argMatch = $selectors; + $argMatchType = 'selector'; + } else { + $argMatchType = 'equals'; + } + + return [ $argMatch, $argMatchType ]; + } + + /** + * Does given value match given match condition? + * + * @param Selectors|string $argMatch + * @param mixed $argVal + * @return bool + * @since 3.0.247 + * + */ + protected function conditionalArgMatch($argMatch, $argVal, $argMatchType) { + + $matches = false; + + if($argMatch instanceof Selectors) { + // Selectors object + /** @var Selector $s */ + $s = $argMatch->first(); + if($s instanceof Selector && $s->field() === '___val') { + $o = WireData(); + $o->set('value', $argVal); + $s->field = 'value'; + $argVal = $o; + } else if(is_array($argVal)) { + $argVal = count($argVal) && is_string(key($argVal)) ? WireData($argVal) : WireArray($argVal); + } + if(is_object($argVal)) { + $matches = $argMatch->matches($argVal); + } + + } else if($argMatchType === 'instanceof') { + if(!is_array($argMatch)) $argMatch = [ $argMatch ]; + foreach($argMatch as $type) { + if(isset($this->argMatchTypes[$type])) { + $argMatchFunc = $this->argMatchTypes[$type]; + $matches = $argMatchFunc($argVal); + } else { + $matches = wireInstanceOf($argVal, $type); + } + if($matches) break; + } + + } else if(is_array($argVal)) { + // match any array element + $matches = in_array($argMatch, $argVal); + + } else { + // exact match + $matches = $argMatch == $argVal; + } + + return $matches; + + } + + /** + * Allow given path hook to run? + * + * This checks if the hook’s path matches the request path, allowing for both * regular and regex matches and populating parenthesized portions to arguments * that will appear in the HookEvent. *