Skip to content

Modules

Miguel Muscat edited this page Nov 30, 2022 · 2 revisions

Modules form the backbone of plugins built with the SDK. They are intended to encapsulate all plugin data, objects, and logic in a compartmentalized and extensible way.

To create a module, declare a class that extends RebelCode\WpSdk\Module. There are no required methods; simply override the methods that your module needs.

Factories

Factories are service declarations that will populate the plugin's DI container with services.

To declare factories in your module, override the getFactories() method. This method should return an associative array where the keys represent the IDs of the services, and the values are the factory instances.

use RebelCode\WpSdk\Module;
use Dhii\Services\Factory;
use Dhii\Services\Factories\Constructor;
use Dhii\Services\Factories\Value;

class MyModule extends Module
{
    public function getFactories(): array
    {
        return [
            'message' => new Value('Hello, world'),
            'greeter' => new Constructor(Greeter::class, ['message']),
        ];
    }
}

The factories MUST be instances of Dhii\Services\Service, which are provided by the dhii/services package.

Extensions

Extensions are service declarations that will modify existing services in the plugin's DI container.

To declare extensions in your module, override the getExtensions() method. This method should return an associative array where the keys represent the IDs of the services that you are extending, and the values are the extension instances.

use RebelCode\WpSdk\Module;
use Dhii\Services\Extension;
use Dhii\Services\Factories\Constructor;
use Dhii\Services\Factories\Value;
use Psr\Container\ContainerInterface;

class OtherModule extends Module
{
    public function getExtensions(): array
    {
        return [
            'message' => new Extension([], function (ContainerInterface $c, string $message) {
                return $message . '!';
            }),
        ];
    }
}

The extensions MUST be instances of Dhii\Services\Service, which are provided by the dhii/services package. For most cases, you'll want to specifically use instances that extend Dhii\Services\Extension, but regular factories will also work - they will simply override the existing service rather than modify it.

Hooks

Modules can hook into WordPress in a declarative way, similar to factories and extensions.

This is done by overriding the getHooks() method. This method should return an associative array where the keys represent the hook names, and the values are the hook handlers.

The hook handlers MUST be instances of RebelCode\WpSdk\Handler. Arrays of hook handler are also allowed.

Example 1: Single hook handler

use RebelCode\WpSdk\Handler;
use RebelCode\WpSdk\Module;

class OtherModule extends Module
{
    public function getHooks(): array
    {
        return [
            'admin_notices' => new Handler(['message'], function (string $message) {
                printf(
                    '<div class="notice notice-info"><p>%s</p></div>',
                    esc_html($message)
                );
            }),
        ];
    }
}

Example 2: Multiple hook handlers

use RebelCode\WpSdk\Handler;
use RebelCode\WpSdk\Module;

class OtherModule extends Module
{
    public function getHooks(): array
    {
        return [
            'admin_notices' => [
                new Handler([/* ... */], function () {
                    /* ... */
                }),
                new Handler([/* ... */], function () {
                    /* ... */
                }),
            ]
        ];
    }
}

Handlers are instantiated in a similar way to factories. The Handler constructor accepts:

  • A list of dependencies, as service IDs.
  • The callback function of the handler.
  • An optional hook priority, which defaults to 10.
  • An optional number of arguments, which defaults to 1.

The Run Method

Modules can have a run() method that runs when the plugin itself is run().

The method receives the plugin's DI container as an argument. This can be used to retrieve services from the container to use in the module's logic.

use RebelCode\WpSdk\Module;
use Psr\Container\ContainerInterface

class MyModule extends Module
{
    public function run(ContainerInterface $c): void
    {
        $message = $c->get('message');
        
        add_action('admin_notices', function () use ($message) {
            printf(
                '<div class="notice notice-info"><p>%s</p></div>',
                esc_html($message)
            );
        });
    }
}

For WordPress plugins, overriding the getHooks() method should be sufficient most of the time, and is the recommended way to hook into WordPress.

Module Scoping

