Hook
Extensibility
Hook
Improving one of WordPress' core features.
Introduction
Hooks are a fundamental part of WordPress.
They are used everywhere for extending, both WordPress core AND plugins.
The problematic tendencies of hooks
In WordPress hooks kan be added by calling add_action or add_filter depending on the type of hook you need.
These functions register callbacks to hooks with a priority and sometimes also the number of arguments needed in the callback. Like so:
add_action('init','do_something_on_init', 10, 2);
function do_something_on_init($foo, $bar){
// My code here
}
Typically, you would find pairs of add_* function calls and their respective callbacks by each other like this
sprinkled throughout the codebase.
Often even on top of each other in a theme's functions.php file or a plugin's main file.
This makes a codebase very messy and makes it hard to find the places where a given piece of functionality gets added.
Our solution to this problem
We want to make sure that hooks have their own place to live in a project's codebase. This way you know where to find them. Always.
The idea is simple: since you can never add a hook without a callback we will focus on the callback and then give this a relation to a hook afterward.
We will also organise these callbacks into different classes and by using PSR-4 autoloading you can split it up as much as you'd like!
So, let's get on to the actual code!
Getting Started
First off, make sure the module is installed and set up.
Next up, create a directory for your hooks.
Usually you would use /app/Hooks
Adding directories for loading
In your project's config add each of your directories and their respective namespaces.
<?php return [
'paths' => [
// Note that this is namespace and relative or absolute path
['MyProjectNamespace\\Hooks', 'app/Hooks'],
['MyOtherNamespace\\Hooks', '/an/absolute/path/to/the/app/Hooks'],
]
];
Each *.php file found, recursively, in these paths will be loaded.
The service will expect files here to match PSR-4.
So app/Hooks/MyHook.php should have the namespace MyProjectNamespace\Hooks\MyHook.
This also works recursively so app/Hooks/Nested/FooHook.php should have the namespace MyProjectNamespace\Hooks\Nested\FooHook.
The paths written here can either be relative to the application root OR absolute. This is useful when you don't necessarily know where your code is located relative to the application.
Eg.
['MyProjectNamespace\\Hooks', __DIR__ ."/Hooks"],
At runtime
You can also register a namespace and path at runtime which is useful for modules.
\MorningMedley\Facades\Hook::register('\MedleyApp\Hooks', base_path('app/Hooks'));
Caching
You can cache all located hooks using wp artisan hook:cache or as part of the wp artisan optimize command which is recommended in production.
The cache file is /bootstrap/cache/hook.php and can be deleted again using wp artisan hook:clear
Using Hooks
Organize your hooks by feature in their own class and/or namespace.
For instance, you could have a Hook class for adding/removing menu items in the WordPress backend
called AdminMenuHooks
Morningmedley doesn't enforce any naming convention here. So you can suffix your classes with Hook, but you don't have to.
Use as many classes as you need to. Thanks to the cached config and opCache performance won't be an issue.
The Hook class
Within your Hooks directory create a PHP class that uses the Hookable trait.
Then make sure to import the Hook Attributes you need.
use MorningMedley\Hook\Traits\Hookable;
use MorningMedley\Hook\Classes\Action;
use MorningMedley\Hook\Classes\Filter;
class AdminMenuHooks {
use Hookable;
}
Attributes
PHP 8.0 added a new feature: Attributes
These allow you to add some extra information on methods, properties and more.
We use these to tell our Hook class which hooks are associated with our methods and properties.
An attribute looks like this:
#[Attribute]
Hooking methods
Attached to a method it would look like this:
#[Attribute]
function foo(){}
We introduce two new attributes: Action and Filter.
Both of these take two arguments. The $hookName and an optional $priority which defaults to 10.
So, finally an example!
- Action
- Action with priority set
- Filter
- Filter with priority set
#[Action('admin_menu')]
public function removeCommentsPage(){
remove_menu_page('edit-comments.php');
}
#[Action('admin_menu', 20)]
public function removeCommentsPage(){
remove_menu_page('edit-comments.php');
}
#[Filter('upload_mimes')]
public function allowSomeMimeType(array $mimeTypes){
$mimeTypes['svg'] = 'image/svg+xml';
return $mimeTypes;
}
#[Filter('upload_mimes', 20)]
public function allowSomeMimeType(array $mimeTypes){
$mimeTypes['svg'] = 'image/svg+xml';
return $mimeTypes;
}
Hooking properties
If you need to return the same static value on a filter every time it is applied then you can add the Filter attribute directly to a property with your desired value.
#[Filter('disable_some_feature')]
public bool $false = false;
This is equivalent to add_filter('disable_some_feature','__return_false');
Examples
Remove the comments menu item
<?php
namespace MyCoolProject\Hooks;
use MorningMedley\Hook\Traits\Hookable;
use MorningMedley\Hook\Classes\Action;
use MorningMedley\Hook\Classes\Filter;
class RemoveCommentsHooks
{
use Hookable;
#[Action('admin_init')]
public function removeAdminMenuItems()
{
\remove_menu_page('edit-comments.php');
}
}
Advanced uses
Using the trait on other classes
If you wish to use this trait on another class, then simply call the hookClass() method.
Here's an example:
use MorningMedley\Hook\Traits\Hookable;
use MorningMedley\Hook\Classes\Action;
class MyHookableClass{
use Hookable;
public function __construct() {
$this->hookClass(); // This registers all Action and Filter attributes.
}
#[Action('init')]
public function thisFunctionCanHook(){
// Do stuff here
}
}
Using Hooks on a ServiceProvider
You can also use the Hookable trait on ServiceProviders. In this case you don't have to call $this->hookClass() yourself as the application handles this for you.
use MorningMedley\Hook\Traits\Hookable;
use MorningMedley\Hook\Classes\Action;
class MyFeatureServiceProvider{
use Hookable;
public function register() {
$this->app->singleton(Foo::class);
}
#[Action('init')]
public function initFoo(){
$this->app->make(Foo::class)->init();
}
}