Last active
May 1, 2023 14:00
-
-
Save talss89/3cf697bff428846ecbaac4d9a84ed145 to your computer and use it in GitHub Desktop.
[Now https://github.com/conduit-innovation/gorilla-claw] WordPress object method hooks - find, remove, replace: Find hook by class name and method, Replace hook and reference original object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
** This Gist developed into a full runtime toolkit for hooks. See https://github.com/conduit-innovation/gorilla-claw ** | |
This is a utility to remove or replace WordPress actions or filters, when the handler is an object method. | |
Can be useful changing Woocommerce block functionality, amongst other things. | |
We first search $wp_filter for the class name and method, then we can either remove or replace it with another function. | |
When replacing, there is some unusual behaviour. In a lot of circumstances, a hook that points to an object method often | |
relies on methods / properties on the original object's $this. If we simply just replace the filter callback with another | |
function, we lose access to the original object. What we ideally need is a way to break into the original scope. | |
This is what `ExtractedHookProxy` does. We wrap our new filter callback in the `ExtractedHookProxy` object, and register | |
the generated `$proxy->__cb` callback as the filter handler itself. | |
Internally, `ExtractedHookProxy` breaks into the original object scope ($this->that) by registering exception handlers on | |
magic methods `__get`, `__set` and `__call`. When these are triggered (ie. trying to access a protected / private | |
property), we catch the Exception, and instead use some Closure-binding-pass-by-ref trickery to allow us to read and | |
write protected / private properties. | |
This is all transparent to the new filter handler, you can just use $this as if it was truly the original object. | |
--- Examples --- | |
1. Remove a hook by class and method name (method parameter is optional) | |
locate_hook_by_classname('pre_render_block', 'Automattic\WooCommerce\Blocks\BlockTypes\ProductQuery', 'update_query')->remove(); | |
2. Replace a hook, but access the original object | |
$hook = locate_hook_by_classname('pre_render_block', 'Automattic\WooCommerce\Blocks\BlockTypes\ProductQuery', 'update_query'); | |
if($hook->exists()) { | |
$hook->replace(function($param) { | |
// The original object is referenced by $this->that (or $hook->that): | |
var_dump($this->that); | |
// It's magic. We can even access private or protected properties! | |
var_dump($this->some_private_var); | |
// Or ... bloody hell ... run private methods! | |
var_dump($this->some_private_method()); | |
return $param; | |
}); | |
} | |
--- Info --- | |
`locate_hook_by_classname()` will always return a valid object, even if the hook wasn't found. You can explicitly check `->exists()` to determine if found or not. | |
This means you can always rely on '->remove()` and `->replace()` being available even if the hook isn't valid. These will be no-op in this case. | |
*/ | |
interface ExtractedHookInterface { | |
public function remove(): bool; | |
public function replace(callable $cb): bool; | |
public function exists(): bool; | |
} | |
class ExtractedHookProxy { | |
public $__cb; | |
protected $__that; | |
function __construct(&$hook_cb, &$that) { | |
$proxy = &$this; | |
$this->__that = &$that; | |
$this->__cb = \Closure::bind(function (...$args) use (&$proxy, &$hook_cb) { | |
\Closure::bind($hook_cb, $proxy)(...$args); | |
}, $that); | |
} | |
function __get($prop) { | |
try { | |
return $this->$prop; | |
} catch (\Exception $e) { | |
return $this->__get_private($prop); | |
} | |
} | |
function __set($prop, $val) { | |
try { | |
$this->$prop = $val; | |
} catch (\Exception $e) { | |
$private = $this->__get_private($prop); | |
$private = $val; | |
} | |
} | |
function __call($method, $args) { | |
try { | |
return $this->$method(...$args); | |
} catch (\Exception $e) { | |
$private = $this->__get_private($method); | |
return \Closure::bind($private, $this->__that, $this->__that)(...$args); | |
} | |
} | |
// This the wildest function I've ever written... break PHP protected / private var rules | |
public function &__get_private($var): mixed { | |
$that = &$this->__that; | |
return \Closure::bind(function &($that) use ($var) { | |
return $that->$var; | |
}, $that, $that)($that); | |
} | |
} | |
class ExtractedHook implements ExtractedHookInterface { | |
public $ident; | |
public $hook; | |
public $filter; | |
public $prio; | |
public $that; | |
function __construct($ident, $hook, $filter, $prio, &$that) { | |
$this->ident = $ident; | |
$this->hook = $hook; | |
$this->filter = $filter; | |
$this->prio = $prio; | |
$this->that = $that; | |
} | |
public function remove(): bool { | |
return remove_filter($this->filter, $this->hook, $this->prio); | |
} | |
public function replace(callable $cb): bool { | |
global $wp_filter; | |
if( | |
isset($wp_filter[$this->filter]) && | |
isset($wp_filter[$this->filter]->callbacks[$this->prio]) && | |
isset($wp_filter[$this->filter]->callbacks[$this->prio][$this->ident]) | |
) { | |
$wp_filter[$this->filter]->callbacks[$this->prio][$this->ident]['function'] = (new ExtractedHookProxy($cb, $this->that))->__cb; | |
return true; | |
} | |
return false; | |
} | |
public function exists(): bool { | |
return true; | |
} | |
} | |
class ExtractedHookNotFound extends ExtractedHook { | |
public function __construct() {} | |
public function remove(): bool { | |
return false; | |
} | |
public function replace(callable $cb): bool { | |
return false; | |
} | |
public function exists(): bool { | |
return false; | |
} | |
} | |
function locate_hook_by_classname(string $filter, string $classname, string | bool $method = false): ExtractedHook { | |
global $wp_filter; | |
if(isset($wp_filter[$filter])) { | |
foreach($wp_filter[$filter]->callbacks as $prio => $hooks) { | |
foreach($hooks as $ident => $callable) { | |
if(!is_array($callable['function'])) { | |
continue; | |
} | |
$obj = $callable['function'][0]; | |
$meth = $callable['function'][1]; | |
if(get_class($obj) === $classname) { | |
if($meth === $method || $method === false) { | |
return new ExtractedHook($ident, $callable['function'], $filter, $prio, $obj); | |
} | |
} | |
} | |
} | |
} | |
return new ExtractedHookNotFound(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment