Skip to main content

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.

/config/hook.php
<?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.

tip

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

note

Morningmedley doesn't enforce any naming convention here. So you can suffix your classes with Hook, but you don't have to.

tip

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.

/app/Hooks/AdminMenuHooks.php
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('admin_menu')]
public function removeCommentsPage(){
remove_menu_page('edit-comments.php');
}

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;
note

This is equivalent to add_filter('disable_some_feature','__return_false');

Examples

Remove the comments menu item

/app/Hooks/RemoveCommentsHooks.php
<?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:

MyHookableClass.php
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.

app/Provider/MyFeatureServiceProvider.php
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();
}
}