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.
- 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);
- (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',
];
}
}
- 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
.
- 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;
}
- 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
);
- 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
- To Update plugin you have to implement the following interfaces:
IModule
- (optionally)
ISystemSpaceModule
,IModuleMenuFetcher
if needed.
- 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 theonLoadSystemSpaceModule
,onLoadWorkSpaceModule
, oronLoadUserSpaceModule
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().
- 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(...);