Modules Plugin

The Modules Plugin provides a robust, extendable architecture for managing business logic in a scalable and modular way. It enables developers to define core logic as Plugins and extend it using independent Modules.

Core Concepts

We have two primary component types: Plugin and Module.

  • Plugin is a system-level component. It describes the direct business logic of the project. Plugins can depend on each other because we guarantee that all of them exist in the system. When you need any plugin, you can get an instance of the Plugin and call the method you need.

  • Module is an extendable component. It extends the Plugin's logic or creates new business logic based on existing plugins.

  • Modules can depend on plugins, but cannot depend on other modules.

To get a Plugin instance:

$this->plugin->mail->send(...);
Core::getInstance()->getPluginInstance('Mail')->method(...);

To get a Module instance: Note: The module is always accessed by its class name without Plugin suffix (case-sensitive).

/** @var TestModulePlugin $plugin */
$plugin = ModulesManager::getInstance()->getModule('TestModule');
assert($plugin instanceof TestModulePlugin);

You can use events or observer listeners for communication between your module and plugins.


Create a New Module

They can be created at three levels:

  • System
  • Workspace
  • User

  • Create a plugin and implement the following interfaces:

    • IModule
    • (optionally) ISystemSpaceModule, IModuleMenuFetcher if needed.

  • Module Init onLoadSystemSpaceModule() If your module implements ISystemSpaceModule, you must override the onLoadSystemSpaceModule() method. Use it as empty method next to onInit().

    Each level requires its corresponding lifecycle method next to onInit():

    Level Interface Lifecycle Method
    System ISystemSpaceModule onLoadSystemSpaceModule()
    Workspace IWorkSpaceModule onLoadWorkSpaceModule()
    User IUserSpaceModule onLoadUserSpaceModule()

💡 These methods replace onInit() for module initialization.

  1. Create a SQL migration to register the module:
INSERT INTO modules (caption, ident, plugin, required, description, hooks, is_active, is_workspace)
VALUES ('Test Module', 'test_module', 'TestModule', NULL, 'Test description.', NULL, true, true);
  1. (optional) Create and implement an observer endpoint listener that you need, for example plugin\Modules\domain\event\IModuleStartListener. Also, you can describe an event into init.php in the Plugin.

Implement event listeners if your module needs to handle specific system events, for example:

For adding Menu item use onFetchModuleMenuHandler from IModuleMenuFetcher

use DisplayPlugin;
use plugin\Modules\IModule;
use plugin\Modules\domain\listener\IModuleMenuFetcher;
use plugin\Modules\domain\model\MenuItemValuesObject;

class TestModulePlugin extends DisplayPlugin implements IModule, IModuleMenuFetcher
{
   /**
    * @event IModuleMenuListener
    * @param array $menuItems
    * @param string|null $area
    * @return void
    */
    public function onFetchModuleMenuHandler(array &$menuItems, ?string $area): void
    {
        if (!$this->hasUserPermissionToSection('some_section')) {
            return;
        }

        $menuIdents = array_column($menuItems, MenuItemValuesObject::FIELD_IDENT);
        $idParentMenuItem = array_search('parent', $menuIdents);

        if (!$idParentMenuItem || empty($menuItems[$idParentMenuItem])) {
            return;
        }

        if (empty($menuItems[$idParentMenuItem][MenuItemValuesObject::FIELD_ITEMS])) {
            $menuItems[$idParentMenuItem][MenuItemValuesObject::FIELD_ITEMS] = [];
        }

        $menuItems[$idParentMenuItem][MenuItemValuesObject::FIELD_ITEMS][] = [
            MenuItemValuesObject::FIELD_CAPTION => __('Menu item 1'),
            MenuItemValuesObject::FIELD_URL     => $this->getUrl('/test/module/menu/'),
            MenuItemValuesObject::FIELD_IDENT   => 'menu-item-1',
        ];
    }
}

  1. Create SQL migration for the module (parameter is_workspace depends on the module level) :

