1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-13 18:24:57 +02:00

Update WireHooks ($hooks API var) to support path hooks

This commit is contained in:
Ryan Cramer
2021-03-05 15:12:23 -05:00
parent 94e71209b3
commit 652a8a58e3

View File

@@ -5,7 +5,7 @@
*
* This class is for internal use. You should manipulate hooks from Wire-derived classes instead.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
*/
@@ -68,7 +68,21 @@ class WireHooks {
* It is for internal use only. See also $defaultHookOptions[allInstances].
*
*/
protected $staticHooks = array();
protected $staticHooks = array(
// 'SomeClass' => [
// 'someMethod' => [ hooks ],
// 'someOtherMethod' => [ hooks ]
// ],
// 'AnotherClass' => [
// 'anotherMethod' => [ hooks ]
// ]
);
/**
* @var array
*
*/
protected $pathHooks = array();
/**
* A cache of all hook method/property names for an optimization.
@@ -79,7 +93,10 @@ class WireHooks {
* This cache exists primarily to gain some speed in our __get and __call methods.
*
*/
protected $hookMethodCache = array();
protected $hookMethodCache = array(
// 'method()' => true,
// 'property' => true,
);
/**
* Same as hook method cache but for "Class::method"
@@ -87,7 +104,10 @@ class WireHooks {
* @var array
*
*/
protected $hookClassMethodCache = array();
protected $hookClassMethodCache = array(
// 'Class::method()' => true,
// 'Class::property' => true,
);
/**
* Cache of all local hooks combined, for debugging purposes
@@ -115,6 +135,24 @@ class WireHooks {
*/
protected $debugTimers = array();
/**
* Characters that can begin a path hook definition (i.e. '/path/' or '!regex!', etc.)
*
* @var string
*
*/
protected $pathHookStarts = '/!@#%.([^';
/**
* Allow use of path hooks?
*
* This should be set to false once reaching the boot stage where it no longer applies.
*
* @var bool
*
*/
protected $allowPathHooks = true;
/**
* @var ProcessWire
*
@@ -506,18 +544,28 @@ class WireHooks {
$toMethod = null;
}
if(is_null($toMethod)) {
if($toMethod === null) {
// $toObject has been omitted and a procedural function specified instead
// $toObject may also be a closure
$toMethod = $toObject;
$toObject = null;
}
if($toMethod === null) {
throw new WireException("Method to call is required and was not specified (toMethod)");
}
if(strpos($method, '___') === 0) {
$method = substr($method, 3);
} else if(strpos($this->pathHookStarts, $method[0]) !== false) {
return $this->addPathHook($object, $method, $toObject, $toMethod, $options);
}
if(method_exists($object, $method)) {
throw new WireException("Method " . $object->className() . "::$method is not hookable");
}
$options = array_merge($this->defaultHookOptions, $options);
if(is_null($toMethod)) throw new WireException("Method to call is required and was not specified (toMethod)");
if(strpos($method, '___') === 0) $method = substr($method, 3);
if(method_exists($object, $method)) throw new WireException("Method " . $object->className() . "::$method is not hookable");
// determine whether the hook handling method is public or private/protected
$toPublic = true;
@@ -754,6 +802,35 @@ class WireHooks {
return $result;
}
/**
* Add a hook that handles a request path
*
* @param Wire $object
* @param string $path
* @param Wire|null|callable $toObject
* @param string $toMethod
* @param array $options
* @return string
* @throws WireException
*
*/
protected function addPathHook(Wire $object, $path, $toObject, $toMethod, $options = array()) {
if(!$this->allowPathHooks) throw new WireException('Path hooks must be attached during init or ready states');
$method = 'ProcessPageView::pathHooks';
$id = $this->addHook($object, $method, $toObject, $toMethod, $options);
$filters = array();
$filterPath = trim(str_replace(array('-', '_', '.'), '/', $path), '/');
foreach(explode('/', $filterPath) as $filter) {
// identify any non-regex portions to use as pre-filters before using regexes
if(ctype_alnum($filter) && strlen($filter) > 1) $filters[] = $filter;
}
$this->pathHooks[$id] = array(
'match' => $path,
'filters' => array(),
);
return $id;
}
/**
* Provides the implementation for calling hooks in ProcessWire
*
@@ -784,6 +861,7 @@ class WireHooks {
$profiler = $this->wire->wire('profiler');
$hooks = null;
$methodExists = false;
$useHookReturnValue = false; // allow use of "return $value;" in hook in addition to $event->return ?
if($type === 'method') {
$methodExists = method_exists($object, $realMethod);
@@ -876,6 +954,12 @@ class WireHooks {
}
if(!$matches) continue; // don't run hook
}
if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) {
if(!$this->allowRunPathHook($hook, $arguments)) continue;
$this->removeHook($object, $hook['id']); // once only
$useHookReturnValue = true;
}
$event = new HookEvent(array(
'object' => $object,
@@ -930,8 +1014,13 @@ class WireHooks {
}
if($returnValue !== null) {
// hook method/func had an explicit return statement with a value
// allow for use of $returnValue as alternative to $event->return?
// hook method/func had an explicit 'return $value;' statement
// we can optionally use this rather than $event->return. Can be useful
// in cases where a return value doesnt need to be passed around to
// more than one hook
if($useHookReturnValue) {
$event->return = $returnValue;
}
}
if($profilerEvent) $profiler->stop($profilerEvent);
@@ -959,6 +1048,66 @@ class WireHooks {
return $result;
}
/**
* Allow given path hook to run?
*
* This checks if the hooks path matches the request path, allowing for both
* regular and regex matches and populating parenthesized portions to arguments
* that will appear in the HookEvent.
*
* @param array $hook
* @param array $arguments
* @return bool
* @since 3.0.173
*
*/
protected function allowRunPathHook(array $hook, array &$arguments) {
$id = $hook['id'];
$pathHook = $this->pathHooks[$id];
$matchPath = $pathHook['match'];
$requestPath = $arguments[0];
$filterFail = false;
// first pre-filter the requestPath against any words matchPath (filters)
foreach($pathHook['filters'] as $filter) {
if(strpos($requestPath, $filter) !== false) continue;
$filterFail = true;
break;
}
if($filterFail) return false;
if(strpos('!@#%', $matchPath[0]) !== false) {
// already in delimited regex format
} else {
// needs to be in regex format
if(strpos($matchPath, '/') === 0) $matchPath = "^$matchPath";
$matchPath = "!$matchPath$!";
}
if(strpos($matchPath, ':') && strpos($matchPath, '(') !== false) {
// named arguments converted to named PCRE capture groups
$matchPath = preg_replace('!\(([-_a-z0-9]+):!i', '(?P<$1>', $matchPath);
}
if(!preg_match($matchPath, $requestPath, $matches)) {
// if match fails, try again with trailing slash state reversed
if(substr($requestPath, -1) === '/') {
$requestPath = rtrim($requestPath, '/');
} else {
$requestPath .= '/';
}
if(!preg_match($matchPath, $requestPath, $matches)) return false;
}
foreach($matches as $key => $value) {
if($key !== 0) $arguments[$key] = $value;
}
return true;
}
/**
* Filter and return hooks matching given property and value
*
@@ -1034,7 +1183,7 @@ class WireHooks {
$object->setLocalHooks($localHooks);
} else {
// static hook
unset($this->staticHooks[$hookClass][$method][$priority]);
unset($this->staticHooks[$hookClass][$method][$priority], $this->pathHooks[$hookID]);
if(empty($this->staticHooks[$hookClass][$method])) {
unset($this->hookClassMethodCache["$hookClass::$method"]);
}
@@ -1070,6 +1219,41 @@ class WireHooks {
return $this->allLocalHooks;
}
/**
* Return all pending path hooks
*
* @return array
* @since 3.0.173
*
*/
public function getAllPathHooks() {
return $this->pathHooks;
}
/**
* Return whether or not any path hooks are pending
*
* @return int
* @since 3.0.173
*
*/
public function hasPathHooks() {
return count($this->pathHooks) > 0;
}
/**
* Get or set whether path hooks are allowed
*
* @param bool|null $allow
* @return bool
* @since 3.0.173
*
*/
public function allowPathHooks($allow = null) {
if($allow !== null) $this->allowPathHooks = (bool) $allow;
return $this->allowPathHooks;
}
/**
* @return string
*