Introduction to Hooks

In this introduction we're going to look at the concept and rationale behind hooks.

A hook is basically a user-configurable event between a 'subscriber' and a 'provider' managed by the HookDispatcher. So this means there are two parties which are 'connected together'. The mediator of this connection is a hook event object.

By 'user-configurable' it is meant that the user many connect provider hooks to subscribers and control the order in which those providers are notified via user-interface within the administration functions of the concerned modules.

Hooks are generally associated with the view in MVC, modifying the view in some way or another. This allows providers to modify content from a subscriber. An example would be to add a comments form to a blog post or to selectively apply filters to the body of the blog post.

Hook areas

As we have seen, hooks consist of 'providers' and 'subscribers' however, it would be more accurate to talk about 'provider hook areas' and 'subscriber hook areas'.

A 'subscriber area' is a single distinct area usually on a form. The reason for subscriber areas is that it gives the developer precise control over where a provider is attached.

A distinct provider can be attached to a distinct subscriber area. The order of provider areas can be varied to control the order they appear in the subscriber area.

Hook categories

Areas are classified into categories. This is to ensure that only compatible hooks can be connected together. A category is "workflow" with contracted points of interaction where data is shared between the subscriber and provider. A category can be defined by any bundle as it is only a contract of area names. It is strongly recommended that these area names be published as constants in a class definition extending \Zikula\Bundle\HookBundle\Category\CategoryInterface.

The Core defined categories are:

  • \Zikula\Bundle\HookBundle\Category\FilterHooksCategory
  • \Zikula\Bundle\HookBundle\Category\UiHooksCategory
  • \Zikula\Bundle\HookBundle\Category\FormAwareCategory (added in Core-1.5.0)

Hook bundles

The next concept is the 'bundle'. To manipulate one subscriber area may require several separate related steps. For example, the usual workflow of a new/edit form would be to display new/edit form in the view. To receive the input in the controller and validate the input. If valid, the data would be committed to the persistence layer, and if not valid, the form would be redisplayed in the view with the validation errors.

It stands to reason then, that if a hook provider seeks to add some additional fields on a form, that those fields would also require validation. It would not make sense to accept the valid subscriber's form only, and disregard the validity of the hooks fields.

Bundles allow us to attach a group of provider handlers to listen to these distinct events, create, edit, validate, process (insert/update/delete to persistence). This allow us to maintain some integrity and give an expected result to the user who ultimately decides to connect providers to subscribers.

Owners

The last grouping of hook areas is into owner, i.e. between two applications, the provider and the subscriber. This will typically be the application name. This allows us to build a user interface and group areas and connections per application to give an easy user interface.

Coupling

The most important thing about hooks is their generic quality. However, we need to qualify what we mean by 'generic'.

In the a previous section we specifically talked about compatibility between subscriber and providers. This implies that there must be a contract between the subscribing and providing sides: when we talk about coupling, it is between the subscriber and provider area implementations. The the contract is defined defined by category standard and the individual hook types within that category.

This does not mean that the applications on either side have a contract with each other. The only coupling is what the subscriber and provider expect from each other in the context of the hook category and specific hook being notified. The key words are "what is expected within that category". This means that both sides are coupled by what the category defines. That is what makes the hooks implementation 'generic' for that category. It means that any application that understands that particular category, can talk with each-other.

You cannot have two subscribers connected to the same provider and expect the provider to behave differently based on which subscriber it's communicating with.

Ultimately, the power of Hooks is that the system in itself does not have any limitations other than what is imposed when creating the standards and contracts of category (of 'hook area').

Hooks merely connects providers and subscribers and facilitates communication between then via an "hook event interface". It is up to the creator of the category and how the hook event object is implemented beyond the required interface. This is what allows us to set up specific contracts for a given hook category.

Hooks vs events

It is worth noting some philosophical differences between Hooks and Events. The most obvious is the use of the term "Subscriber". In events, a "subscriber" is a class that provides responses to events. In hooks, a "subscriber" is the module that is contracted to call the events. So in essence, the role is reversed from what you might expect. In Hooks, the "provider" module supplies a class that provides responses.

Here are some other differences. First, events are coupled to the EventDispatcher and ultimately, uses the EventDispatcher to dispatch events.

Generic event objects are provided:

  • Symfony\Component\EventDispatcher\Event
  • Symfony\Component\EventDispatcher\GenericEvent

This is really for convenience but could be probably a source of confusion at the same time. Ultimately, every event that is triggered is different, but it is clearer if using specific event objects than one event object for everything.

new UserLoginEvent($user) is more clear to the listeners than new Event($user). It's more clear what the event object is about and the event object may provide a clearer interface - instead of some random "args" or miscellaneous $subject, we're able to create a proper interface more OO style.

The point is that regardless of whether you use a generic event object or a specific one, there is always an expectation or contract created by the one who triggers the event. That affects how the listener must interact and behave. We normally consider this tied to the event name. Each different event name requires a different set of interactions because event is different.

Hook events on the other hand may have unique event names, but they must all behave the same way according to their category and type. That means if you have a category called 'filter_hooks' and a type called 'filter', then regardless of the hook event name (which is always unique), the contract is exactly the same for that category + type. This is what allows any subscriber and provider to be connected together that have a matching category. This is what is meant by generic communication: generic by category, and the power is in the flexibility this standardisation brings.

Mechanism

So in simple terms, Hooks are a complex interpretation of the observer notification pattern. That is, events are triggered and handlers may listen to these events. The event is encapsulated by an object which implements a HookInterface.

The key difference to a standard event notifier is that the hook manager allows specific control over which handlers listen to which events and in what order. This can ultimate be controlled by a UI or other mechanism.