1440 lines
45 KiB
PHP
1440 lines
45 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire Hooks Manager
|
||
*
|
||
* This class is for internal use. You should manipulate hooks from Wire-derived classes instead.
|
||
*
|
||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
*/
|
||
|
||
class WireHooks {
|
||
|
||
/**
|
||
* Debug hooks
|
||
*
|
||
*/
|
||
const ___debug = false;
|
||
|
||
/**
|
||
* Refers to ALL hooks
|
||
*
|
||
*/
|
||
const getHooksAll = 0;
|
||
|
||
/**
|
||
* Refers only to LOCAL hooks
|
||
*
|
||
*/
|
||
const getHooksLocal = 1;
|
||
|
||
/**
|
||
* Refers only to STATIC hooks
|
||
*
|
||
*/
|
||
const getHooksStatic = 2;
|
||
|
||
/**
|
||
* When a hook is specified, there are a few options which can be overridden: This array outlines those options and the defaults.
|
||
*
|
||
* - type: may be either 'method' or 'property'. If property, then it will respond to $obj->property rather than $obj->method().
|
||
* - before: execute the hook before the method call? Not applicable if 'type' is 'property'.
|
||
* - after: execute the hook after the method call? (allows modification of return value). Not applicable if 'type' is 'property'.
|
||
* - priority: a number determining the priority of a hook, where lower numbers are executed before higher numbers.
|
||
* - allInstances: attach the hook to all instances of this object? (store in staticHooks rather than localHooks). Set automatically, but you may still use in some instances.
|
||
* - 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
|
||
* - public: auto-assigned to true or false by addHook() as to whether the method is public or private/protected.
|
||
*
|
||
*/
|
||
protected $defaultHookOptions = array(
|
||
'type' => 'method',
|
||
'before' => false,
|
||
'after' => true,
|
||
'priority' => 100,
|
||
'allInstances' => false,
|
||
'fromClass' => '',
|
||
'argMatch' => null,
|
||
'objMatch' => null,
|
||
);
|
||
|
||
/**
|
||
* Static hooks are applicable to all instances of the descending class.
|
||
*
|
||
* This array holds references to those static hooks, and is shared among all classes descending from Wire.
|
||
* It is for internal use only. See also $defaultHookOptions[allInstances].
|
||
*
|
||
*/
|
||
protected $staticHooks = array(
|
||
// 'SomeClass' => [
|
||
// 'someMethod' => [ hooks ],
|
||
// 'someOtherMethod' => [ hooks ]
|
||
// ],
|
||
// 'AnotherClass' => [
|
||
// 'anotherMethod' => [ hooks ]
|
||
// ]
|
||
);
|
||
|
||
/**
|
||
* @var array
|
||
*
|
||
*/
|
||
protected $pathHooks = array(
|
||
// 'HookID' => [
|
||
// 'match' => '/foo/bar/{baz}/(.+)/',
|
||
// 'filters' => [ 0 => '/foo/', 2 => '/bar/' ],
|
||
// ], ...
|
||
// ]
|
||
);
|
||
|
||
/**
|
||
* A cache of all hook method/property names for an optimization.
|
||
*
|
||
* Hooked methods end with '()' while hooked properties don't.
|
||
*
|
||
* This does not distinguish which instance it was added to or whether it was removed.
|
||
* This cache exists primarily to gain some speed in our __get and __call methods.
|
||
*
|
||
*/
|
||
protected $hookMethodCache = array(
|
||
// 'method()' => true,
|
||
// 'property' => true,
|
||
);
|
||
|
||
/**
|
||
* Same as hook method cache but for "Class::method"
|
||
*
|
||
* @var array
|
||
*
|
||
*/
|
||
protected $hookClassMethodCache = array(
|
||
// 'Class::method()' => true,
|
||
// 'Class::property' => true,
|
||
);
|
||
|
||
/**
|
||
* Cache of all local hooks combined, for debugging purposes
|
||
*
|
||
*/
|
||
protected $allLocalHooks = array();
|
||
|
||
/**
|
||
* Cached parent classes and interfaces
|
||
*
|
||
* @var array of class|interface => [ 'parentClass', 'parentClass', 'interface', 'interface', 'etc.' ]
|
||
*
|
||
*/
|
||
protected $parentClasses = array();
|
||
|
||
/**
|
||
* @var Config
|
||
*
|
||
*/
|
||
protected $config;
|
||
|
||
/**
|
||
* @var array
|
||
*
|
||
*/
|
||
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;
|
||
|
||
/**
|
||
* Populated when a path hook requires a redirect
|
||
*
|
||
* @var string
|
||
*
|
||
*/
|
||
protected $pathHookRedirect = '';
|
||
|
||
/**
|
||
* @var ProcessWire
|
||
*
|
||
*/
|
||
protected $wire;
|
||
|
||
/**
|
||
* Conditional argument match types and the PHP function to detect them
|
||
*
|
||
* @var string[]
|
||
*
|
||
*/
|
||
protected $argMatchTypes = array(
|
||
'array' => 'is_array',
|
||
'bool' => 'is_bool',
|
||
'float' => 'is_float',
|
||
'int' => 'is_int',
|
||
'null' => 'is_null',
|
||
'object' => 'is_object',
|
||
'string' => 'is_string',
|
||
);
|
||
|
||
/**
|
||
* Construct WireHooks
|
||
*
|
||
* @param ProcessWire $wire
|
||
* @param Config $config
|
||
*
|
||
*/
|
||
public function __construct(ProcessWire $wire, Config $config) {
|
||
$this->wire = $wire;
|
||
$this->config = $config;
|
||
}
|
||
|
||
/**
|
||
* Return all hooks associated with $object or method (if specified)
|
||
*
|
||
* @param Wire $object
|
||
* @param string $method Optional method that hooks will be limited to. Or specify '*' to return all hooks everywhere.
|
||
* @param int $getHooks Get hooks of type, specify one of the following constants:
|
||
* - WireHooks::getHooksAll returns all hooks [0] (default)
|
||
* - WireHooks::getHooksLocal returns local hooks [1] only
|
||
* - WireHooks::getHooksStatic returns static hooks [2] only
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getHooks(Wire $object, $method = '', $getHooks = self::getHooksAll) {
|
||
|
||
$hooks = array();
|
||
|
||
// see if we can do a quick exit
|
||
if($method && $method !== '*' && !$this->isHookedOrParents($object, $method)) return $hooks;
|
||
|
||
// first determine which local hooks when should include
|
||
if($getHooks !== self::getHooksStatic) {
|
||
$localHooks = $object->getLocalHooks();
|
||
if($method && $method !== '*') {
|
||
// populate all local hooks for given method
|
||
if(isset($localHooks[$method])) $hooks = $localHooks[$method];
|
||
} else {
|
||
// populate all local hooks, regardless of method
|
||
// note: sort of return hooks is no longer priority based
|
||
// @todo account for '*' method, which should return all hooks regardless of instance
|
||
foreach($localHooks as $method => $methodHooks) {
|
||
$hooks = array_merge(array_values($hooks), array_values($methodHooks));
|
||
}
|
||
}
|
||
}
|
||
|
||
// if only local hooks requested, we can return them now
|
||
if($getHooks === self::getHooksLocal) return $hooks;
|
||
|
||
$needSort = false;
|
||
$namespace = __NAMESPACE__ ? __NAMESPACE__ . "\\" : "";
|
||
$objectParentNamespaces = array();
|
||
|
||
// join in static hooks
|
||
foreach($this->staticHooks as $className => $staticHooks) {
|
||
$_className = $namespace . $className;
|
||
if(!$object instanceof $_className && $method !== '*') {
|
||
$_namespace = wireClassName($object, 1) . "\\";
|
||
if($_namespace !== $namespace) {
|
||
// objects in other namespaces
|
||
$_className = $_namespace . $className;
|
||
if(!$object instanceof $_className) { // && $method !== '*') {
|
||
// object likely extends a class not in PW namespace, so check class parents instead
|
||
if(empty($objectParentNamespaces)) {
|
||
foreach(wireClassParents($object) as $nscn => $cn) {
|
||
list($ns,) = explode("\\", $nscn);
|
||
$objectParentNamespaces[$ns] = $ns;
|
||
}
|
||
}
|
||
$nsok = false;
|
||
foreach($objectParentNamespaces as $ns) {
|
||
$_className = "$ns\\$className";
|
||
if(!$object instanceof $_className) continue;
|
||
$nsok = true;
|
||
break;
|
||
}
|
||
if(!$nsok) continue;
|
||
}
|
||
} else {
|
||
continue;
|
||
}
|
||
}
|
||
// join in any related static hooks to the local hooks
|
||
if($method && $method !== '*') {
|
||
// retrieve all static hooks for method
|
||
if(!empty($staticHooks[$method])) {
|
||
if(count($hooks)) {
|
||
$collisions = array_intersect_key($hooks, $staticHooks[$method]);
|
||
$hooks = array_merge($hooks, $staticHooks[$method]);
|
||
if(count($collisions)) {
|
||
// identify and resolve priority collisions
|
||
foreach($collisions as $priority => $hook) {
|
||
$n = 0;
|
||
while(isset($hooks["$priority.$n"])) $n++;
|
||
$hooks["$priority.$n"] = $hook;
|
||
}
|
||
}
|
||
$needSort = true;
|
||
} else {
|
||
$hooks = $staticHooks[$method];
|
||
}
|
||
}
|
||
} else {
|
||
// no method specified, retrieve all for class
|
||
// note: priority-based array indexes are no longer in tact
|
||
$hooks = array_values($hooks);
|
||
foreach($staticHooks as /* $_method => */ $methodHooks) {
|
||
$hooks = array_merge($hooks, array_values($methodHooks));
|
||
}
|
||
}
|
||
}
|
||
|
||
if($needSort && count($hooks) > 1) {
|
||
defined("SORT_NATURAL") ? ksort($hooks, SORT_NATURAL) : uksort($hooks, "strnatcmp");
|
||
}
|
||
|
||
return $hooks;
|
||
}
|
||
|
||
/**
|
||
* Returns true if the method/property hooked, false if it isn't.
|
||
*
|
||
* This is for optimization use. It does not distinguish about class instance.
|
||
* It only distinguishes about class if you provide a class with the $method argument (i.e. Class::).
|
||
* As a result, a true return value indicates something "might" be hooked, as opposed to be
|
||
* being definitely hooked.
|
||
*
|
||
* If checking for a hooked method, it should be in the form `Class::method()` or `method()` (with parenthesis).
|
||
* If checking for a hooked property, it should be in the form `Class::property` or `property`.
|
||
*
|
||
* If you need to check if a method/property is hooked, including any of its parent classes, use
|
||
* the `WireHooks::isMethodHooked()`, `WireHooks::isPropertyHooked()`, or `WireHooks::hasHook()` methods instead.
|
||
*
|
||
* @param string $method Method or property name in one of the following formats:
|
||
* Class::method()
|
||
* Class::property
|
||
* method()
|
||
* property
|
||
* @param Wire|null $instance Optional instance to check against (see hasHook method for details)
|
||
* Note that if specifying an $instance, you may not use the Class::method() or Class::property options for $method argument.
|
||
* @return bool
|
||
* @see WireHooks::isMethodHooked(), WireHooks::isPropertyHooked(), WireHooks::hasHook()
|
||
*
|
||
*/
|
||
public function isHooked($method, Wire $instance = null) {
|
||
if($instance) return $this->hasHook($instance, $method);
|
||
if(strpos($method, ':') !== false) {
|
||
$hooked = isset($this->hookClassMethodCache[$method]); // fromClass::method() or fromClass::property
|
||
} else {
|
||
$hooked = isset($this->hookMethodCache[$method]); // method() or property
|
||
}
|
||
return $hooked;
|
||
}
|
||
|
||
/**
|
||
* Similar to isHooked() method but also checks parent classes for the hooked method as well
|
||
*
|
||
* This method is designed for fast determinations of whether something is hooked
|
||
*
|
||
* @param string|Wire $class
|
||
* @param string $method Name of method or property
|
||
* @param string $type May be either 'method', 'property' or 'either'
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function isHookedOrParents($class, $method, $type = 'either') {
|
||
|
||
$property = '';
|
||
if(is_object($class)) {
|
||
$className = wireClassName($class);
|
||
$object = $class;
|
||
} else {
|
||
$className = $class;
|
||
$object = null;
|
||
}
|
||
|
||
if($object) {
|
||
// first check local hooks attached to this instance
|
||
$localHooks = $object->getLocalHooks();
|
||
if(!empty($localHooks[rtrim($method, '()')])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if($type == 'method' || $type == 'either') {
|
||
if(strpos($method, '(') === false) $method .= '()';
|
||
if($type == 'either') $property = rtrim($method, '()');
|
||
} else {
|
||
$property = rtrim($method, '()');
|
||
}
|
||
|
||
if($type == 'method') {
|
||
if(!isset($this->hookMethodCache[$method])) return false; // not hooked for any class
|
||
$hooked = isset($this->hookClassMethodCache["$className::$method"]);
|
||
} else if($type == 'property') {
|
||
if(!isset($this->hookMethodCache[$property])) return false; // not hooked for any class
|
||
$hooked = isset($this->hookClassMethodCache["$className::$property"]);
|
||
} else {
|
||
if(!isset($this->hookMethodCache[$method])
|
||
&& !isset($this->hookMethodCache[$property])) return false;
|
||
$hooked = isset($this->hookClassMethodCache["$className::$property"]) ||
|
||
isset($this->hookClassMethodCache["$className::$method"]);
|
||
}
|
||
|
||
if(!$hooked) {
|
||
foreach($this->getClassParents($class) as $parentClass) {
|
||
if($type == 'method') {
|
||
if(isset($this->hookClassMethodCache["$parentClass::$method"])) {
|
||
$hooked = true;
|
||
$this->hookClassMethodCache["$class::$method"] = true;
|
||
}
|
||
} else if($type == 'property') {
|
||
if(isset($this->hookClassMethodCache["$parentClass::$property"])) {
|
||
$hooked = true;
|
||
$this->hookClassMethodCache["$class::$property"] = true;
|
||
}
|
||
} else {
|
||
if(isset($this->hookClassMethodCache["$parentClass::$method"])) {
|
||
$hooked = true;
|
||
$this->hookClassMethodCache["$class::$method"] = true;
|
||
}
|
||
if(!$hooked && isset($this->hookClassMethodCache["$parentClass::$property"])) {
|
||
$hooked = true;
|
||
$this->hookClassMethodCache["$class::$property"] = true;
|
||
}
|
||
}
|
||
if($hooked) break;
|
||
}
|
||
}
|
||
|
||
return $hooked;
|
||
}
|
||
|
||
/**
|
||
* Similar to isHooked() method but also checks parent classes for the hooked method as well
|
||
*
|
||
* This method is designed for fast determinations of whether something is hooked
|
||
*
|
||
* @param string|Wire $class
|
||
* @param string $method Name of method
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isMethodHooked($class, $method) {
|
||
return $this->isHookedOrParents($class, $method, 'method');
|
||
}
|
||
|
||
/**
|
||
* Similar to isHooked() method but also checks parent classes for the hooked property as well
|
||
*
|
||
* This method is designed for fast determinations of whether something is hooked
|
||
*
|
||
* @param string|Wire $class
|
||
* @param string $property Name of property
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function isPropertyHooked($class, $property) {
|
||
return $this->isHookedOrParents($class, $property, 'property');
|
||
}
|
||
|
||
/**
|
||
* Similar to isHooked(), returns true if the method or property hooked, false if it isn't.
|
||
*
|
||
* Accomplishes the same thing as the isHooked() method, but this is more accurate,
|
||
* and potentially slower than isHooked(). Less for optimization use, more for accuracy use.
|
||
*
|
||
* It checks for both static hooks and local hooks, but only accepts a method() or property
|
||
* name as an argument (i.e. no Class::something) since the class context is assumed from the current
|
||
* instance. Unlike isHooked() it also analyzes the instance's class parents for hooks, making it
|
||
* more accurate. As a result, this method works well for more than just optimization use.
|
||
*
|
||
* If checking for a hooked method, it should be in the form "method()".
|
||
* If checking for a hooked property, it should be in the form "property".
|
||
*
|
||
* @param Wire $object
|
||
* @param string $method Method() or property name
|
||
* @return bool
|
||
* @throws WireException whe you try to call it with a Class::something() type method.
|
||
* @todo differentiate between "method()" and "property"
|
||
*
|
||
*/
|
||
public function hasHook(Wire $object, $method) {
|
||
|
||
$hooked = false;
|
||
if(strpos($method, '::') !== false) {
|
||
throw new WireException("You may only specify a 'method()' or 'property', not 'Class::something'.");
|
||
}
|
||
|
||
// quick exit when possible
|
||
if(!isset($this->hookMethodCache[$method])) return false;
|
||
|
||
$_method = rtrim($method, '()');
|
||
$localHooks = $object->getLocalHooks();
|
||
|
||
if(!empty($localHooks[$_method])) {
|
||
// first check local hooks attached to this instance
|
||
$hooked = true;
|
||
} else if(!empty($this->staticHooks[$object->className()][$_method])) {
|
||
// now check if hooked in this class
|
||
$hooked = true;
|
||
} else {
|
||
// check parent classes and interfaces
|
||
foreach($this->getClassParents($object) as $class) {
|
||
if(!empty($this->staticHooks[$class][$_method])) {
|
||
$hooked = true;
|
||
$this->hookClassMethodCache["$class::$method"] = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $hooked;
|
||
}
|
||
|
||
/**
|
||
* Get an array of parent classes and interfaces for the given object
|
||
*
|
||
* @param Wire|string $object Maybe either object instance or class name
|
||
* @param bool $cache Allow use of cache for getting or storing? (default=true)
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getClassParents($object, $cache = true) {
|
||
if(is_string($object)) {
|
||
$className = $object;
|
||
} else {
|
||
$className = $object->className();
|
||
}
|
||
if($cache && isset($this->parentClasses[$className])) {
|
||
$classes = $this->parentClasses[$className];
|
||
} else {
|
||
$classes = wireClassParents($object, false);
|
||
$interfaces = wireClassImplements($object);
|
||
if(is_array($interfaces)) $classes = array_merge($interfaces, $classes);
|
||
if($cache) $this->parentClasses[$className] = $classes;
|
||
}
|
||
return $classes;
|
||
}
|
||
|
||
|
||
/**
|
||
* Hook a function/method to a hookable method call in this object
|
||
*
|
||
* Hookable method calls are methods preceded by three underscores.
|
||
* You may also specify a method that doesn't exist already in the class
|
||
* The hook method that you define may be part of a class or a globally scoped function.
|
||
*
|
||
* If you are hooking a procedural function, you may omit the $toObject and instead just call via:
|
||
* $this->addHook($method, 'function_name'); or $this->addHook($method, 'function_name', $options);
|
||
*
|
||
* @param Wire $object
|
||
* @param string|array $method Method name to hook into, NOT including the three preceding underscores.
|
||
* May also be Class::Method for same result as using the fromClass option.
|
||
* May also be array OR CSV string of either of the above to add multiple (since 3.0.137).
|
||
* @param object|null|callable $toObject Object to call $toMethod from,
|
||
* Or null if $toMethod is a function outside of an object,
|
||
* Or function|callable if $toObject is not applicable or function is provided as a closure.
|
||
* @param string|array $toMethod Method from $toObject, or function name to call on a hook event, or $options array.
|
||
* @param array $options See $defaultHookOptions at the beginning of this class. Optional.
|
||
* @return string A special Hook ID that should be retained if you need to remove the hook later.
|
||
* If the $method argument was a CSV string or array of multiple methods to hook, then CSV string of hook IDs
|
||
* will be returned, and the same CSV string can be used with removeHook() calls. (since 3.0.137).
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function addHook(Wire $object, $method, $toObject, $toMethod = null, $options = array()) {
|
||
|
||
if(empty($options['noAddHooks']) && (is_array($method) || strpos($method, ',') !== false)) {
|
||
// potentially multiple methods to hook in $method argument
|
||
return $this->addHooks($object, $method, $toObject, $toMethod, $options);
|
||
}
|
||
|
||
if(is_array($toMethod)) {
|
||
// $options array specified as 3rd argument
|
||
if(count($options)) {
|
||
// combine $options from addHookBefore/After and user specified options
|
||
$options = array_merge($toMethod, $options);
|
||
} else {
|
||
$options = $toMethod;
|
||
}
|
||
$toMethod = null;
|
||
}
|
||
|
||
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);
|
||
|
||
// determine whether the hook handling method is public or private/protected
|
||
$toPublic = true;
|
||
if($toObject) {
|
||
if(method_exists($toObject, $toMethod)) $_toMethod = $toMethod;
|
||
else if(method_exists($toObject, "___$toMethod")) $_toMethod = "___$toMethod";
|
||
else $_toMethod = null;
|
||
if($_toMethod) {
|
||
try {
|
||
$ref = new \ReflectionMethod($toObject, $_toMethod);
|
||
$toPublic = $ref->isPublic();
|
||
} catch(\Exception $e) {
|
||
$toPublic = false;
|
||
}
|
||
}
|
||
unset($ref);
|
||
}
|
||
|
||
if(strpos($method, '::')) {
|
||
list($fromClass, $method) = explode('::', $method, 2);
|
||
if(strpos($fromClass, '(') !== false) {
|
||
// extract object selector match string
|
||
list($fromClass, $objMatch) = explode('(', $fromClass, 2);
|
||
$objMatch = trim($objMatch, ') ');
|
||
if(Selectors::stringHasSelector($objMatch)) {
|
||
/** @var Selectors $selectors */
|
||
$selectors = $this->wire->wire(new Selectors());
|
||
$selectors->init($objMatch);
|
||
$objMatch = $selectors;
|
||
}
|
||
if($objMatch) $options['objMatch'] = $objMatch;
|
||
}
|
||
$options['fromClass'] = $fromClass;
|
||
}
|
||
|
||
$argOpen = strpos($method, '(');
|
||
if($argOpen) {
|
||
// arguments to match may be specified in method name
|
||
$argClose = strpos($method, ')');
|
||
if($argClose === $argOpen+1) {
|
||
// method just has a "()" which can be discarded
|
||
$method = rtrim($method, '() ');
|
||
} else if($argClose > $argOpen+1) {
|
||
// extract argument selector match string(s), arg 0: Something::something(selector_string)
|
||
// or: Something::something(1:selector_string, 3:selector_string) matches arg 1 and 3.
|
||
list($method, $argMatch) = explode('(', $method, 2);
|
||
$argMatch = trim($argMatch, ') ');
|
||
if(strpos($argMatch, ':') !== false) {
|
||
// zero-based argument indexes specified, i.e. 0:template=product, 1:order_status
|
||
$args = preg_split('/\b([0-9]):/', trim($argMatch), -1, PREG_SPLIT_DELIM_CAPTURE);
|
||
if(count($args)) {
|
||
$argMatch = array();
|
||
array_shift($args); // blank
|
||
while(count($args)) {
|
||
$argKey = (int) trim(array_shift($args));
|
||
$argVal = trim(array_shift($args), ', ');
|
||
$argMatch[$argKey] = $argVal;
|
||
}
|
||
}
|
||
} else {
|
||
// just single argument specified, so argument 0 is assumed
|
||
}
|
||
if(is_string($argMatch)) $argMatch = array(0 => $argMatch);
|
||
foreach($argMatch as $argKey => $argVal) {
|
||
if(Selectors::stringHasSelector($argVal)) {
|
||
/** @var Selectors $selectors */
|
||
$selectors = $this->wire->wire(new Selectors());
|
||
$selectors->init($argVal);
|
||
$argMatch[$argKey] = $selectors;
|
||
}
|
||
}
|
||
if(count($argMatch)) $options['argMatch'] = $argMatch;
|
||
}
|
||
}
|
||
|
||
$localHooks = $object->getLocalHooks();
|
||
|
||
if($options['allInstances'] || $options['fromClass']) {
|
||
// hook all instances of this class
|
||
$hookClass = $options['fromClass'] ? $options['fromClass'] : $object->className();
|
||
if(!isset($this->staticHooks[$hookClass])) $this->staticHooks[$hookClass] = array();
|
||
$hooks =& $this->staticHooks[$hookClass];
|
||
$options['allInstances'] = true;
|
||
$local = 0;
|
||
|
||
} else {
|
||
// hook only this instance
|
||
$hookClass = '';
|
||
$hooks =& $localHooks;
|
||
$local = 1;
|
||
}
|
||
|
||
$priority = (string) $options['priority'];
|
||
|
||
if(!isset($hooks[$method])) {
|
||
if(ctype_digit($priority)) $priority = "$priority.0";
|
||
|
||
} else {
|
||
if(strpos($priority, '.')) {
|
||
// priority already specifies a sub value: extract it
|
||
list($priority, $n) = explode('.', $priority);
|
||
$options['priority'] = $priority; // without $n
|
||
$priority .= ".$n";
|
||
} else {
|
||
$n = 0;
|
||
$priority .= ".0";
|
||
}
|
||
// come up with a priority that is unique for this class/method across both local and static hooks
|
||
while(($hookClass && isset($this->staticHooks[$hookClass][$method][$priority]))
|
||
|| isset($localHooks[$method][$priority])) {
|
||
$n++;
|
||
$priority = "$options[priority].$n";
|
||
}
|
||
}
|
||
|
||
// Note hookClass is always blank when this is a local hook
|
||
$id = "$hookClass:$priority:$method";
|
||
$options['priority'] = $priority;
|
||
|
||
$hook = array(
|
||
'id' => $id,
|
||
'method' => $method,
|
||
'toObject' => $toObject,
|
||
'toMethod' => $toMethod,
|
||
'toPublic' => $toPublic,
|
||
'options' => $options,
|
||
);
|
||
$hooks[$method][$priority] = $hook;
|
||
|
||
// cache record known hooks so they can be detected quickly
|
||
$cacheValue = $options['type'] == 'method' ? "$method()" : "$method";
|
||
if($options['fromClass']) $this->hookClassMethodCache["$options[fromClass]::$cacheValue"] = true;
|
||
$this->hookMethodCache[$cacheValue] = true;
|
||
if($options['type'] === 'either') {
|
||
$cacheValue = "$cacheValue()";
|
||
$this->hookMethodCache[$cacheValue] = true;
|
||
if($options['fromClass']) $this->hookClassMethodCache["$options[fromClass]::$cacheValue"] = true;
|
||
}
|
||
|
||
// keep track of all local hooks combined when debug mode is on
|
||
if($local && $this->config->debug) {
|
||
$debugClass = $object->className();
|
||
$debugID = $debugClass . $id;
|
||
while(isset($this->allLocalHooks[$debugID])) $debugID .= "_";
|
||
$debugHook = $hooks[$method][$priority];
|
||
$debugHook['method'] = $debugClass . "->" . $debugHook['method'];
|
||
$this->allLocalHooks[$debugID] = $debugHook;
|
||
}
|
||
|
||
// sort by priority, if more than one hook for the method
|
||
if(count($hooks[$method]) > 1) {
|
||
if(defined("SORT_NATURAL")) {
|
||
ksort($hooks[$method], SORT_NATURAL);
|
||
} else {
|
||
uksort($hooks[$method], "strnatcmp");
|
||
}
|
||
}
|
||
|
||
if($local) {
|
||
$object->setLocalHooks($hooks);
|
||
}
|
||
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* Add a hooks to multiple methods at once
|
||
*
|
||
* This is the same as addHook() except that the $method argument is an array or CSV string of hook definitions.
|
||
* See the addHook() method for more detailed info on arguments.
|
||
*
|
||
* @param Wire $object
|
||
* @param array|string $methods Array of one or more strings hook definitions, or CSV string of hook definitions
|
||
* @param object|null|callable $toObject
|
||
* @param string|array|null $toMethod
|
||
* @param array $options
|
||
* @return string CSV string of hook IDs that were added
|
||
* @throws WireException
|
||
* @since 3.0.137
|
||
*
|
||
*/
|
||
protected function addHooks(Wire $object, $methods, $toObject, $toMethod = null, $options = array()) {
|
||
|
||
if(!is_array($methods)) {
|
||
// potentially multiple methods defined in a CSV string
|
||
// could also be a single method with CSV arguments
|
||
|
||
$str = (string) $methods;
|
||
$argSplit = '|';
|
||
|
||
// skip optional useless parenthesis in definition to avoid unnecessary iterations
|
||
if(strpos($str, '()') !== false) $str = str_replace('()', '', $str);
|
||
|
||
if(strpos($str, '(') === false) {
|
||
// If there is a parenthesis then it is multi-method definition without arguments
|
||
// Example: "Pages::saveReady, Pages::saved"
|
||
$methods = explode(',', $str);
|
||
|
||
} else {
|
||
// Single or multi-method definitions, at least one with arguments
|
||
// Isolate commas that are for arguments versus comments that separate multiple hook methods:
|
||
// Single method example: "Page(template=order)::changed(0:order_status, 1:name=pending)"
|
||
// Multi method example: "Page(template=order)::changed(0:order_status, 1:name=pending), Page::saved"
|
||
|
||
while(strpos($str, $argSplit) !== false) $argSplit .= '|';
|
||
$strs = explode('(', $str);
|
||
|
||
foreach($strs as $key => $val) {
|
||
if(strpos($val, ')') === false) continue;
|
||
list($a, $b) = explode(')', $val, 2);
|
||
if(strpos($a, ',') !== false) $a = str_replace(array(', ', ','), $argSplit, $a);
|
||
$strs[$key] = "$a)$b";
|
||
}
|
||
|
||
$str = implode('(', $strs);
|
||
$methods = explode(',', $str);
|
||
|
||
foreach($methods as $key => $method) {
|
||
if(strpos($method, $argSplit) === false) continue;
|
||
$methods[$key] = str_replace($argSplit, ', ', $method);
|
||
}
|
||
}
|
||
}
|
||
|
||
$result = array();
|
||
$options['noAddHooks'] = true; // prevent addHook() from calling addHooks() again
|
||
|
||
foreach($methods as $method) {
|
||
$method = trim($method);
|
||
$hookID = $this->addHook($object, $method, $toObject, $toMethod, $options);
|
||
$result[] = $hookID;
|
||
}
|
||
|
||
$result = implode(',', $result);
|
||
|
||
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();
|
||
$path = trim($path);
|
||
$pathParts = explode('/', trim($path, '/'));
|
||
$key = null;
|
||
|
||
foreach($pathParts as $index => $filter) {
|
||
|
||
// see if it is alphanumeric, other than dash or underscore
|
||
if(!ctype_alnum($filter) && !ctype_alnum(str_replace(array('-', '_'), '', $filter))) {
|
||
// likely a regex pattern or named argument, see if we can use some from beginning
|
||
$filterNew = '';
|
||
for($n = 0; $n < strlen($filter); $n++) {
|
||
$test = substr($filter, 0, $n+1);
|
||
if(!ctype_alnum($test)) break;
|
||
$filterNew = $test;
|
||
}
|
||
if(!strlen($filterNew)) continue;
|
||
$filter = $filterNew;
|
||
}
|
||
|
||
// test the filter to see which one will match
|
||
foreach(array("/$filter/", "/$filter", "$filter/") as $test) {
|
||
$pos = strpos($path, $test);
|
||
if($pos === false) continue;
|
||
$filter = $test;
|
||
break;
|
||
}
|
||
|
||
// ensure array index 0 only ever refers to match at beginning
|
||
$key = $pos === 0 && $index === 0 ? 0 : $index + 1;
|
||
$filters[$key] = $filter;
|
||
}
|
||
|
||
// trailing slash on last filter is optional
|
||
if($key !== null) $filters[$key] = rtrim($filters[$key], '/');
|
||
|
||
$this->pathHooks[$id] = array(
|
||
'match' => $path,
|
||
'filters' => $filters,
|
||
);
|
||
|
||
return $id;
|
||
}
|
||
|
||
/**
|
||
* Provides the implementation for calling hooks in ProcessWire
|
||
*
|
||
* Unlike __call, this method won't trigger an Exception if the hook and method don't exist.
|
||
* Instead it returns a result array containing information about the call.
|
||
*
|
||
* @param Wire $object
|
||
* @param string $method Method or property to run hooks for.
|
||
* @param array $arguments Arguments passed to the method and hook.
|
||
* @param string|array $type May be any one of the following:
|
||
* - method: for hooked methods (default)
|
||
* - property: for hooked properties
|
||
* - before: only run before hooks and do nothing else
|
||
* - after: only run after hooks and do nothing else
|
||
* - Or array[] of hooks (from getHooks method) to run (does not call hooked method)
|
||
* @return array Returns an array with the following information:
|
||
* [return] => The value returned from the hook or NULL if no value returned or hook didn't exist.
|
||
* [numHooksRun] => The number of hooks that were actually run.
|
||
* [methodExists] => Did the hook method exist as a real method in the class? (i.e. with 3 underscores ___method).
|
||
* [replace] => Set by the hook at runtime if it wants to prevent execution of the original hooked method.
|
||
*
|
||
*/
|
||
public function runHooks(Wire $object, $method, $arguments, $type = 'method') {
|
||
|
||
$hookTimer = self::___debug ? $this->hookTimer($object, $method, $arguments) : null;
|
||
$realMethod = "___$method";
|
||
$cancelHooks = false;
|
||
$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);
|
||
if(!$methodExists && method_exists($object, $method)) {
|
||
// non-hookable method exists, indicating we may be in a manually called runHooks()
|
||
$methodExists = true;
|
||
$realMethod = $method;
|
||
}
|
||
}
|
||
|
||
if(is_array($type)) {
|
||
// array of hooks to run provided in $type argument
|
||
$hooks = $type;
|
||
$type = 'custom';
|
||
}
|
||
|
||
$result = array(
|
||
'return' => null,
|
||
'numHooksRun' => 0,
|
||
'methodExists' => $methodExists,
|
||
'replace' => false,
|
||
);
|
||
|
||
if($type === 'method' || $type === 'property' || $type === 'either') {
|
||
if(!$methodExists && !$this->isHookedOrParents($object, $method, $type)) {
|
||
return $result; // exit quickly when we can
|
||
}
|
||
}
|
||
|
||
if($hooks === null) $hooks = $this->getHooks($object, $method);
|
||
|
||
foreach(array('before', 'after') as $when) {
|
||
|
||
if($type === 'method') {
|
||
if($when === 'after' && $result['replace'] !== true) {
|
||
if($methodExists) {
|
||
$result['return'] = $object->_callMethod($realMethod, $arguments);
|
||
} else {
|
||
$result['return'] = null;
|
||
}
|
||
}
|
||
} else if($type === 'after') {
|
||
if($when === 'before') continue;
|
||
} else if($type === 'before') {
|
||
if($when === 'after') break;
|
||
}
|
||
|
||
foreach($hooks as /* $priority => */ $hook) {
|
||
|
||
if(!$hook['options'][$when]) continue;
|
||
if($type === 'property' && $hook['options']['type'] === 'method') continue;
|
||
if($type === 'method' && $hook['options']['type'] === 'property') continue;
|
||
|
||
if(!empty($hook['options']['objMatch'])) {
|
||
/** @var Selectors $objMatch */
|
||
$objMatch = $hook['options']['objMatch'];
|
||
// object match comparison to determine at runtime whether to execute the hook
|
||
if(is_object($objMatch)) {
|
||
if(!$objMatch->matches($object)) continue;
|
||
} else {
|
||
if(((string) $object) != $objMatch) continue;
|
||
}
|
||
}
|
||
|
||
if($type == 'method' && !empty($hook['options']['argMatch'])) {
|
||
// argument comparison to determine at runtime whether to execute the hook
|
||
$argMatches = $hook['options']['argMatch'];
|
||
$matches = true;
|
||
foreach($argMatches as $argKey => $argMatch) {
|
||
/** @var Selectors $argMatch */
|
||
$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;
|
||
}
|
||
}
|
||
if(!$matches) break;
|
||
}
|
||
if(!$matches) continue; // don't run hook
|
||
}
|
||
|
||
if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) {
|
||
$allowRunPathHook = $this->allowRunPathHook($hook['id'], $arguments);
|
||
$this->removeHook($object, $hook['id']); // once only
|
||
if(!$allowRunPathHook) continue;
|
||
$useHookReturnValue = true;
|
||
}
|
||
|
||
$event = new HookEvent(array(
|
||
'object' => $object,
|
||
'method' => $method,
|
||
'arguments' => $arguments,
|
||
'when' => $when,
|
||
'return' => $result['return'],
|
||
'id' => $hook['id'],
|
||
'options' => $hook['options']
|
||
));
|
||
$this->wire->wire($event);
|
||
|
||
$toObject = $hook['toObject'];
|
||
$toMethod = $hook['toMethod'];
|
||
|
||
if($profiler) {
|
||
$profilerEvent = $profiler->start($hook['id'], $this, array(
|
||
'event' => $event,
|
||
'hook' => $hook,
|
||
));
|
||
} else {
|
||
$profilerEvent = false;
|
||
}
|
||
|
||
if(is_null($toObject)) {
|
||
$toMethodCallable = is_callable($toMethod);
|
||
if(!$toMethodCallable && strpos($toMethod, "\\") === false && __NAMESPACE__) {
|
||
$_toMethod = $toMethod;
|
||
$toMethod = "\\" . __NAMESPACE__ . "\\$toMethod";
|
||
$toMethodCallable = is_callable($toMethod);
|
||
if(!$toMethodCallable) {
|
||
$toMethod = "\\$_toMethod";
|
||
$toMethodCallable = is_callable($toMethod);
|
||
}
|
||
}
|
||
if($toMethodCallable) {
|
||
$returnValue = $toMethod($event);
|
||
} else {
|
||
// hook fail, not callable
|
||
$returnValue = null;
|
||
}
|
||
} else {
|
||
/** @var Wire $toObject */
|
||
if($hook['toPublic']) {
|
||
// public
|
||
$returnValue = $toObject->$toMethod($event);
|
||
} else {
|
||
// protected or private
|
||
$returnValue = $toObject->_callMethod($toMethod, array($event));
|
||
}
|
||
$toMethodCallable = true;
|
||
}
|
||
|
||
if($returnValue !== null) {
|
||
// 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 doesn’t need to be passed around to
|
||
// more than one hook
|
||
if($useHookReturnValue) {
|
||
$event->return = $returnValue;
|
||
}
|
||
}
|
||
|
||
if($profilerEvent) $profiler->stop($profilerEvent);
|
||
|
||
if(!$toMethodCallable) continue;
|
||
|
||
$result['numHooksRun']++;
|
||
|
||
if($event->cancelHooks === true) $cancelHooks = true;
|
||
|
||
if($when == 'before') {
|
||
$arguments = $event->arguments;
|
||
$result['replace'] = $event->replace === true || $result['replace'] === true;
|
||
if($result['replace']) $result['return'] = $event->return;
|
||
}
|
||
|
||
if($when == 'after') $result['return'] = $event->return;
|
||
if($cancelHooks) break;
|
||
}
|
||
if($cancelHooks) break;
|
||
}
|
||
|
||
if($hookTimer) Debug::saveTimer($hookTimer);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*
|
||
* @param string $id Hook ID
|
||
* @param array $arguments
|
||
* @return bool
|
||
* @since 3.0.173
|
||
*
|
||
*/
|
||
protected function allowRunPathHook($id, array &$arguments) {
|
||
|
||
$pathHook = $this->pathHooks[$id];
|
||
$requestPath = $arguments[0];
|
||
$filterFail = false;
|
||
|
||
// first pre-filter the requestPath against any words matchPath (filters)
|
||
foreach($pathHook['filters'] as $key => $filter) {
|
||
$pos = strpos($requestPath, $filter);
|
||
if($pos === false || ($key === 0 && $pos !== 0)) $filterFail = true;
|
||
if($filterFail) break;
|
||
}
|
||
|
||
if($filterFail) return false;
|
||
|
||
// at this point the path hook passed pre-filters and might match
|
||
|
||
$pageNum = $this->wire->wire()->input->pageNum();
|
||
$slashed = substr($requestPath, -1) === '/' && strlen($requestPath) > 1;
|
||
$matchPath = $pathHook['match'];
|
||
$regexDelim = ''; // populated only for user-specified regex
|
||
$pageNumArgument = 0; // populate in $arguments when {pageNum} present in match pattern
|
||
|
||
if(strpos('!@#%', $matchPath[0]) !== false) {
|
||
// already in delimited regex format
|
||
$regexDelim = $matchPath[0];
|
||
} else {
|
||
// needs to be in regex format
|
||
if(strpos($matchPath, '/') === 0) $matchPath = "^$matchPath";
|
||
$matchPath = "#$matchPath$#";
|
||
}
|
||
|
||
if(strpos($matchPath, '{pageNum}') !== false) {
|
||
// the {pageNum} named argument maps to $input->pageNum. remove the {pageNum} argument
|
||
// from the match path since it is handled differently from other named arguments
|
||
$find = array('/{pageNum}/', '/{pageNum}', '{pageNum}');
|
||
$matchPath = str_replace($find, '/', $matchPath);
|
||
$pathHook['match'] = str_replace($find, '/', $pathHook['match']);
|
||
$pageNumArgument = $pageNum;
|
||
} else if($pageNum > 1) {
|
||
// hook does not handle pagination numbers above 1
|
||
return false;
|
||
}
|
||
|
||
if(strpos($matchPath, ':') && strpos($matchPath, '(') !== false) {
|
||
// named arguments in format “(name: value)” converted to named PCRE capture groups
|
||
$matchPath = preg_replace('#\(([-_a-z0-9]+):#i', '(?P<$1>', $matchPath);
|
||
}
|
||
|
||
if(strpos($matchPath, '{') !== false) {
|
||
// named arguments in format “{name}” converted to named PCRE capture groups
|
||
// note that the match pattern of any URL segment is assumed for this case
|
||
$matchPath = preg_replace('#\{([_a-z][-_a-z0-9]*)\}#i', '(?P<$1>[^/]+)', $matchPath);
|
||
}
|
||
|
||
if(!preg_match($matchPath, $requestPath, $matches)) {
|
||
// if match fails, try again with trailing slash state reversed
|
||
if($slashed) {
|
||
$requestPath2 = rtrim($requestPath, '/');
|
||
} else {
|
||
$requestPath2 = "$requestPath/";
|
||
}
|
||
if(!preg_match($matchPath, $requestPath2, $matches)) return false;
|
||
}
|
||
|
||
// check on trailing slash
|
||
if(strpos($matchPath, '/?') === false) {
|
||
// either slash or no-slash is required, depending on whether match pattern ends with one
|
||
$slashRequired = substr(rtrim($pathHook['match'], $regexDelim . '$)+'), -1) === '/';
|
||
$this->pathHookRedirect = '';
|
||
if($slashRequired && !$slashed) {
|
||
// trailing slash required and not present
|
||
$this->pathHookRedirect = $requestPath . '/';
|
||
return false;
|
||
} else if(!$slashRequired && $slashed) {
|
||
// lack of trailing slash required and one is present
|
||
$this->pathHookRedirect = rtrim($requestPath, '/');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// success: at this point the requestPath has matched
|
||
$arguments['path'] = $arguments[0];
|
||
if($pageNumArgument) $arguments['pageNum'] = $pageNumArgument;
|
||
|
||
foreach($matches as $key => $value) {
|
||
// populate requested arguments
|
||
if($key !== 0) $arguments[$key] = $value;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Filter and return hooks matching given property and value
|
||
*
|
||
* @param array $hooks Hooks from getHooks() method
|
||
* @param string $property Property name from hook (or hook options)
|
||
* @param string|bool|int $value Value to match
|
||
* @return array
|
||
*
|
||
*/
|
||
public function filterHooks(array $hooks, $property, $value) {
|
||
foreach($hooks as $key => $hook) {
|
||
if(array_key_exists($property, $hook)) {
|
||
if($hook[$property] !== $value) unset($hooks[$key]);
|
||
} else if(array_key_exists($property, $hook['options'])) {
|
||
if($hook['options'][$property] !== $value) unset($hooks[$key]);
|
||
}
|
||
}
|
||
return $hooks;
|
||
}
|
||
|
||
/**
|
||
* Start timing a hook and return the timer name
|
||
*
|
||
* @param Wire $object
|
||
* @param String $method
|
||
* @param array $arguments
|
||
* @return string
|
||
*
|
||
*/
|
||
protected function hookTimer($object, $method, $arguments) {
|
||
$timerName = $object->className() . "::$method";
|
||
$notes = array();
|
||
foreach($arguments as $argument) {
|
||
if(is_object($argument)) $notes[] = get_class($argument);
|
||
else if(is_array($argument)) $notes[] = "array(" . count($argument) . ")";
|
||
else if(strlen($argument) > 20) $notes[] = substr($argument, 0, 20) . '...';
|
||
}
|
||
$timerName .= "(" . implode(', ', $notes) . ")";
|
||
if(isset($this->debugTimers[$timerName])) {
|
||
$this->debugTimers[$timerName]++;
|
||
$timerName .= " #" . $this->debugTimers[$timerName];
|
||
} else {
|
||
$this->debugTimers[$timerName] = 1;
|
||
}
|
||
Debug::timer($timerName);
|
||
return $timerName;
|
||
}
|
||
|
||
/**
|
||
* Given a Hook ID provided by addHook() this removes the hook
|
||
*
|
||
* To have a hook function remove itself within the hook function, say this is your hook function:
|
||
* function(HookEvent $event) {
|
||
* $event->removeHook(null); // remove self
|
||
* }
|
||
*
|
||
* @param Wire $object
|
||
* @param string|array|null $hookID Can be single hook ID, array of hook IDs, or CSV string of hook IDs
|
||
* @return Wire
|
||
*
|
||
*/
|
||
public function removeHook(Wire $object, $hookID) {
|
||
if(is_array($hookID) || strpos($hookID, ',')) {
|
||
return $this->removeHooks($object, $hookID);
|
||
}
|
||
if(!empty($hookID) && substr_count($hookID, ':') === 2) {
|
||
// local hook ID ":100.0:methodName" or static hook ID "ClassName:100.0:methodName"
|
||
list($hookClass, $priority, $method) = explode(':', $hookID);
|
||
if(empty($hookClass)) {
|
||
// local hook
|
||
$localHooks = $object->getLocalHooks();
|
||
unset($localHooks[$method][$priority]);
|
||
$object->setLocalHooks($localHooks);
|
||
} else {
|
||
// static hook
|
||
unset($this->staticHooks[$hookClass][$method][$priority], $this->pathHooks[$hookID]);
|
||
if(empty($this->staticHooks[$hookClass][$method])) {
|
||
unset($this->hookClassMethodCache["$hookClass::$method"]);
|
||
}
|
||
}
|
||
}
|
||
return $object;
|
||
}
|
||
|
||
/**
|
||
* Given a hook ID or multiple hook IDs (in array or CSV string) remove the hooks
|
||
*
|
||
* @param Wire $object
|
||
* @param array|string $hookIDs
|
||
* @return Wire
|
||
* @since 3.0.137
|
||
*
|
||
*/
|
||
protected function removeHooks(Wire $object, $hookIDs) {
|
||
if(!is_array($hookIDs)) $hookIDs = explode(',', $hookIDs);
|
||
foreach($hookIDs as $hookID) {
|
||
$this->removeHook($object, $hookID);
|
||
}
|
||
return $object;
|
||
}
|
||
|
||
/**
|
||
* Return the "all local hooks" cache
|
||
*
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getAllLocalHooks() {
|
||
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
|
||
*
|
||
* @param string $requestPath Optionally provide request path to determine if any might match (3.0.174+)
|
||
* @return bool
|
||
* @since 3.0.173
|
||
*
|
||
*/
|
||
public function hasPathHooks($requestPath = '') {
|
||
// first pre-filter the requestPath against any words matchPath (filters)
|
||
if(strlen($requestPath)) return $this->filterPathHooks($requestPath, true);
|
||
return count($this->pathHooks) > 0;
|
||
}
|
||
|
||
/**
|
||
* Return path hooks that have potential to match given request path
|
||
*
|
||
* @param string $requestPath
|
||
* @param bool $has Specify true to change return value to boolean as to whether any can match (default=false)
|
||
* @return array|bool
|
||
* @since 3.0.174
|
||
*
|
||
*/
|
||
public function filterPathHooks($requestPath, $has = false) {
|
||
$pathHooks = array();
|
||
foreach($this->pathHooks as $id => $pathHook) {
|
||
$fail = false;
|
||
foreach($pathHook['filters'] as $filter) {
|
||
$fail = strpos($requestPath, $filter) === false;
|
||
if($fail) break;
|
||
}
|
||
if(!$fail) {
|
||
$pathHooks[$id] = $pathHook;
|
||
if($has) break;
|
||
}
|
||
}
|
||
return $has ? count($pathHooks) > 0 : $pathHooks;
|
||
}
|
||
|
||
/**
|
||
* 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 redirect URL required by an applicable path hook, or blank otherwise
|
||
*
|
||
* @return string
|
||
* @since 3.0.173
|
||
*
|
||
*/
|
||
public function getPathHookRedirect() {
|
||
return $this->pathHookRedirect;
|
||
}
|
||
|
||
/**
|
||
* @return string
|
||
*
|
||
*/
|
||
public function className() {
|
||
return wireClassName($this, false);
|
||
}
|
||
|
||
public function __toString() {
|
||
return $this->className();
|
||
}
|
||
|
||
}
|