Skip to content

Instantly share code, notes, and snippets.

@talss89
Last active May 1, 2023 14:00
Show Gist options
  • Save talss89/3cf697bff428846ecbaac4d9a84ed145 to your computer and use it in GitHub Desktop.
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
<?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