1405 lines
44 KiB
PHP
1405 lines
44 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 2021 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;
|
|||
|
|
|||
|
/**
|
|||
|
* 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)) {
|
|||
|
$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)) {
|
|||
|
$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 = ($local ? $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
|
|||
|
$pos = false;
|
|||
|
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_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();
|
|||
|
}
|
|||
|
|
|||
|
}
|