Download the PHP package calvinalkan/better-wordpress-hooks without Composer

On this page you can find all versions of the php package calvinalkan/better-wordpress-hooks. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package better-wordpress-hooks

⚠️⚠️⚠️ This repository has been deprecated in favor of snicco/better-wp-hooks. A stable v1.0.0 has been released and is actively developed.


BetterWpHooks - A modern, OOP Wrapper around the WordPress Plugin API.

CircleCI code coverage last commit open issues License: MIT php version lines of code

BetterWpHooks is a small library that wraps the WordPress Plugin/Hook API, allowing for modern, object-oriented PHP.

Some included features are:

🚀 Lazy instantiation of classes registered in actions and filters.

🔥 Zero configuration dependency-resolution of class and method dependencies using an IoC-Container.

❤ Conditional dispatching and handling of hooks based on parameters only available at runtime.

📦 Inbuilt testing module, no more third-party mocking libraries or bootstrapping core to test hooks.

⭐ 100 % compatibility with WordPress Core and the way users can interact with custom hooks and filters.

Table of Contents


Why bother?

Being released in WordPress 2.1, in a time when OOP wasn't a thing yet in PHP, The WordPress Plugin-API has several shortcomings.

The main issues of the WordPress Plugin/Hook API are:

  1. No usage of dependency injection or an IoC-Container . If you care about the quality and maintainability of your code you use an IoC-Container.

  2. Defining the number of parameters your callback should expect is annoying and often leads to seemingly random bugs if the order or amount of the received parameters should ever change. WordPress should be able to solve this on its own behind the scenes but due to the insane backwards compatibility commitments the native PHP Reflection API can't be used.
  3. There is no proper place to define actions and filters. Many WordPress developers default to using the class constructor which is not a great option for several reasons. Another common approach is using a custom factory which often leads to your IDE not being able to detect where you added hooks. Not ideal.
  4. When using class-based callbacks, the only option besides using static methods ( *don't do that ) is to instantiate the class on every request before creating the WordPress Hook. There isn't any modern PHP framework that forces you to instantiate classes to MAYBE be used later as an event observer. Additionally, using OOP practices when defining hooks always leads to hooks being unremovable for third-party developers because WordPress will use spl_object_hash to store the hook id.

Let's review an example that can be found similarly in 95% of popular WordPress plugins.

For a simple class this might work just fine, but now imagine that MyClass has nested dependencies and handles several WordPress hooks. Constructing this object on every request just doesn't feel right and changing the constructor arguments in the future might be very painful.

  1. WordPress is event-driven. Hooks have to be added on every request. Yet there is no clearly defined way to conditionally fire hooks based on variables you only have available at runtime. Some code may only ever be required under very specific circumstances, yet the class callbacks get instantiated on every request. An example might be a class that handles sending a gift card if a customer placed an order with a total value greater than 500$.

We only ever use this class under very special circumstances, yet we have to create it on every request to pass it to the WordPress hook.

BetterWpHooks solves all of these problems and provides many more convenient features for a better WordPress developer experience.


Requirements

In theory, you should be able to use this package with some minor modifications with every PHP Version >= 7.0 , but I did not actively test it and neither should you be using PHP Versions that are not actively supported anymore by the PHP maintainers.


Installation

From the root directory of your plugin or theme, execute the following command from the terminal.


Key Concepts

Terminology

In the following WordPress actions and filters will be referred to as events. Hook callbacks, be it a class or an anonymous closure will be referred to as event listeners.

Entry Point

BetterWpHooks was built in mind with how the WordPress plugin ecosystem works. Unlike many other packages that try to modernize WordPress BetterWpHooks can be used by an unlimited amount of plugins at the same time without conflicts.

The main entry point to the library is the trait BetterWpHooksFacade(src) .

By creating a custom class that uses this Trait you gain access to your instance of the core of the library.

In the following, we are going to assume that we are using this library inside a plugin called AcmePlugin.

The class AcmeEventswill be your entry point to an instance of the main class of the libraryBetterWpHooks(src) which provides you access to the 3 main collaborators of the library:

1. IoC-Container

To auto-resolve dependencies of event listeners and automatically create objects, BetterWpHooks makes use of an Inversion of Control (IoC) Container. Since many WordPress plugins already use an IoC-Container, it would make no sense to force the usage of any specific container implementation.

The entire library is dependent on an ContainerAdapterInterface . By default, an adapter for the illuminate/container is used. Every feature of the Illumiante/Container is fully supported.

The actual container implementation can be swapped out by confirming to a simple interface, so you can use any other container like:

The only constraint is that auto-resolving ( auto-wiring ) needs to be supported by your container!

2. Events

Events represent WordPress Actions and Filters. There is however no need to distinguish between the two. That is taken care of under the hood.

There are two ways to use Events with BetterWpHooks:

  1. Events as event objects (recommended, as it provides way more features)
  2. Similar to WordPress Actions and Filters but still using the IoC-Container to resolve classes (not-recommended)

3. Event Dispatcher

This is the main class that servers as a layer between your plugin code and the WordPress Plugin API. Instead of directly creating hooks via add_action and add_filter you create them using the Event Dispatcher.

4. Event Mapper

The Event Mapper is a small class that can be used to automatically transform core ( or 3rd-party ) actions and filters into custom event objects, thus allowing you to use all the provided features even with events ( hooks ) you have no control over.


Bootstrapping

To use BetterWpHooks you need to bootstrap it in 3 simple steps. This should be done in your main plugin file or any other file that gets executed before WordPress fires its first Hook. You should also require the composer autoloader in your main plugin file.

1. Create your entry-point class

2. Create an instance of the BetterWpHooks class that will be mapped to AcmeEvents:

3. Map core and 3rd party hooks to custom event objects ( optional but recommended ).

All that this does is dispatching your custom event object when the WP Hooks is fired. We'll see why we do this in a minute. By default, mapped events are not resolved from the service container as they should only be data classes. However, if you pass 'resolve' as the first key your mapped event will be resolved from the container and dispatched afterwards.

4. Create Event Listeners that should handle your custom events and boot the class.

The entire process can also be created as a fluent API.

Ideally, you would create a plain PHP files that just return an array of your mapped events and events listeners. This way you have all your events nicely in one file instead of being separated over your entire codebase.

Complete example:


Using BetterWpHooks

Alright, why would I do all this? Time for some examples:

Let's assume we are developing a Woocommerce Extension that gives users the possibility to extend their WooCommerce store with several marketing-related features.

One of them being the gift card functionality described in the intro.

Complete Example using third-party hooks.

We create an event that represents the action (src) . This Event needs to extend AcmeEvents.

This action hook provides the order_id of the created order and the form submission data. We will create a working example of this functionality in incremental steps improving it slowly.

  1. Create an event object. Event objects are plain PHP objects that do not store any logic.

  2. Create a listener that handles the logic when an order with a total >= 500$ is created.

  3. Wire the Event and Listener. ( The wiring should be done in a separate, plain PHP file )

So what happens now when WooCommerce creates an order ?

  1. WordPress will fire the woocommerce_checkout_order_processed action.
  2. Since under the hood a custom event was mapped to this action WordPress will now call a closure that will create a new instance of the Event using the passed arguments from the filter to construct the event object.
  3. The called closure will first create the instance and then dispatch an event which passes the created object as an argument to any registered Listener.
  4. Since we registered a listener for the HighOrderValueCreated event, the handleEventmethod on the ProcessGiftcardsclass is now called. ( See How it works for a detailed explanation ).
  5. The constructor dependencies $mailer, $logger are automatically injected into the class.
  6. If the handleEvent() would have any method dependencies besides the event object, those method dependencies would have also been injected automatically.

Improving


Conditional Dispatching

You might have noticed that we have not handled the logic regarding the conditional execution based on the order value yet.

  1. We apply a Trait to our class. We also give users the choice to define a custom amount for when a gift card should be sent.

Before the Dispatcher dispatches an event that uses this trait the shouldDispatch Method is called and evaluated. If it returns false, the event will not be fired by WordPress at all. It will never hit the Plugin/Hook API and no instance of ProcessGiftCards, Mailer and Logger will be ever be created.

Interfaces instead of concrete implementations.

We now want to give users the option to use a different mailer. We support Sendgrid, AWS and MailGun. How do we do this? In order to not break the Dependency Inversion Principle we now use a MailerInterface

Assuming you are using the default Container Adapter you can now do the following:

The class will now automatically receive the correct mailer implementation based on which option the site admin currently has selected.

The configuration of the container should be handled by a separate class. For all options and documentation check out theIlluminate/Container docs over at laravel.com.

Again: if the minimum threshold for the total order value is not met, none of this will ever be executed. Everything is lazy-loaded at runtime.

I hope both agree that this implementation is cleaner and most importantly, a lot more extensible and maintainable than anything we can currently implement with the WordPress Plugin/Hook API.

Dispatching your Events:

There are two ways you can dispatch events (hooks) in your code with BetterWpHooks.

  1. Dispatching events as objects.

Instead of doing

You can do the following:

This will first create a new instance of the event passing the arguments into the constructor and then run the event through the Dispatcher instance provided by your custom class AcmeEvents ( Remember all events would extend the AcmeEvents class ).

When creating object events, all arguments that should be passed to the constructor have to be wrapped in an array.

This will not work:

Only the $book_id would be passed to the constructor.


  1. Dispatching events via the class

This approach can be used if you don't want to create a dedicated event object for an event.

This is similar to the way do_action() works, but you still get access to most features of BetterWpHooks like auto-resolution of dependencies and the testing module. However, you won't be able to use conditional dispatching of events quite the same as you would when using approach #1.

Besides, your listeners would need to accept the same number of parameters that you passed when dispatching the event. With approach #1 your listeners always receive one argument only, the event object instance.

Using the Facade class you have two options to define arguments which both have the same outcome.

However, only the first passed parameter must be the identifier of the event you want to dispatch.


Dispatching Helpers.

Sometimes you might only ever want to dispatch one of your events under specific conditions.

There is also the opposite available:


Wordpress Filters

Using the default Plugin/Hook API you need to distinguish between using add_action and . BetterWpHooks takes care of this under the hood. The syntax is the same for defining actions and filters. Let's review a simple example where we might want to allow other developers to modify appointment data created by our fictional appointment plugin.

``

A third-party developer ( or maybe a paid extension of your plugin ) might now filter the appointment object just as he normally would. Example:

``

Alternatively you could dispatch the filter like this:

``


Default return values.

BetterWpHooks recognized that you are trying to dispatch a filter and will automatically handle the case where no listeners might be created to pick up on the event. In that case no object will ever be built, in fact not event the Plugin API will be called under the hood.

Default return values are evaluated in the following order:

  1. If you are dispatching an event object you can define a ` method on the event object class. This method will be called if it exists, and the returned value will be passed as a default value.

  2. If there is no ` method on the event class, but you are dispatching an event object, the object itself will be returned. For the example above the instance of AppointmentCreated would be returned.

  3. If #1 and #2 are not possible the first parameter passed into the ` method will be returned.

Return values for invalid callback

A common pain point when offering filters is that users don't respect the API of your filter and return incompatible values. If you are using object events you can typehint the expected return value on the default() method. If the value returned from a filter is either:

  1. The same as the original payload or
  2. NOT of the same type as the typehint on default() the filtered value will be discarded, and your event`s default method will be called with both the payload, and the filtered value, giving you the option to fix things.

Example `` Assuming a third party developer would hook into your filter like this:


Valid Listeners

A listener, just like with the default WordPress Plugin/Hook API can either be an anonymous closure, or a class callable. Any of the following options are valid for creating a listener with the Dispatcher. By default, if no method is specified BetterWpHooks will try to call the ` method on your listener.

``

See the section on custom identifier for its use cases.


Dependency Resolution

Let's look at a complex example of how dependency resolution works with BetterWpHooks. This is assuming that you are using the inbuilt Illuminate/Container Adapter. With other containers, there might be slight differences with how you need to define method and constructor dependencies.

The shall handle various events regarding Bookings. ( creation, deletion, rescheduling etc.)

When a Booking is cancelled we want to notify the hotel owner and the guest, and also make an API call to booking.com to update our availability. However, we only need the BookingcomClient in one method of this EventListener let's not make it a constructor dependency.

Our class will look like this:

``

When the ` event is dispatched the following things happen:

  1. The class is instantiated.
  2. Using the the is instantiated.
  3. The class is instantiated using the new .
  4. A new is instantiated.
  5. The method is called passing in the dispatched event object and the .

Method arguments that are created from the dispatched event should always be declared first in the method signature before any other dependencies.


Conditional Event Dispatching

This is meant to be used for actions and filters whose triggering are not under your control. ( i.e. core or third-party plugin hooks).

For events that you control it's easier to just use the and helpers. However, if the logic for when your event should be dispatched is complex, it might be preferable to put this logic inside the event object.

After mapping a third-party hook to a custom event, you should use the ` Trait on your Event class.

``

The return value of the ` method is evaluated every time before anything is executed. If is returned, the WordPress Plugin/API will never be hit, neither will any class get instantiated.

Conditional Event Listening

In some cases, it might be useful to determine at runtime if a listener should handle an Event.

Let's consider the following fictional Use case:

Every time an appointment is booked you want to:

  1. Notify the responsible staff member via Slack.
  2. Send a confirmation email to the customer
  3. Add the customer to an external SaaS like Mailchimp.
  4. If the customer booked an appointment with a total value greater than $300 you also want to notify the business owner via SMS, so he can personally serve the client.

So how could we do this?

Our code dispatches an event every time an appointment is created:

``

It's clear that conditionally dispatching the event is not an option here, because we want Listeners 1-3 to always handle the event.

All listeners are correctly registered for the AppointmentCreated event.

``

This is where we can use conditional event listeners

We define the listener like this with the trait ListensConditionally

When a listener has this trait, before calling the defined method ( here it's handleEvent() ) the return value of shouldHandleis evaluated. If false is returned the method will not be executed and the object will get passed to the next listeners unchanged ( if there are any ).

``

Stopping a listener chain

If for some reason you want to stop every following listener from executing after a specific listener you can have the listener use the Trait.

Caveats: This will remove every listener registered by your instance of BetterWpHooks for the current request. The order in which listeners were registered does not matter. The first registered listener that is using the StopsPropagation Trait will be called, while every other listener will be removed for the current request.

``

If Listener4 were to use the Trait, every other Listener would be removed for Event1 during the current request.

API

In theory, you should not need to use the underlying service classes provided to you by your AcmeEvents class outside the bootstrapping process.

If that need arises, BetterWpHooks makes it easy to do so.

Your class AcmeEvents ( see Entry-Point ) serves as Facade to the underlying services, pretty similar to how Laravel Facades work.

Every static method call is not static but resolves to the underlying instance of your class using PHP's magic-method.

There is a dedicated class ` ( src) that provides IDE-autocompletion and also serves as documentation of the available methods.

Container

`

You can directly call methods on the dispatcher using your AcmeEvents class. Method calls to dispatcher methods take precedence over calls to the main ` class.

`

Marking a listener as unremovable

Sometimes third-party developers tinker too much with a plugin's codebase and remove complete callbacks using the core function `, which is fine in most cases. However, if the implications of removing a given filter might not be completely obvious and will most likely cause your plugin to be broken you can mark a listener as unremovable using the unremovable() method instead of .

The listener will now be unremovable through your AcmeEvents class, and the only other possibility would be to guess the exact spl_object_hash() since that is how WordPress creates hook-ids .

`

The following combinations are valid ways to search for a registered listener.

`

The combination of class and method has to be a match. Only the class is not enough. However, you can forget a listener by only passing the class name if you registered the listener with the default handleEvent() method.

Deleting a listener for an event.

`

Inbuilt Testing Module

To unit test code in the context of WordPress, one should not have to bootstrap the entire WordPress Core. There are two great WordPress mocking libraries out there:

Both are great and work, I have used them both before. However, it never felt right to have to use a dedicated mocking framework just so that all the code does not blow up because WordPress Core functions are undefined.

Inspired by the way Laravel handles event testing, BetterWpHooks was built with testing in mind before the first line of code was written.

There are two ways you can use the testing module with BetterWpHooks:

1. Completely swapping out the underlying dispatcher with a fake dispatcher: Using this option none of your registered listeners will be executed.

``

You can also pass a closure to the assertDispatched or assertNotDispatched` methods to assert that an event was dispatched that passes a given "truth test".

``

2. Only faking a subset of events: If you are doing Integration Testing, but you still want to fake some events, ( maybe because they are talking to a slow or unstable external API ) you might do the following:

  1. Create a test that extends BetterWpHooksTestCase
  2. In the setUp method of your test, call $this->setUpWp
  3. In the tearDown method of you test, $this->tearDownWp

BetterWpHooksTestCase extends \PHPUnit\Framework\TestCase and takes care of loading all only the WordPress Hook API. Loading single pieces of WordPress is a dangerous and brittle endeavour, which is why we created a stand-alone repo that exactly mirrors the WordPress Hook API. This repo is manually synced for every WordPress release.

BetterWpHooksTestCase also takes care of clearing the global state before and after every test.

You get the full features of the WordPress Hook API, but your tests will still run blazing fast.

Let's assume we want to test the following method.

``

The $this->bootstrapAcmeEvents(); can be anything you want, but if you want your listeners to execute you need to properly bootstrap your AcmeEventsinstance. It's recommended that you create a custom factory class and don't bootstrap your instance in your main plugin file., so you maintain greater testing flexibility.

How it works

To understand how BetterWpHooks works, it's necessary to explain how the Core Plugin/Hook API works.

At a basic level, everything you add via add_action() and add_filter() is stored in a global variable $wp_filter. ( ...yikes )

Many WP-devs don't know this, but add_action() and add_filter() are exactly the same. The add_action function only delegates to add_filter().

When either do_action('tag') or apply_filters('tag') is called, Wordpress iterates over every registered array key inside the global $wp_filter['tag'] associative array and calls the registered callback functions.

A callback can either be:

How events are dispatched


The class is responsible for dispatching events. You have access to an instance of this class via your AcmeEventsFacade.

This is a simplified version of the responsible dispatch method.

``

Like the example demonstrates, the WordPress Plugin API is used but through a layer of abstraction, BetterWpHooks can introduce most of its before we hit the Plugin API. We also might never hit it if conditions are not met

If all conditions were to pass, the following method call:

``

would be similar to:

``

Now, WordPress would call all the registered Hook Callbacks which brings us to:

How Listeners are called.


BetterWpHooks serves as a layer between your plugin code and the Plugin API. It still uses the Plugin API but in a different way.

There are 3 types of Listeners BetterWpHooks creates under the hood depending on you defined them during the bootstrapping process.

The difference between Instance Listeners and Class Listeners is, that an Instance Listener already contains an instantiated class ( because you passed it in ).

No matter which type of Listener is created, they are all wrapped inside an anonymous closure which is then passed to the WordPress Plugin API.

This happens inside the ListenerFactory class.

``

WordPress does not directly call the class callable. It only knows about the anonymous closure which when executed will execute the listener if conditions are met.

Like this, we can achieve lazy instantiation of objects and put an IoC-Container in between WordPress and the Listener. The actual building of the happens inside the execute() method which is defined in the AbstractListener class and differs a bit for every listener type.

Compatibility

BetterWpHooks is 100% compatible with how the WordPress Plugin/Hook API works.

No more accessing the global $wp_filter or editing source files because hooks are unremovable. You also don't have to remember the hook priority like you would when trying to remove a hook with remove_filter() .

One caveat:

If you are using this library, or any other third-party dependency for that matter in a plugin that you plan on distributing on WordPress.org there is the risk of running into conflicts, when two plugins require the same dependency but bundle it in different versions.

The composer autoloader will only load the version that is first required. Since WordPress loads plugins alphabetically there might be issues if your plugin relies on features, that are only implemented in newer versions of a dependency, while the plugin that was first loaded required an older version.

This is not an issue of composer, nor of this library, but from WordPress still not having a dedicated solution for dependency management in 2021.

Until WordPress finds are way to solve this, the only way to be 100% is to wrap every dependency that your plugin has in your own namespace (...yikes again).

However, there are projects that facilitate this process:

For further info on this matter check out this article and especially the comment section.

https://wppusher.com/blog/a-warning-about-using-composer-with-wordpress/

TO-DO

Contributing

BetterWpHooks is completely open-source and everybody is encouraged to participate by:

Credits


All versions of better-wordpress-hooks with dependencies

PHP Build Version
Package Version
Requires php Version ^7.3
illuminate/support Version 8.35.1
calvinalkan/base-container Version 0.1.5
calvinalkan/wordpress-hook-api-clone Version 5.7.2
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package calvinalkan/better-wordpress-hooks contains the following files

Loading the files please wait ....