Abstract Module Class
Revision: 2026/01/28
When creating a new module, the first step is to create a folder inside /Modules/module-name.
Once the folder is created, you need to create a file {module-name}Module.php
The module file is automatically loaded by Get::loadModules();
Any PHP code written inside is therefore executed on every page.
If you want to use the recommended structure, you can create a class inside the module file that extends AbstractModule called {ModuleName}Module.
Two Ways to Configure a Module
There are two ways to configure a module:
- New method (recommended): Using the
configure()method with fluent syntax - Legacy method: Using protected properties (still supported for backward compatibility)
Method 1: Using configure() (Recommended)
The recommended way to configure a module is using the configure() method with the ModuleRuleBuilder. This provides a clean, fluent interface similar to the RuleBuilder for models.
namespace Modules\Posts;
use App\Abstracts\AbstractModule;
class PostsModule extends AbstractModule
{
protected function configure($rule): void
{
$rule->page('posts')
->title('Posts Management')
->access('authorized')
->permissions(['access' => 'Access Posts'])
->menu('Posts', '', 'bi bi-file-earmark-post-fill', 10)
->menu('Categories', 'action=categories', 'bi bi-tags-fill', 20)
->version(250901);
}
public function bootstrap()
{
// Bootstrap code here
}
}
Available Configuration Methods
| Method | Parameters | Description |
|---|---|---|
page(string) |
Module name | Sets the module page name (used in URLs: ?page=posts) |
title(string) |
Module title | Sets the module title displayed in the interface |
menu(string, string, string, int) |
name, url, icon, order | Adds a menu link to the sidebar. URL is relative to page. Order determines position (lower = higher) |
menuLinks(array) |
Array of links | Sets all menu links at once. Array format: [['name'=>'...', 'url'=>'...', 'icon'=>'...', 'order'=>10]] |
access(string) |
public|registered|authorized|admin | Sets the access level required for the module |
permission(array) |
['key' => 'description'] | Defines permission for 'authorized' access. Example: ['access' => 'Access Posts'] |
router($router) |
Class name or instance | Sets a custom router (string class name or object instance) |
model($model) |
Class name or instance | Sets a custom model (string class name or object instance) |
shell($shell) |
Class name or instance | Sets a custom shell handler for CLI commands |
hook($hook) |
Class name or instance | Sets a custom hook handler |
api($api) |
Class name or instance | Sets a custom API handler |
install($install) |
Class name or instance | Sets a custom installation handler |
disableCli() |
Disables automatic CLI commands for installation/update/uninstall | |
isCoreModule() |
Marks the module as a core system module, preventing its removal or disabling from the backend | |
version(int) |
Version number | Sets the module version (format: YYMMDD, e.g., 250901 for 2025-09-01) |
addModels(array) |
['name' => ModelClass::class] | Adds additional models for modules with multiple tables |
setJs(string) |
Path to JavaScript file | Adds a JavaScript file to load. Supports relative paths (/Assets/script.js) and absolute paths (Modules/MyModule/Assets/script.js) |
setCss(string) |
Path to CSS file | Adds a CSS file to load. Supports relative paths (/Assets/style.css) and absolute paths (Modules/MyModule/Assets/style.css) |
headerTitle(string) |
Title text | Sets the page header title (displayed in the page header area) |
headerDescription(string) |
Description text | Sets the page header description (displayed below the title) |
addHeaderLink(string, string, string) |
title, url, icon | Adds a navigation link to the header (similar to menu()) |
headerStyle(string) |
Style name | Sets header links style: pills, tabs, underline, default (default: pills) |
headerPosition(string) |
Position name | Sets header links position: top-left, top-right (default: top-left) |
If your module file and class names follow the convention, you don't have to manually append the module class to the end of the file.
Complete Configuration Example
protected function configure($rule): void
{
$rule->page('mymodule')
->title('My Module')
->access('authorized')
->permission(['access' => 'Access My Module', 'edit' => 'Edit My Module'])
->menu('Dashboard', '', 'bi bi-speedometer2', 10)
->menu('Settings', 'action=settings', 'bi bi-gear-fill', 20)
->menu('Reports', 'action=reports', 'bi bi-file-earmark-bar-graph', 30)
->setJs('/Assets/mymodule.js')
->setCss('/Assets/mymodule.css')
->headerTitle('My Module Dashboard')
->headerDescription('Comprehensive module management system')
->addHeaderLink('Home', '?page=mymodule', 'bi bi-house')
->addHeaderLink('Analytics', '?page=mymodule&action=analytics', 'bi bi-graph-up')
->headerStyle('pills')
->headerPosition('top-left')
->addModels([
'Reports' => ReportsModel::class,
'Categories' => CategoriesModel::class
])
->disableCli(false)
->version(250901);
}
Loading JavaScript and CSS Files
The setJs() and setCss() methods automatically load JavaScript and CSS files when the module page is accessed. They support multiple path formats:
Relative Paths (from module folder)
// These paths are relative to your module folder
$rule->setJs('/Assets/script.js') // Loads: Modules/YourModule/Assets/script.js
->setCss('./Assets/style.css') // Loads: Modules/YourModule/Assets/style.css
->setJs('Assets/main.js'); // Loads: Modules/YourModule/Assets/main.js
Absolute Paths (from Modules directory)
// These paths start from the Modules directory
$rule->setJs('Modules/Auth/Assets/auth.js') // Loads from Auth module
->setCss('Modules/FileManager/Assets/style.css'); // Loads from FileManager module
Multiple Files
$rule->setJs('/Assets/jquery.min.js')
->setJs('/Assets/main.js')
->setCss('/Assets/bootstrap.css')
->setCss('/Assets/custom.css');
Note: Files are loaded automatically when the module page is accessed. You don't need to manually load them in init() or bootstrap().
Configuring the Page Header
You can customize the page header with title, description, and navigation links:
Header Title and Description
$rule->headerTitle('Posts Management')
->headerDescription('Create, edit, and manage all your blog posts');
Header Navigation Links
Add navigation links to the header using the addHeaderLink() method (similar to menu()):
$rule->addHeaderLink('All Posts', '?page=posts', 'bi bi-list')
->addHeaderLink('Add New', '?page=posts&action=new', 'bi bi-plus-circle')
->addHeaderLink('Categories', '?page=posts&action=categories', 'bi bi-tags')
->addHeaderLink('Settings', '?page=posts&action=settings', 'bi bi-gear');
Parameters for addHeaderLink():
title(string, required): Display text for the linkurl(string, optional): Link URL (default: empty string)icon(string, optional): Bootstrap icon class (default: empty string)
Customizing Style and Position
Control how the links are displayed and positioned:
$rule->headerStyle('tabs') // Style: pills, tabs, underline, default
->headerPosition('top-right'); // Position: top-left, top-right
Complete Header Example
protected function configure($rule): void
{
$rule->page('blog')
->title('Blog')
->headerTitle('Blog Management System')
->headerDescription('Create and manage your blog content')
->addHeaderLink('Dashboard', '?page=blog', 'bi bi-speedometer2')
->addHeaderLink('New Post', '?page=blog&action=new', 'bi bi-plus-lg')
->addHeaderLink('Categories', '?page=blog&action=categories', 'bi bi-tags')
->addHeaderLink('Comments', '?page=blog&action=comments', 'bi bi-chat-dots')
->headerStyle('pills')
->headerPosition('top-left');
}
Method 2: Using Protected Properties (Legacy)
The traditional way to configure a module is using protected properties. This method is still fully supported for backward compatibility.
namespace Modules\Posts;
use App\Abstracts\AbstractModule;
class PostsModule extends AbstractModule
{
protected $page = 'posts';
protected $title = 'Posts';
protected $access = 'authorized';
protected $permission = ['access' => 'Access Posts Module'];
protected $menu_links = [
['url' => '', 'name' => 'Posts', 'icon' => 'bi bi-file-earmark-post-fill', 'order' => 10],
['url' => 'action=categories', 'name' => 'Categories', 'icon' => 'bi bi-tags-fill', 'order' => 20]
];
protected $version = 250901;
protected ?array $additional_models = ['Reports' => ReportsModel::class];
public function bootstrap()
{
// Bootstrap code here
}
}
Access Control
The access property/method defines who can access the module. Possible values:
public: Anyone can access, including guestsregistered: Only logged-in users can access (default)authorized: Only users with specific permissions can access (requirespermissionconfiguration)admin: Only administrators can access
Permissions for Authorized Access
When using access('authorized'), you must define permissions:
$rule->access('authorized')
->permission(['access' => 'Access Posts', 'edit' => 'Edit Posts']);
These permissions can be managed from the Users module.
You can also share a single permission across multiple modules by using the group.permission format as the array key. If the key contains exactly one dot, the part before the dot is used as the permission group and the part after the dot is the permission key. This makes the Users module show a single permission entry that grants access to all modules using that same group and key.
$rule->access('authorized')
->permission(['school.access' => 'Access School']);
Menu Links
The menu() method adds links to the sidebar navigation. Parameters:
name: Display name of the linkurl: URL relative to the module page (e.g.,'action=settings'becomes?page=mymodule&action=settings)icon: Bootstrap icon class (e.g.,'bi bi-gear-fill')order: Display order (lower numbers appear first, default: 10)
Menu Examples
// Single menu item
$rule->menu('Dashboard', '', 'bi bi-speedometer2', 10);
// Multiple menu items
$rule->menu('Posts', '', 'bi bi-file-post', 10)
->menu('Categories', 'action=categories', 'bi bi-tags', 20)
->menu('Settings', 'action=settings', 'bi bi-gear', 30);
// Or set all at once with menuLinks()
$rule->menuLinks([
['name' => 'Dashboard', 'url' => '', 'icon' => 'bi bi-speedometer2', 'order' => 10],
['name' => 'Settings', 'url' => 'action=settings', 'icon' => 'bi bi-gear', 'order' => 20]
]);
Lifecycle Methods
bootstrap()
The bootstrap() method is called when the module is being used (in pages, shell, APIs, or hooks). Use it to initialize models, routers, and other resources. It's called only once per request.
public function bootstrap()
{
// Load JavaScript/CSS
Theme::set('javascript', Route::url().'/Modules/Posts/Assets/posts.js');
Theme::set('styles', Route::url().'/Modules/Posts/Assets/posts.css');
// Initialize custom resources
$this->loadTranslations();
}
init()
The init() method is called only when the module page is accessed (?page=module-name). This is where you load page-specific assets.
public function init()
{
Theme::set('javascript', Route::url().'/Modules/Posts/Assets/page.js');
}
Context-Specific Init Methods
These methods are called in specific contexts:
hookInit(): Called during the 'init' hook phasecliInit(): Called when CLI commands are runapiInit(): Called when API requests are handledjobsInit(): Called during cron job initializationjobsStart(): Called when background jobs start
Multiple Tables Management
For modules that need multiple tables, use the addModels() method:
class MyModuleModule extends AbstractModule
{
protected function configure($rule): void
{
$rule->page('mymodule')
->title('My Module')
->addModels([
'Reports' => ReportsModel::class,
'Categories' => CategoriesModel::class
]);
}
}
Access additional models using getAdditionalModels():
// Get all additional models
$models = $this->getAdditionalModels();
// Get a specific model by name
$reportsModel = $this->getAdditionalModels('Reports');
Module Installation
Modules are installed by placing them in the /Modules/ folder. If the module has a model, it can use automatic CLI commands for installation:
php milkadmin/cli.php {module-name}:install
php milkadmin/cli.php {module-name}:update
php milkadmin/cli.php {module-name}:uninstall
If the model properly implements buildTable(), no additional code is needed. Otherwise, you can override these methods:
installExecute()
Executes module installation. Override to customize installation process:
public function installExecute(): void
{
// Create tables
$this->model->buildTable();
// Insert default data
$this->model->store(['title' => 'Default Post']);
// Save configuration
Config::set('posts_per_page', 10);
}
installUpdate($html)
Called after module update. Returns HTML for the update completion page:
public function installUpdate($html): string
{
// Update tables
$this->model->buildTable();
// Perform data migration if needed
// ...
$html .= 'Module updated successfully!';
return $html;
}
uninstallModule()
Called during module uninstallation. The module folder is not deleted but disabled (renamed with a dot prefix):
public function uninstallModule()
{
// Drop additional tables first
$models = $this->getAdditionalModels();
foreach ($models as $model) {
$model->dropTable();
}
// Delete configuration
Config::delete('posts_per_page');
// Call parent to drop main table
parent::uninstallModule();
}
Installation Form Validation
To add custom fields to the installation form and validate them:
public function installCheckData($errors, array $data = []): array
{
if (empty($data['api_key'])) {
$errors['api_key'] = 'API Key is required';
}
if (strlen($data['api_key']) < 20) {
$errors['api_key'] = 'API Key must be at least 20 characters';
}
return $errors;
}
Disabling CLI Commands
To prevent a module from having automatic CLI installation commands:
protected function configure($rule): void
{
$rule->page('system')
->title('System Module')
->disableCli(true); // No CLI commands
}
CLI Commands (Shell)
Use PHP attributes to define CLI commands:
use App\Attributes\Shell;
use App\Cli;
#[Shell('test')]
public function testCommand()
{
Cli::success("Command executed successfully!");
}
#[Shell('import')]
public function importCommand($file)
{
Cli::info("Importing file: $file");
// Import logic here
}
Execute from command line:
php milkadmin/cli.php posts:test
php milkadmin/cli.php posts:import data.csv
Version Management
The version property uses the format YYMMDD (year, month, day):
$rule->version(250901); // Version for September 1, 2025
$rule->version(250902); // Version for September 2, 2025