GeniXCMS

Create a Module

categoryHow To edit_calendar31 Mar 2026

How to Create a Module

This guide walks you through building a GeniXCMS module, either automatically using the CLI or manually from scratch.


⚡ Quick Start: Using the CLI

The fastest way to scaffold a new module is by using the built-in genix console. This command automatically generates the required directory structure, the index.php entry point with metadata, and a base library class.

Generate Scaffolding

In your project root, run:

php genix make:module my-new-plugin

Activate the Module

Once generated, you can instantly activate it via the CLI:

php genix module:activate my-new-plugin

Module Structure

A module lives in inc/mod/{module-slug}/ and must contain at minimum:

inc/mod/my-module/
├── index.php           ← Required: Module entry point + metadata
├── options.php         ← Optional: Admin settings page
├── inc/
│   └── MyModule.lib.php  ← Main module class
└── layout/
    └── myview.php      ← Frontend template (Latte)

Step 1: Create index.php (Entry Point)

The comment block at the top is parsed by the CMS to display the module in the admin panel.

<?php
/**
 * Name: My Module
 * Desc: A brief description of what this module does.
 * Version: 1.0.0
 * Build: 1.0.0
 * Developer: Your Name
 * URI: https://yoursite.com
 * License: MIT License
 * Icon: fa fa-cogs
 */

// Register a PSR-style autoloader for the module's own classes
function loadMyModuleLib($class_name)
{
    Mod::inc($class_name . ".lib", '', dirname(__FILE__) . "/inc/");
}
spl_autoload_register('loadMyModuleLib');

// Instantiate the main module class
new MyModule();

Step 2: Create the Module Class (inc/MyModule.lib.php)

The module class constructor hooks into the CMS. The mod_control hook is the key action point for frontend pages.

<?php
class MyModule
{
    public function __construct()
    {
        // Attach to the frontend page hook
        Hooks::attach('mod_control', ['MyModule', 'myPage']);

        // REQUIRED: Register this module's hook ID into Mod::$listMenu.
        // Without this, the URL router will return a 404 for /mod/myPage.html.
        Mod::addMenuList(['myPage' => 'My Module Page']);

        // Optional: Attach to other hooks
        Hooks::attach('footer_load_lib', ['MyModule', 'loadAssets']);
        Hooks::attach('admin_page_dashboard_action', ['MyModule', 'dashboardWidget']);
    }

    /**
     * Frontend page handler.
     * NOTE: Inside hook callbacks, $data is wrapped by Hooks::run() as [$original_data].
     * Use $data[0] to access the actual data array.
     */
    public static function myPage($data)
    {
        // Only execute when this module's URL is active
        if ($data[0]['mod'] !== 'myPage') {
            return;
        }

        $page_data = $data[0]; // Start with the original data

        // Handle form submission
        if (isset($_POST['submit'])) {
            $token = Typo::cleanX($_POST['token']);
            if (!Token::validate($token)) {
                $page_data['alertDanger'][] = _('Invalid security token.');
            } else {
                $message = Typo::cleanX($_POST['message']);
                // Process the form...
                $page_data['alertSuccess'][] = _('Submitted successfully!');
            }
        }

        // REQUIRED: Always return the result of Mod::inc().
        // Hooks::run() collects the return value. If you don't return it,
        // the content will be silently discarded and nothing will render.
        return Mod::inc('myview', $page_data, realpath(__DIR__ . '/../layout/'));
    }

    public static function loadAssets()
    {
        // Recommended: Use the Asset class for dependency & context management
        Asset::register('my-mod-js', 'js', Site::$url . '/inc/mod/my-module/assets/app.js', 'footer', ['jquery'], 20, 'frontend');
        Asset::enqueue('my-mod-js');
    }

    public static function dashboardWidget($data)
    {
        // Output a widget on the admin dashboard
        echo '<div class="col-md-4"><div class="info-box">My Module Stats</div></div>';
    }
}

Step 3: Create a Frontend View (layout/myview.php)

Layout files use the Latte template engine.

<section class="container my-5">
    <h1>My Module Page</h1>

    {* Display alerts *}
    {if isset($alertSuccess)}
        <div class="alert alert-success">{$alertSuccess[0]}</div>
    {/if}

    {* Main form *}
    <form method="POST" action="">
        <input type="hidden" name="token" value="{$token}">

        <div class="form-group">
            <label>Your Message</label>
            <textarea name="message" class="form-control" required></textarea>
        </div>

        <button type="submit" name="submit" class="btn btn-primary">Submit</button>
    </form>
</section>

Step 4: Create options.php (Admin Settings)

The options.php file is shown in Admin > Modules > [Your Module] > Settings.

<?php
// Save options
if (isset($_POST['mymodule_save'])) {
    $data = [
        'notify_email' => Typo::cleanX($_POST['notify_email']),
    ];
    Options::update('mymodule_options', json_encode($data));
}

$opt = json_decode(Options::get('mymodule_options'), true) ?? [];
?>
<section class="content">
    <h1><i class="fa fa-cogs"></i> My Module Settings</h1>
    <div class="box">
        <div class="box-body">
            <form method="POST">
                <div class="form-group">
                    <label>Notification Email</label>
                    <input type="email" name="notify_email" class="form-control"
                           value="<?= $opt['notify_email'] ?? '' ?>">
                </div>
                <button type="submit" name="mymodule_save" class="btn btn-primary">Save</button>
            </form>
        </div>
    </div>
