Create a Module
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:
Control::frontend()detects themodroute.Mod.control.phpextracts the module ID from the URL (e.g.myPage).- It checks
Mod::$listMenu— populated byMod::addMenuList()at system init — to confirm the module is valid. If not found, a 404 is returned. - The
modtemplate is rendered, which callsHooks::run('mod_control', $data). - 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
Mod::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.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
- Place your module folder in
inc/mod/my-module/. - Go to Admin > Modules.
- Find your module and click Install.
- 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.
- Create a migration file in
inc/migrations/. - Use
Db::query()within the migration'sup()method to create your tables. - Users will run
php genix migrateto 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:
- Create
inc/lib/Control/Api/MyResourceApi.class.php. - 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;
}
http:// for external assets. The core security policy blocks insecure sources by default to prevent mixed content vulnerabilities.See Also
HooksClass — All available hooks.ModClass — Module utilities.MailClass — Send emails from modules.OptionsClass — Store and retrieve module settings.TokenClass — CSRF protection for module forms.- Control Layer Reference — How module routes are dispatched.
ModelClass — Base ORM class.