for MySql

INSERT INTO `modules` (`caption`, `ident`, `plugin`, `required`, `description`, `hooks`, `is_active`, `is_workspace`)
VALUES ('Test Module', 'test_module', 'TestModule', NULL, 'Test description.', NULL, 1, 1);

for Postgres

INSERT INTO modules (caption, ident, plugin, required, description, hooks, is_active, is_workspace)
    VALUES ('Test Module', 'test_module', 'TestModule', NULL, 'Test description.', NULL, true, true);

Module options

(optional) If Module should have specific configuration options. You can add it to module_options table.

To add an option, create a SQL migration like this:

for MySql

SET @id_module_test = (SELECT `id`
                       FROM `modules`
                       WHERE `ident` = 'test_module');

INSERT INTO `module_options` (`id_module`, `caption`, `name`, `type`, `value`, `required`, `id_workspace`)
VALUES (@id_module_test, 'Test numbers', 'number', 'number', '3', 1, NULL);

for Postgres

DO $$
    DECLARE
        id_module_test integer;
    BEGIN
        SELECT id INTO id_module_test FROM modules WHERE ident = 'test_module';

        INSERT INTO module_options (id_module, caption, name, type, value, required, id_workspace)
        VALUES (id_module_test, 'Test numbers', 'number', 'number', '3', true, NULL);
    END;
$$;

In your module, you can then access these options:

use DisplayPlugin;
use plugin\Modules\IModule;

class TestModulePlugin extends DisplayPlugin implements IModule
{
   public function getNumber(): int
   {
      return (int) $this->getOption('number');
   }
}

Module events

If you need to create an extended endpoint in your Plugin or module, you can dispatch and listen to custom events using ModulesManager.

  1. Create an interface to describe the event listener method in the plugin folder:
namespace plugin\TestPlugin\domain\event;

use plugin\TestPlugin\domain\model\TestValuesObject;

interface ITestListener
{
    public function onTestHandler(TestValuesObject $testValuesObject): void;
}
  1. Now you can call your endpoint in one of these ways:
use plugin\Modules\ModulesManager;
use plugin\TestPlugin\domain\event\ITestListener;

ModulesManager::getInstance()->dispatchObserverEndpoint(
    ITestListener::class,
    function (ITestListener $instance, ...$args) {
        $instance->onTestHandler(...$args);
    },
    $testValuesObject
);

// OR

ModulesManager::getInstance()->dispatchObserverEndpointByMethodName(
    ITestListener::class,
    'onTestHandler',
    $testValuesObject
);
  1. Now, any module can Subscribe to the event inside any module:

Warning: Event arguments are passed by reference. Changing them affects the original data.

use DisplayPlugin;
use plugin\Modules\IModule;
use plugin\TestPlugin\domain\event\ITestListener;

class TestModulePlugin extends DisplayPlugin implements IModule, ITestListener
{
    public function onTestHandler(TestValuesObject $testValuesObject): void
    {
        // Handle the event
    }
}

Warning: Event arguments are passed by reference. Changing them affects the original data.


Store events

Modules can also subscribe to system events related to ModuleWorkSpacesStore.

Supported events:

BeforeUpdateModuleEvent

The BeforeUpdateModuleEvent class is an event that is triggered before a module is updated.

Triggered before a module is updated, use BeforeUpdateModuleEvent class

Method: * getModule(): Returns the ModuleValuesObject associated with this event.

public function onBeforeUpdateModuleHandler(BeforeUpdateModuleEvent $event): void
{
    $module = $event->getModule();
    // Your logic before updating the module
}

BeforeUpdateWorkSpaceModuleEvent

Triggered before a workspace module is updated.

Methods: * getModule(): Returns the ModuleValuesObject associated with this event. * getWorkSpace(): Returns the IWorkSpace associated with this event.

