Module Development Guide¶
This guide walks you through building a Gambio GX module from scratch using the Store Module Skeleton.
Prerequisites¶
- A Gambio shop installation (local or test system)
- PHP >= 8.0
- Basic knowledge of PHP, HTML, CSS, and JavaScript
- A GitHub account (required for Store publishing)
How Gambio Modules Work¶
Every Gambio module lives inside src/GXModules/{Vendor}/{ModuleName}/. The {Vendor} folder is your company or developer name, and {ModuleName} is the name of your module.
The only required file is GXModule.json. This file registers your module in the Module Center, where shop administrators can install and uninstall it. Everything else is optional and depends on what your module needs to do.
Step 1: Create the Module Directory¶
src/GXModules/AcmeCorp/MyModule/
Replace AcmeCorp with your vendor name and MyModule with your module name.
Step 2: Create GXModule.json¶
This is the only mandatory file. At minimum:
{
"title": "my_module.PAGE_TITLE",
"description": "my_module.DESCRIPTION"
}
This registers your module in the Module Center. The values reference translation keys from your TextPhrases files.
Adding Configuration Fields¶
To give administrators a settings page, add the configuration array:
{
"title": "my_module.PAGE_TITLE",
"description": "my_module.DESCRIPTION",
"configuration": [
{
"title": "my_module.SECTION_SETTINGS",
"fields": {
"enableFeature": {
"type": "checkbox",
"label": "my_module.LABEL_ENABLE"
},
"apiKey": {
"type": "text",
"label": "my_module.LABEL_API_KEY",
"required": true
}
}
}
]
}
Gambio automatically generates the configuration page from this JSON. No HTML templates or controllers needed.
For the complete field type reference, see GXModule.json Reference.
Adding Lifecycle Hooks (Optional)¶
Note
Lifecycle hooks are completely optional. If your module only needs a simple configuration (checkboxes, text fields, selects, etc.), you do not need any PHP action files. Gambio handles storing and reading configuration values automatically. You only need hooks if your module requires custom logic during installation, uninstallation, or when saving settings (e.g. creating database tables, clearing caches, or validating input).
You can run custom PHP code when the module is installed, uninstalled, or when settings are saved:
{
"install": {
"controller": "GXModules\\AcmeCorp\\MyModule\\Admin\\Actions\\InstallAction",
"method": "onInstall"
},
"uninstall": {
"controller": "GXModules\\AcmeCorp\\MyModule\\Admin\\Actions\\InstallAction",
"method": "onUninstall"
},
"save": {
"controller": "GXModules\\AcmeCorp\\MyModule\\Admin\\Actions\\SaveAction",
"method": "onSave"
}
}
When resolved via DI container (class registered in ServiceProvider), the install/uninstall method receives only the parsed GXModule.json data:
public function onInstall(array $gxModulesJsonData): void
When resolved via MainFactory fallback, it receives:
public function onInstall($db, array $moduleData, $languageTextManager, $cacheControl): void
$db: CI_DB_query_builder database instance$moduleData: Parsed GXModule.json content as array$languageTextManager: LanguageTextManager for translations$cacheControl: DataCache for cache clearing
The save method receives ($db, $configurationStorage, $languageTextManager, $cacheControl). Use it for cache invalidation or validation after config changes.
Example: Creating Database Tables in Lifecycle Hooks¶
Note
Gambio does not have an automatic migration system with up/down. If your module needs its own database tables, you must create and drop them yourself inside the install and uninstall hooks.
Here is a complete example of an install/uninstall action class (MainFactory variant):
<?php
// Admin/Actions/InstallAction.php
namespace GXModules\AcmeCorp\MyModule\Admin\Actions;
class InstallAction
{
public function onInstall($db, array $moduleData, $languageTextManager, $cacheControl): void
{
// Create a custom table for the module
$db->query("
CREATE TABLE IF NOT EXISTS my_module_data (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
external_ref VARCHAR(255) NOT NULL DEFAULT '',
sync_status ENUM('pending', 'synced', 'error') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_id (order_id),
INDEX idx_sync_status (sync_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
// Optionally seed default data
$db->query("
INSERT IGNORE INTO my_module_data (order_id, external_ref, sync_status)
VALUES (0, 'initial', 'synced')
");
}
public function onUninstall($db, array $moduleData, $languageTextManager, $cacheControl): void
{
// Clean up: drop the custom table
$db->query("DROP TABLE IF EXISTS my_module_data");
}
}
If your class is resolved via the DI container (registered in ServiceProvider), inject Doctrine\DBAL\Connection instead:
<?php
// Admin/Actions/InstallAction.php
namespace GXModules\AcmeCorp\MyModule\Admin\Actions;
use Doctrine\DBAL\Connection;
class InstallAction
{
private Connection $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function onInstall(array $gxModulesJsonData): void
{
$this->connection->executeStatement("
CREATE TABLE IF NOT EXISTS my_module_data (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
external_ref VARCHAR(255) NOT NULL DEFAULT '',
sync_status ENUM('pending', 'synced', 'error') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_id (order_id),
INDEX idx_sync_status (sync_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
}
public function onUninstall(array $gxModulesJsonData): void
{
$this->connection->executeStatement("DROP TABLE IF EXISTS my_module_data");
}
}
Tip
Always use CREATE TABLE IF NOT EXISTS and DROP TABLE IF EXISTS to make your install/uninstall idempotent. This prevents errors if the module is reinstalled or the hook runs multiple times.
Step 3: Add Translations¶
Create language files so your module labels are translatable:
Admin/TextPhrases/german/my_module.lang.inc.php
Admin/TextPhrases/english/my_module.lang.inc.php
Each file returns a key-value array:
<?php
$t_language_text_section_content_array = [
'PAGE_TITLE' => 'My Module',
'DESCRIPTION' => 'This module does something useful.',
'SECTION_SETTINGS' => 'Settings',
'LABEL_ENABLE' => 'Enable feature',
'LABEL_API_KEY' => 'API Key',
];
The section name comes from the filename (without .lang.inc.php). Reference keys in GXModule.json as {section}.{KEY}, e.g. my_module.PAGE_TITLE.
Shop Text Phrases¶
If your module needs translations on the storefront (not just in the admin), place language files in Shop/TextPhrases/:
Shop/TextPhrases/German/my_module.lang.inc.php
Shop/TextPhrases/English/my_module.lang.inc.php
Note that language folder names are capitalized (German, English).
In Smarty templates, load them with: {load_language_text section="my_module"}, then use {$txt.KEY}.
Step 4: Add Storefront Customizations (Optional)¶
CSS / SCSS¶
Create a main.scss file in Shop/Themes/All/Css/ to add styles across all themes:
Shop/Themes/All/Css/main.scss
This file is automatically included when the theme styles are built. You can add your styles directly or import additional SCSS files:
// main.scss
@import 'components/buttons';
@import 'components/badges';
.my-module-widget {
border: 1px solid #ccc;
padding: 1rem;
}
To target a specific theme, replace All with the theme name:
Shop/Themes/Malibu/Css/main.scss
JavaScript¶
Place JavaScript files in Shop/Themes/All/Javascript/{page}/ where {page} matches the storefront page:
Shop/Themes/All/Javascript/product_info/my_module.js : Product detail page
Shop/Themes/All/Javascript/product_listing/my_module.js : Category listing
Shop/Themes/All/Javascript/shopping_cart/my_module.js : Shopping cart
Shop/Themes/All/Javascript/index/my_module.js : Homepage
Smarty Template Overrides¶
Override Smarty template snippets by mirroring the theme directory structure:
Shop/Themes/All/snippets/footer/footer.html
This replaces the default footer template across all themes.
Step 5: Add an Admin Menu Entry (Optional)¶
Create Admin/Menu/my_module.menu.json to add an entry to the admin sidebar:
[{
"id": "BOX_HEADING_MY_MODULE",
"sort": 400,
"class": "fa fa-puzzle-piece",
"title": "my_module.PAGE_TITLE",
"type": "standalone",
"items": [{
"sort": 10,
"link": "admin/my-module",
"title": "my_module.PAGE_TITLE"
}]
}]
The link value is relative to the shop root URL (without the domain). Use modern route paths (e.g. admin/my-module) that match your routes.php definitions.
This replaces the deprecated XML menu format (menu_*.xml). After adding or changing menu entries, clear the module cache in the Gambio Admin (Toolbox > Caches).
Step 6: Extend Existing Functionality with Overloads (Optional)¶
Overloads let you extend any class managed by Gambio's MainFactory.
Admin Overloads¶
Place files in Admin/Overloads/{ClassName}/:
// Admin/Overloads/OrderExtenderComponent/MyModuleOrderExtender.inc.php
class MyModuleOrderExtender extends MyModuleOrderExtender_parent
{
public function proceed()
{
parent::proceed();
// Your custom logic here
}
}
Common admin overload targets:
- OrderExtenderComponent: Order detail page
- AdminApplicationTopExtenderComponent: Every admin page (early)
- AdminEditProductExtenderComponent: Product editing page
- PDFOrderExtenderComponent: PDF invoice generation
Shop Overloads¶
Place files in Shop/Overloads/{ClassName}/:
// Shop/Overloads/ApplicationTopExtenderComponent/MyModuleAppTop.inc.php
class MyModuleAppTop extends MyModuleAppTop_parent
{
public function proceed()
{
parent::proceed();
// Runs on every storefront page load
}
}
Rules:
1. Your class must extend {ClassName}_parent (a pseudo-class resolved by MainFactory)
2. Always call the parent method to preserve the overload chain
3. The file must use the .inc.php extension
Step 7: Add a ServiceProvider for Dependency Injection (Optional)¶
Create MyModuleServiceProvider.php at the module root to register services in the DI container:
<?php
namespace GXModules\AcmeCorp\MyModule;
use Gambio\Core\Application\DependencyInjection\AbstractModuleBootableServiceProvider;
class MyModuleServiceProvider extends AbstractModuleBootableServiceProvider
{
public function provides(): array
{
return [MyService::class];
}
public function register(): void
{
$this->application->registerShared(MyService::class, MyServiceImpl::class)
->addArgument(\Gambio\Core\Configuration\ConfigurationService::class);
}
public function boot(): void
{
// Register event listeners using inflections
$this->application->inflect(\Gambio\Core\Event\EventListenerProvider::class)
->invokeMethod('attachListener', [SomeEvent::class, MyEventListener::class]);
}
}
Use AbstractModuleBootableServiceProvider when you need boot() (for event listeners). Use AbstractModuleServiceProvider if you only need register().
Step 8: Add a Module Class for Events and Middleware (Optional)¶
Create MyModuleModule.php at the module root. It is auto-detected when named *Module.php:
<?php
namespace GXModules\AcmeCorp\MyModule;
use Gambio\Core\Application\Modules\AbstractModule;
class MyModuleModule extends AbstractModule
{
public function eventListeners(): ?array
{
return [
SomeEvent::class => [MyListener::class],
];
}
public function shopMiddleware(): ?array
{
return [MyShopMiddleware::class];
}
public function adminMiddleware(): ?array
{
return [];
}
public function apiMiddleware(): ?array
{
return [];
}
public function dependsOn(): ?array
{
return [];
}
}
Step 9: Add HTTP Routes (Optional)¶
Create routes.php at the module root for custom HTTP endpoints:
<?php
use Gambio\Core\Application\Routing\RouteCollector;
return static function (RouteCollector $routeCollector) {
$routeCollector->get('/admin/my-module', MyOverviewAction::class);
$routeCollector->group('/admin/api/my-module', function (RouteCollector $group) {
$group->get('', FetchAllAction::class);
$group->post('', CreateAction::class);
$group->put('/{id:\d+}', UpdateAction::class);
$group->delete('/{id:\d+}', DeleteAction::class);
});
};
Handler classes must implement PSR-15 RequestHandlerInterface and should be registered in the ServiceProvider.
Step 10: Add a Cronjob (Optional)¶
Register a scheduled task with 4 files in Admin/CronjobConfiguration/:
MyCronjob.json: Configuration:
{
"name": "MyCronjob",
"title": "my_cronjob.TITLE",
"configuration": {
"active": {
"name": "active",
"type": "checkbox",
"label": "my_cronjob.LABEL_ACTIVE",
"defaultValue": false
},
"interval": {
"name": "interval",
"type": "select",
"label": "my_cronjob.LABEL_INTERVAL",
"defaultValue": "0 * * * *",
"values": [
{ "value": "*/5 * * * *", "text": "my_cronjob.EVERY_5_MINUTES" },
{ "value": "0 * * * *", "text": "my_cronjob.EVERY_HOUR" },
{ "value": "0 0 * * *", "text": "my_cronjob.EVERY_DAY" }
]
}
}
}
MyCronjobTask.inc.php: Execution logic:
class MyCronjobTask extends AbstractCronjobTask
{
public function run(array $cronjobStartArguments): void
{
$this->logger->log('Starting sync...');
// Your scheduled task logic
$this->logger->log('Sync complete.');
}
}
MyCronjobDependencies.inc.php: Dependencies:
class MyCronjobDependencies extends AbstractCronjobDependencies
{
// Add getter methods for services the task needs
}
MyCronjobLogger.inc.php: Logger:
class MyCronjobLogger extends AbstractCronjobLogger
{
// Default implementation is usually sufficient
}
Add translations in Admin/TextPhrases/{lang}/my_cronjob.lang.inc.php.
Step 11: Add index.html Files¶
Place an empty <html></html> file named index.html in every directory of your module. This is a Gambio convention to prevent directory listing on web servers:
<html></html>
Minimal vs. Full Module¶
Not every module needs all extension points. Here are some examples:
CSS-only module (just style changes):
GXModules/AcmeCorp/PinkButtons/
GXModule.json
Shop/Themes/All/Css/pink_buttons.css
JavaScript enhancement (no PHP needed):
GXModules/AcmeCorp/ProductEnhancer/
GXModule.json
Shop/Themes/All/Javascript/product_info/enhancer.js
Full-featured module (all extension points):
GXModules/AcmeCorp/MyModule/
GXModule.json
MyModuleServiceProvider.php
MyModuleModule.php
routes.php
Admin/Actions/...
Admin/CronjobConfiguration/...
Admin/Menu/my_module.menu.json
Admin/Overloads/...
Admin/TextPhrases/...
Shop/Overloads/...
Shop/Themes/All/...
Next Steps¶
- GXModule.json Reference: Complete field type documentation
- Local Testing: How to test your module
- store.json Reference: Store metadata format
- Publishing Guide: How to submit to the Gambio Store