mirror of
https://github.com/processwire/processwire.git
synced 2025-08-10 08:44:46 +02:00
Add support for conditional hooks that match method return values. For details see: https://processwire.com/docs/modules/hooks/#conditional-hooks-matching-return-value-or-type
This commit is contained in:
@@ -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. <Page>, <User>, <string>, <object>, <bool>, etc.
|
||||
$argMatch = trim($argMatch, '<>');
|
||||
if(strpos($argMatch, '|')) {
|
||||
// i.e. <User|Role|Permission> or <int|float> 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. <WireArray> or <ThisPage|ThatPage>
|
||||
$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.
|
||||
*
|
||||
|
Reference in New Issue
Block a user