The module system will automatically add the module's ID as a prefix to all of its services. This is done to prevent accidental collisions between services of different modules, allowing you to use simple service names without having to worry about conflicts.

For example, consider the below module:

// SettingsModule.php

use RebelCode\WpSdk\Module;

class SettingsModule extends Module 
{
    public function getFactories(): array
    {
        return [
            'save_alert' => new Value('Settings saved!'),
        ];
    }
}

If the module is registered with the ID "settings":

// modules.php

return [
    'settings' => new SettingsModule(),
];

Then its save_alert service will be available from the container with the ID settings/save_alert:

$plugin->get('settings/save_alert');

However, modules do not need to use their own IDs as prefixes. The prefixes are only required when a module's services are being accessed from outside that module.

Example:

// SettingsModule.php
use RebelCode\WpSdk\Module;
use Psr\Container\ContainerInterface;

class SettingsModule extends Module
{
    public function run(ContainerInterface $c): void
    {
        // Notice how the "save_handler" service ID is not prefixed
        $c->get('save_handler')->listen();
    }
    
    public function getFactories(): array
    {
        return [
            'save_alert' => new Value('Settings saved!'),
            // Notice how the "save_alert" dependency is not prefixed
            'save_handler' => new Constructor(SaveHandler::class, ['save_alert']),
        ];
    }
}

Inter-module dependencies

When a module service has dependencies from a different module, that service will need to use the ID prefix, as mentioned in the previous section. However, that service will also need to use the @ prefix.

Example:

// modules.php

return [
    'settings' => new SettingsModule(),
    'notifications' => new NotificationsModule(),
];
// SettingsModule.php

use RebelCode\WpSdk\Module;
use Psr\Container\ContainerInterface;

class SettingsModule extends Module
{
    public function getFactories(): array
    {
        return [
            'save_handler' => new Constructor(SaveHandler::class, [
                // Notice the "@" prefix
                '@notifications/settings_saved'
            ]),
        ];
    }
}

This serves two purposes:

  1. It tells the module system that this dependency ID already has a prefix, and should not be prefixed with settings/, which would result in settings/notifications/settings_saved.
  2. It communicates to the reader where that dependency can be found.

Important: The @ prefix is only required in service dependencies. It should NOT be used in extension IDs. Those IDs are already assumed to be "external" to the module.

Example:

class SettingsModule extends Module
{
    public function getExtensions(): array
    {
        return [
            // Notice how the extension does not have the "@" prefix
            'notifications/settings_saved' => new Extension(/* ... */),
        ];
    }
}

The Scoping Delimiter

By default, services are prefixed with the module ID, followed by a / delimiter. You can change this delimiter by specifying it as the second argument in the plugin's static factory method:

Examples #1:

$plugin = Plugin::create('my-plugin', '.');
$plugin->get('settings.save_alert');

Examples #2:

$plugin = Plugin::create('my-plugin', '->');
$plugin->get('settings->save_alert');

It is safe to use the delimiter inside your service IDs. The module system does not interpret delimiters in any way. In fact, we encourage the use of "hierarchical" service IDs using the same delimiter.

// SettingsModule.php
class SettingsModule extends Module
{
    public function getFactories(): array
    {
        return [
            'assets::js' => new Script::factory(
                'my-settings-js',
                'js/settings.js',
                '1.0',
                ['jquery'],
                'assets::js::l10n'
            ),
            'assets::js::l10n' => new Factory([], function () {
                return new ScriptL10n([
                    'ajaxUrl' => admin_url('admin-ajax.php'),
                    'nonce' => wp_create_nonce('save_my_settings'),
                ]);
            }),
        ];
    }
}

// plugin.php
$plugin = Plugin::create(__FILE__, '::');
$plugin->get('settings::assets::js::l10n');

By doing so, your service IDs consistent will be consistent and will take on a "tree-like" structure. This is actually the reason why we chose the / path-like delimiter by default.

Built-in Modules

The SDK automatically adds the following modules to your plugin:

These modules add useful functionality that is common to most plugins, and integrate parts of WordPress into the module system.

Clone this wiki locally