Below is a very simple mock of the hook system. It doesn't take into account priorities, and it is more modernized for expected usage so it might not support some previous edge cases.
You'll notice that add_action actually calls the exact same code as add_filter, and that's what core actually does.
The gist is that there's a global array. You could store this in your base class if you wanted, too. WordPress backs their stuff with objects, too, but I'm simplifying things here.
global $wp_test_hooks;
if(!is_array($wp_test_hooks)) {
$wp_test_hooks = [];
}
if(!function_exists('add_filter')) {
function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
global $wp_test_hooks;
$wp_test_hooks[$hook_name][] = [
$callback,
$priority,
$accepted_args,
];
return true;
}
}
if(!function_exists('add_action')) {
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
return add_filter( $hook_name, $callback, $priority, $accepted_args );
}
}
if(!function_exists('apply_filters')){
function apply_filters( $hook_name, ...$values ) {
$value = array_shift($values);
global $wp_test_hooks;
if(array_key_exists($hook_name, $wp_test_hooks)){
foreach($wp_test_hooks[$hook_name] as $parts){
list($callback, $priority, $accepted_args) = $parts;
$value = $callback($value, ...$values);
}
}
return $value;
}
}
if(!function_exists('do_action')){
function do_action( $hook_name, ...$arg ) {
global $wp_test_hooks;
if(array_key_exists($hook_name, $wp_test_hooks)){
foreach($wp_test_hooks[$hook_name] as $parts){
list($callback, $priority, $accepted_args) = $parts;
$callback(...$arg);
}
}
}
}
You can see it in action by using normal WordPress code:
add_action('name1', static function(){echo 'In the Action';});
add_filter('name2', static function($originalValue){return 'In the Filter';});
do_action('name1', 'test', 'stuff');
echo PHP_EOL;
echo apply_filters('name2', 'test', 'stuff');
Demo here: https://3v4l.org/DAVl0
All that said, if you don't manually call do_action or apply_filters in your code, don't both with the bulk of this. You can use the functions, just leave their bodies empty. More often than not, it is WordPress or a plugin that is invoking those methods, and if you aren't invoking WordPress or calling those functions, there's no need for logic or even the global array. What you probably want is:
if(!function_exists('add_filter')) {
function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
}
if(!function_exists('add_action')) {
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
}
if(!function_exists('apply_filters')){
function apply_filters( $hook_name, ...$values ) { }
}
if(!function_exists('do_action')){
function do_action( $hook_name, ...$arg ) { }
}