Milk Admin

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 link
  • url (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 guests
  • registered: Only logged-in users can access (default)
  • authorized: Only users with specific permissions can access (requires permission configuration)
  • 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 link
  • url: 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 phase
  • cliInit(): Called when CLI commands are run
  • apiInit(): Called when API requests are handled
  • jobsInit(): Called during cron job initialization
  • jobsStart(): 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
Loading...