Creating a Module

1. Implement Required Interfaces

In your plugin class, implement IModule and any scope interface you need:

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

These lifecycle methods replace onInit() for module-specific initialization. Keep onInit() only for logic that must run unconditionally (e.g. facade setup).

2. SQL Migration

Register the module in the modules table. The plugin column holds the class name without the Plugin suffix.

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);

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);

Set is_workspace = true/1 for workspace-scoped modules.

3. Add a Menu Item (optional)

Implement IModuleMenuFetcher and add the item inside onFetchModuleMenuHandler. Gate it behind a permission check.

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
     */
    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',
        ];
    }
}