public function onBeforeUpdateWorkSpaceModuleHandler(BeforeUpdateWorkSpaceModuleEvent $event): void
{
    $module = $event->getModule();
    $workspace = $event->getWorkSpace();
    // Your logic before updating workspace module
}

CreateWorkSpaceProviderEvent

Triggered to create a workspace provider.

Methods: * getWorkSpaceProvider(): Returns the IWorkSpaceProvider instance or null if not set. * setWorkSpaceProvider(IWorkSpaceProvider $workSpaceProvider): Sets the workspace provider instance.

public function onCreateWorkSpaceProviderHandler(CreateWorkSpaceProviderEvent $event): void
{
    $provider = $event->getWorkSpaceProvider();
    // Set your workspace provider if needed
}

InitWorkSpacesModulesStoreEvent

Triggered to initialize the ModuleWorkSpacesStore.

Methods: * getStore(): Returns the ModuleWorkSpacesStore instance to be initialized.

public function onInitWorkSpacesModulesStoreHandler(InitWorkSpacesModulesStoreEvent $event): void
{
    $store = $event->getStore();
    // Your logic for initializing the store
}

InitWorkSpacesModulesStoreEvent

The InitWorkSpacesModulesStoreEvent class is an event that is triggered to initialize the ModuleWorkSpacesStore.

Methods: * getStore(): Returns the ModuleWorkSpacesStore instance to be initialized.


Upgrade Plugin to Module

  1. To Update plugin you have to implement the following interfaces:
    • IModule
    • (optionally) ISystemSpaceModule, IModuleMenuFetcher if needed.

  2. Module Init onLoadSystemSpaceModule() If your module implements ISystemSpaceModule, you must override the onLoadSystemSpaceModule() method. It replaces the standard onInit() method in modules.

Important: Modules should keep onInit() method in the plugin. And just create an empty implementation of the onLoadSystemSpaceModule, onLoadWorkSpaceModule, or onLoadUserSpaceModule method depending on the module level.

Example: migration from onInit() to onLoadSystemSpaceModule() based on Books module: Previous plugin code (with onInit()):

public function onInit()
{
    parent::onInit();
    $this->_booksFacade = $this->_getBooksFacadeInstance();
}

Converted module code (with onLoadSystemSpaceModule()):

public function onInit()
{
    parent::onInit();
    $this->_booksFacade = $this->_getBooksFacadeInstance();
}

public function onLoadSystemSpaceModule(): void
{
}
Note: parent::onInit() is no longer needed in modules initialisation method. Only module-specific initialization should be kept inside onLoadSystemSpaceModule().

  1. Create a SQL migration to register the module and update navigation: DELETE OLD Menu Item from navigation ** add it to modules2workspaces** table if module is Workspace level .

INSERT INTO modules (caption, ident, plugin, required, description, hooks, is_active, is_workspace)
VALUES ('Test Module', 'test_module', 'TestModule', NULL, 'Test description.', NULL, true, true);
- Delete old navigation from database
DELETE FROM festi_menus
WHERE url = '/test_plugin_menu/';

DELETE FROM festi_url_rules
WHERE plugin = 'TestModule';
- Add new module relation to all workspaces
INSERT INTO modules2workspaces (id_module, id_workspace, is_active)
SELECT (SELECT id FROM modules WHERE ident = 'test_module'), id, TRUE
FROM schools
WHERE type = 'general';

  • Add Module options to module_options table if needed.

  • To show menu item in navigation Implement event listeners like it is described in module creation section:

    public function onFetchModuleMenuHandler(array &$menuItems, ?string $area): void

now you can get a Module instance like this:

/** @var TestModulePlugin $plugin */
$plugin = ModulesManager::getInstance()->getModule('TestModule');
assert($plugin instanceof TestModulePlugin);

or like a plugin if you kept onInit() method in the plugin:

$this->plugin->mail->send(...);
Core::getInstance()->getPluginInstance('Mail')->method(...);