public function addHook($method, $toObject, $toMethod = null, $options = array())
{
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 (is_null($toMethod)) {
// $toObject has been ommitted and a procedural function specified instead
// $toObject may also be a closure
$toMethod = $toObject;
$toObject = null;
}
if (is_null($toMethod)) {
throw new WireException("Method to call is required and was not specified (toMethod)");
}
if (substr($method, 0, 3) == '___') {
throw new WireException("You must specify hookable methods without the 3 preceding underscores");
}
if (method_exists($this, $method)) {
throw new WireException("Method " . $this->className() . "::{$method} is not hookable");
}
$options = array_merge(self::$defaultHookOptions, $options);
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)) {
$objMatch = new Selectors($objMatch);
}
if ($objMatch) {
$options['objMatch'] = $objMatch;
}
}
$options['fromClass'] = $fromClass;
}
$argOpen = strpos($method, '(');
if ($argOpen && strpos($method, ')') > $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)) {
$argMatch[$argKey] = new Selectors($argVal);
}
}
if (count($argMatch)) {
$options['argMatch'] = $argMatch;
}
}
if ($options['allInstances'] || $options['fromClass']) {
// hook all instances of this class
$hookClass = $options['fromClass'] ? $options['fromClass'] : $this->className();
if (!isset(self::$staticHooks[$hookClass])) {
self::$staticHooks[$hookClass] = array();
}
$hooks =& self::$staticHooks[$hookClass];
$options['allInstances'] = true;
$local = 0;
} else {
// hook only this instance
$hookClass = '';
$hooks =& $this->localHooks;
$local = 1;
}
$priority = (string) $options['priority'];
if (!isset($hooks[$method])) {
if (ctype_digit($priority)) {
$priority = "{$priority}.0";
}
$hooks[$method] = array();
} 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(self::$staticHooks[$hookClass][$method][$priority]) || isset($this->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;
$hooks[$method][$priority] = array('id' => $id, 'method' => $method, 'toObject' => $toObject, 'toMethod' => $toMethod, 'options' => $options);
// cacheValue is just the method() or property, cacheKey includes optional fromClass::
$cacheValue = $options['type'] == 'method' ? "{$method}()" : "{$method}";
$cacheKey = ($options['fromClass'] ? $options['fromClass'] . '::' : '') . $cacheValue;
self::$hookMethodCache[$cacheKey] = $cacheValue;
// keep track of all local hooks combined when debug mode is on
if ($this->wire('config')->debug && $hooks === $this->localHooks) {
$debugClass = $this->className();
$debugID = ($local ? $debugClass : '') . $id;
while (isset(self::$allLocalHooks[$debugID])) {
$debugID .= "_";
}
$debugHook = $hooks[$method][$priority];
$debugHook['method'] = $debugClass . "->" . $debugHook['method'];
self::$allLocalHooks[$debugID] = $debugHook;
}
// sort by priority, if more than one hook for the method
if (count($hooks[$method]) > 1) {
defined("SORT_NATURAL") ? ksort($hooks[$method], SORT_NATURAL) : uksort($hooks[$method], "strnatcmp");
}
return $id;
}