</section>

How Modules Are Loaded

When a user visits /mod/myPage.html (with Smart URL) or ?mod=myPage:

  1. Control::frontend() detects the mod route.
  2. Mod.control.php extracts the module ID from the URL (e.g. myPage).
  3. It checks Mod::$listMenu — populated by Mod::addMenuList() at system init — to confirm the module is valid. If not found, a 404 is returned.
  4. The mod template is rendered, which calls Hooks::run('mod_control', $data).
  5. Your module's callback executes and returns the rendered HTML.
/mod/myPage.html
  → Mod.control.php
  → array_key_exists('myPage', Mod::$listMenu) → valid!
  → render mod template
  → Hooks::run('mod_control', $data)
  → MyModule::myPage($data)          ← $data is [$original_data]
  → return Mod::inc('myview', ...)   ← MUST return!
  → content displayed in template
priority_high
ImportantMod::addMenuList() is mandatory for any module that exposes a public frontend URL. The router checks this list to validate requests — a missing entry means a 404 for all visitors.
warning
WarningAlways return the result of Mod::inc() from your hook callback. Hooks::run() collects return values to build the final output. If you omit return, the content is silently discarded and the page body appears blank.

Useful Mod Class Methods

// Include a view file from a custom path
Mod::inc('view-name', $data, '/path/to/layout/');

// Register module page in menu manager
Mod::addMenuList(['hookName' => 'Display Label']);

Available Hooks for Modules

Hook Type Description
mod_control Action Frontend page execution point
admin_page_dashboard_action Action Add widget to admin dashboard
admin_page_top_action Action Output at top of admin page
admin_page_bottom_action Action Output at bottom of admin page
footer_load_lib Action Add JS/CSS to page footer
header_load_meta Action Add tags to HTML <head>
post_content_filter Filter Modify post content before display
post_submit_add_action Action Fires after a new post is saved
user_login_action Action Fires after successful user login

Complete Example: Contact Form Module

The built-in Contact Form module demonstrates this full pattern:

inc/mod/contact-form/
├── index.php               — Registers autoloader, instantiates Contact class
├── options.php             — Admin instructions panel
├── inc/
│   └── Contact.lib.php     — Main class: hooks into mod_control,
│                             handles POST, sends email via Mail::send()
└── layout/
    └── contactpage.php     — Latte form template

Key integration points from Contact.lib.php:

// In constructor — BOTH are required:
Hooks::attach('mod_control', ['Contact', 'contactPage']);
Mod::addMenuList(['contactPage' => 'Contact Us']); // ← enables routing

// In contactPage() — note $data[0] and the return statement:
public static function contactPage($data)
{
    if ($data[0]['mod'] == 'contactPage') {
        // Handle POST, validate, call Mail::send()
        // ...
        // MUST use return so Hooks::run() captures the output:
        return Mod::inc('contactpage', $data[0], realpath(__DIR__.'/../layout/'));
    }
}

Installing the Module

  1. Place your module folder in inc/mod/my-module/.
  2. Go to Admin > Modules.
  3. Find your module and click Install.
  4. To add a frontend page link, go to Admin > Menus and create a new menu item selecting Mod type with your module's page name.

Modern Module Features (since 2.0.0)

1. Database Migrations

Don't use Db::query() in your constructor to create tables. Use the Migration System.

  1. Create a migration file in inc/migrations/.
  2. Use Db::query() within the migration's up() method to create your tables.
  3. Users will run php genix migrate to setup your module's database.

2. Access Control List (ACL)

Register custom permissions for your module features in your constructor:

Acl::register('MOD_MYMODULE_CAN_EXPORT', 'Allow users to export my module data');

Then check it before any sensitive action:

if (Acl::check('MOD_MYMODULE_CAN_EXPORT')) {
    // Perform export...
}

3. API Resources

Expose your module data over the RESTful API:

  1. Create inc/lib/Control/Api/MyResourceApi.class.php.
  2. Your resource will be available at /api/v1/myresource/.

4. Custom Editor Support

If your module provides a new editing experience, register it using the Editor Class:

Editor::register('my_editor', 'My Cool Editor', [self::class, 'initEditor']);

Module Security (CSP & Assets)

If your module injects scripts from external CDNs (like Google Maps or Chart.js), you must register these domains in the Content Security Policy whitelist.

Registering Trusted Origins

Use the system_security_headers_args hook in your module's constructor:

// In inc/mod/my-module/inc/MyModule.lib.php
public function __construct() {
    Hooks::attach('system_security_headers_args', [$this, 'extendCSP']);
}

/**
 * Add trusted origins to the global CSP.
 */
public function extendCSP($rules) {
    if (!in_array("https://maps.googleapis.com", $rules['script-src'])) {
        $rules['script-src'][] = "https://maps.googleapis.com";
    }
    return $rules;
}
warning
CautionAvoid using http:// for external assets. The core security policy blocks insecure sources by default to prevent mixed content vulnerabilities.

See Also