# Subscribers

One characteristic from the WordPress code is that anything starts with a hook callback registration.

This makes the code heavily relying on the WordPress hook API and the logic to register hook is repeated over and over leading to these block of code to be present anywhere inside the plugin:

For filters:

```php
add_filter('my_filter', 'my_callback');
```

For actions:

```php
add_action('my_action', 'my_callback');
```

This code repetition creates multiples issues.

First it is coupling the business logic from the application to the WordPress API which can be an issue if there is any change in that API.

The second issue is while unit testing as it makes testing more complex. The WordPress API is not defined in that context, and it forces to mock that logic for all hooks from the class each time a new test for the business logic is created.

To solve that it is possible to separate the actual registration from the callback function to the hook from the mapping from callback to the hook declaration.

In the same shape that solution would have two entities.

A subscriber holding the business logic and the mapping from callbacks to the hooks declarations.

An event manager that would be registering hooks to the WordPress API from a list of mapping from callbacks to the hooks.

{% @mermaid/diagram content="flowchart TB
subgraph WordPress API
Filters
Actions
end
subgraph Plugin core
EventManager
end
subgraph Business logic
FrontSubscriber
AdminSubscriber
end
FrontSubscriber -- Send mapping --> EventManager
AdminSubscriber -- Send mapping --> EventManager
EventManager -- Register callbacks --> Filters
EventManager -- Register callbacks --> Actions" %}

Now that the overall picture is given it is now time to go into the implementation details.

For the mapping an array will be using following that format.

Each key from the array will the name of the hook to register.

Each value from the array will be an array for each callback linked to that hook. Each callback being defined by three elements:

* the name from the callback method.
* the priority of that callback.
* the number of parameters from the callback.

To summarize it will have the following shape.

```php
[
    "my_hook" => [
        [
            "my_callback",
            10,
            2
        ],
        [
            "my_second_callback",
            10,
            0
        ],
    ],
    "my_second_hook" => [
        [
            "my_third_callback",
            10,
            3
        ],
    ],
]
```

This is this data that will the interface for callback and hooks mapping between the subscribers and the event manager.

Note: It is not necessary to indicate if the hook is a filter or an action inside the mapping.

This is due to the fact that for WordPress actions are filters that return nothing. Due to that it is possible to register an action callback as a filter callback, and it will be still called while using `do_action` method.

The event manager system translates to that classes.

{% @mermaid/diagram content="classDiagram

```
namespace BusinessLogic {

    
    class Subscriber {
    public get_subscribed_events() array
    public my_callback($data, $id) void
    public my_second_callback() void
    public my_third_callback($post, $id, $user) void
    }
    
}

namespace PluginCore {
    class SubscriberInterface {
        <<interface>>
        public get_subscribed_events() array
    }
    
    class EventManager  {
        public add_subscriber( SubscriberInterface $subscriber ) void
    }
}

Subscriber --|> SubscriberInterface
EventManager .. SubscriberInterface: Uses" %}
```

With the following code for `SubscriberInterface` interface.

```php
interface SubscriberInterface {
    public function get_subscribed_events() array;
}
```

For the `Subscriber` it will have to implement the `get_subscribed_events` from the `SubscriberInterface` interface to return the mapping from callbacks to their respective hooks.

```php
class Subscriber implements SubscriberInterface {
    public function get_subscribed_events() array {
        return [
                    "my_hook" => [
                        [
                            "my_callback",
                            10,
                            2
                        ],
                        [
                            "my_second_callback",
                            10,
                            0
                        ],
                    ],
                    "my_second_hook" => [
                        [
                            "my_third_callback",
                            10,
                            3
                        ],
                    ],
                ];
    }
    
    public function my_callback($data, $id): void {
        // logic from the callback
    }
    
    // other callback methods
}
```

Finally, the last step is to implement the event manager.

First the method `add_callback` will have to implemented to register a callback. To register hooks it is possible to use only the function `add_filter` as for WordPress actions are in fact filters that returns nothing.

```php
    protected function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
        add_filter( $hook_name, $callback, $priority, $accepted_args );
    }
```

Once the method `add_callback` is created then the wiring between the `add_subscriber` method and the callback registration needs to be created to register all callbacks for each hook when a subscriber is passed.

```php
class EventManager {
    public function add_subscriber( SubscriberInterface $subscriber ): void {
       
        $events = $subscriber->get_subscribed_events();

        if ( empty( $events ) ) {
            return;
        }

        foreach ( $events as $hook_name => $parameters ) {
               if( ! is_array( $parameters ) ) {
                    $class_name = get_class($subscriber);
                    throw new Exception("Invalid hook format for $hook_name from $class_name ");
               }
        
            $this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 );
        }
    }
    
    protected function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
        add_filter( $hook_name, $callback, $priority, $accepted_args );
    }
}
```

Finally, the last step is to wire everything inside the main plugin PHP file.

```php
$event_manager = new EventManager();

$subscriber = new Subscriber();

$event_manager->add_subscriber($subscriber);
```

This way the business logic is now decoupled from the WordPress API and in case future changes are needed to be related to that it would impact only the event manager and not anymore the business logic.

In the same way it is also possible to unit test the business logic without having to mock `add_filter` and `add_action` functions.

However, the definition from the subscriber is verbose and needs to be simplified to make it easier for the developer and less error-prone.

A good interface would be using a docblock as following and abstract the hook registration.

```php
/**
* @hook my_hook 10
 */
public function my_callback() {

}
```

To achieve that two steps would have to be added to the previous process.

First a detection from the hooks inside the doc-blocks from each method from subscribers.

Second an adapter to make the new subscriber type able to work with the event manager which requires an `SubscriberInterface` type to register the callbacks.

{% @mermaid/diagram content="classDiagram
namespace PluginCore {
class SubscriberInterface {
<<interface>>
public get\_subscribed\_events() array
}

```
    class SubscriberAdapter {
        public __construct(string $class, array $events)
    }
    
    class SubscriberAdapterFactory {
        public make(string $class) SubscriberInterface
    }
}

SubscriberAdapter --|> SubscriberInterface
SubscriberAdapterFactory .. SubscriberInterface: Uses
SubscriberAdapterFactory .. SubscriberAdapter: Creates" %}
```

With the following code for the `SubscriberAdapter`.

```php
class SubscriberAdapter implements SubscriberInterface
{

    protected $object;

    protected $events;

    public function __construct( $object, array $events = [] )
    {
        $this->object = $object;
        $this->events = $events;
    }

    public function get_subscribed_events(): array
    {
        return $this->events;
    }

    public function __call($name, $arguments)
    {


        if( method_exists( $this, $name ) ) {
            return $this->{$name}(...$arguments);
        }

        return $this->object->{$name}(...$arguments);
    }
}
```

And the following logic for `SubscriberAdapterFactory` .

```php
class SubscriberAdapterFactory
{
    public function make($object): SubscriberInterface
    {
        $methods = get_class_methods($object);
        $reflectionClass = new ReflectionClass($object);
        $events = [];
        foreach ($methods as $method) {
            $method_reflection = $reflectionClass->getMethod($method);
            $doc_comment = $method_reflection->getDocComment();
            if ( ! $doc_comment ) {
                continue;
            }
            $pattern = "#@hook\s(?<name>[a-zA-Z\\\-_$/]+)(\s(?<priority>[0-9]+))?#";

            preg_match_all($pattern, $doc_comment, $matches, PREG_PATTERN_ORDER);
            if(! $matches) {
                continue;
            }

            foreach ($matches[0] as $index => $match) {
                $hook = $matches['name'][$index];

                $events[$hook][] = [
                    $method,
                    key_exists('priority', $matches) && key_exists($index, $matches['priority']) && $matches['priority'][$index] !== "" ? (int) $matches['priority'][$index] : 10,
                    $method_reflection->getNumberOfParameters(),
                ];
            }
        }

        return new SubscriberAdapter($object, $events);
    }
}
```

Once the two parts are implemented then it is time to add it to the previous logic to load the new subscribers.

```php
$event_manager = new EventManager();

$subscriber = Subscriber::class;

$subscriber_adapter_factory = new SubscriberAdapterFactory();

$subscriber = $subscriber_adapter_factory->make($subscriber);

$event_manager->add_subscriber($subscriber);
```

With that new logic it is possible to load subscribers using doc-blocks for registering hooks.

However, even now subscribers are more pleasant to work with a major issue is still present. Currently, all subscribers will be loaded on the initial load from the plugin even they are not used.

For a small plugin that is not an issue but when it is going to grow then this will become a performance issue that needs to be tackled.

For that a lazy load from the subscribers will have to be implements, so they will be loaded just in time when they are needed.

To achieve this the `SubscriberAdapter` class will have to become a proxy class which will handle the lazy load logic from the subscriber the following way.

{% @mermaid/diagram content="flowchart TB
Initial\[The proxy instantiates with the subscriber name]
Called\[A method is called on the proxy]
CheckHooks{{Is the method called get\_subscribed\_events ?}}
Hooks{{Return hooks mapping}}
Check{{The proxy checks if the subscriber is loaded}}
Load\[The proxy loads the subscriber]
Loaded\[The proxy forwards the call to the subscriber]
Initial -.-> Called
Called --> CheckHooks
CheckHooks -- "No" --> Check
CheckHooks -- "Yes" --> Hooks
Hooks -.-> Called
Check -- "Not loaded" --> Load
Load --> Loaded
Check -- "Loaded" --> Loaded
Loaded -.-> Called" %}

Which is going to translate to the following implementation.

```php
class SubscriberProxy implements SubscriberInterface
{

    protected $object;

    protected $events;

    protected $instance;

    public function __construct( string $object, array $events = [] )
    {
        $this->object = $object;
        $this->events = $events;
    }

    public function get_subscribed_events(): array
    {
        return $this->events;
    }

    public function __call($name, $arguments)
    {


        if( method_exists( $this, $name ) ) {
            return $this->{$name}(...$arguments);
        }
        
        if( ! $this->instance ) {
            $this->instance = new $this->object;
        }

        return $this->instance->{$name}(...$arguments);
    }
}
```

And the following modification inside `SubscriberAdapterFactory` which going to be renamed `SubscriberProxyFactory`.

```php
class SubscriberProxyFactory
{
    public function make(string $object): SubscriberInterface
    {
        $methods = get_class_methods($object);
        $reflectionClass = new ReflectionClass($object);
        $events = [];
        foreach ($methods as $method) {
            $method_reflection = $reflectionClass->getMethod($method);
            $doc_comment = $method_reflection->getDocComment();
            if ( ! $doc_comment ) {
                continue;
            }
            $pattern = "#@hook\s(?<name>[a-zA-Z\\\-_$/]+)(\s(?<priority>[0-9]+))?#";

            preg_match_all($pattern, $doc_comment, $matches, PREG_PATTERN_ORDER);
            if(! $matches) {
                continue;
            }

            foreach ($matches[0] as $index => $match) {
                $hook = $matches['name'][$index];

                $events[$hook][] = [
                    $method,
                    key_exists('priority', $matches) && key_exists($index, $matches['priority']) && $matches['priority'][$index] !== "" ? (int) $matches['priority'][$index] : 10,
                    $method_reflection->getNumberOfParameters(),
                ];
            }
        }

        return new SubscriberAdapter($object, $events);
    }
}
```

Now the performance issue is finally addressed the subscribers are finally completed and allow clean code while not making the developer experience or performance any worse.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wp-launchpad.gitbook.io/launchpad/tutorials/notions/framework-concepts/subscribers.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
