# Sample EasyDCIM Extension
- Information
- Download
- Installation
- Activation
- File Structure
- Module Class
- Service Provider
- Event Subscriber
- Adding a Device Tab
- Adding Widgets
- Adding a Custom Grid Column
- Model Observer
- Settings Page
- API v3 Example
- Module Migrations
- Custom Model Table
- Artisan Commands and Scheduler
- Email Notification Example
- Backend Assets
- Routing
- Recommended Development Pattern
This article describes the official Sample EasyDCIM Extension, also known as CustomModule. It is a reference addon for developers who want to create their own EasyDCIM extensions.
The current sample targets EasyDCIM 1.25.1 and newer. It uses Laravel 12 patterns, API v3 with Sanctum tokens, service providers, event subscribers, Eloquent observers, module migrations, widgets, commands, and backend forms.
# Information
The Sample Extension helps developers learn the expected addon structure in the EasyDCIM control panel. It demonstrates how to:
- register an addon in EasyDCIM,
- add backend routes and pages,
- register API v3 routes,
- build a settings page with the EasyDCIM form builder,
- listen to EasyDCIM events,
- add device tabs and dashboard widgets,
- add a custom column to an existing grid,
- react to Eloquent model events,
- run module migrations,
- register Artisan commands and scheduled tasks,
- send an email notification with EasyDCIM mail templates.
# Download
The sample extension may be downloaded from this repository:
Sample Extension (opens new window)
Repository URL:
https://repository.easydcim.com/easydcim/custom_module.git
# Installation
Unzip or clone the extension into:
/opt/easydcim/modules/addons/CustomModule
# Activation
To activate the extension, open Modules → My Modules in the EasyDCIM admin area and activate Custom Module.
During activation, the module runs its own migration files and seeds demo rows used by the custom model table example.
# File Structure
The sample extension is split into small files so each Laravel pattern is easy to find:
modules/addons/CustomModule/
├── Commands/
│ ├── CustomModuleCommand.php
│ └── IpmiBmcResetCommand.php
├── Controller/
│ ├── Api/v3/ExampleController.php
│ ├── CustomModelController.php
│ ├── DeviceController.php
│ ├── OutputController.php
│ ├── PageController.php
│ └── SettingsController.php
├── Mail/
│ └── DeviceDeletedMail.php
├── Model/
│ └── CustomModel.php
├── Observers/
│ └── DeviceObserver.php
├── Support/Installers/
│ └── CustomModelTableInstaller.php
├── Widgets/
│ ├── CustomModuleDashboardWidget.php
│ └── CustomModuleDeviceWidget.php
├── config/
├── database/migrations/
├── lang/
├── templates/
├── CustomModule.php
├── CustomModuleEventHandler.php
├── CustomModuleProvider.php
└── routes.php
# Module Class
The main module class is:
CustomModule.php
It defines module metadata such as name, slug, version, description, documentation link, and activation hooks.
The activation hook is a good place to run module migrations and seed default demo data:
public function onActivate()
{
Artisan::call('migrate', [
'--path' => 'modules/addons/CustomModule/database/migrations',
'--force' => true,
]);
app(CustomModelTableInstaller::class)->install();
}
Use onDeactivate() for cleanup actions only when it is safe to do so. Avoid deleting user data automatically unless the administrator clearly expects it.
# Service Provider
The service provider is:
CustomModuleProvider.php
The provider should stay small. It should only bootstrap services owned by the module, for example:
- translation namespace,
- Artisan commands,
- scheduled tasks,
- model observers.
Example:
public function register()
{
$this->app['translator']->addNamespace(
'custom-module',
base_path('modules/addons/CustomModule/lang')
);
$this->registerCommands();
parent::register('CustomModule');
}
Scheduled tasks are registered in boot():
public function boot(Schedule $schedule)
{
parent::boot($schedule);
$schedule->command('custom-module:ipmi-bmc-reset')->daily();
}
# Event Subscriber
Most event listeners are placed in:
CustomModuleEventHandler.php
EasyDCIM automatically subscribes a class named {ModuleName}EventHandler when it exists in the module namespace. This keeps the provider clean and makes event logic easier to maintain.
The sample event subscriber demonstrates:
- adding a widget to the device summary page,
- adding a custom tab to the device page,
- adding a dashboard widget,
- adding a column to a grid,
- adding a backend menu item,
- reacting to OS installation events,
- optional service/order lifecycle hooks.
Example event registration:
public function subscribe($events): void
{
$events->listen('backend.devices.summary', [$this, 'registerDeviceSummaryWidget']);
$events->listen('easydcim.server.tabs: render', [$this, 'registerDeviceTab']);
$events->listen('backend.dashboard', [$this, 'registerDashboardWidget']);
$events->listen('easydcim.grid: columns', [$this, 'registerGridColumns']);
}
# Adding a Device Tab
The sample extension adds a custom tab to the server/device page:
public function registerDeviceTab(TabsGenerator $tabGenerator): void
{
$tabGenerator->appendTab([
'url' => route('backend.custom.module.device.tab', ['id' => $tabGenerator->model->id]),
'title' => 'Custom Device Tab',
]);
}
The tab content is handled by DeviceController and rendered with:
templates/device/tab.twig
# Adding Widgets
The sample extension contains two widget examples:
Widgets/CustomModuleDeviceWidget.php
Widgets/CustomModuleDashboardWidget.php
Device summary widget registration:
public function registerDeviceSummaryWidget(SmartBoxGenerator $widgets): void
{
$widgets->registerBox(new CustomModuleDeviceWidget());
}
Dashboard widget registration:
public function registerDashboardWidget(SmartBoxGenerator $widgets): void
{
$widgets->registerBox(new CustomModuleDashboardWidget());
}
Widget templates are stored in:
templates/widgets/device/summary.twig
templates/widgets/dashboard/summary.twig
# Adding a Custom Grid Column
The sample extension shows how to inject a custom column into an existing EasyDCIM grid.
For the server grid, the actions column key is :actions. The sample inserts the custom column directly before the actions column so it appears before the summary, edit, and delete buttons.
public function registerGridColumns(Collection $columns, string $modelName, string $path): Collection
{
if ($modelName !== 'server') {
return $columns;
}
$columnKey = 'custom_module_value';
$columnDefinition = [
'label' => trans('custom-module::backend.custom_value'),
'sortable' => false,
'type' => 'Key',
'value' => static fn ($model) => sprintf('CustomModule #%s', $model->getKey()),
];
$actionsColumnKey = $columns->has(':actions') ? ':actions' : 'actions';
if (! $columns->has($actionsColumnKey)) {
$columns->put($columnKey, $columnDefinition);
return $columns;
}
$columns->forget($columnKey);
$orderedColumns = $columns->all();
$columns->forget($columns->keys()->all());
foreach ($orderedColumns as $key => $definition) {
if ($key === $actionsColumnKey) {
$columns->put($columnKey, $columnDefinition);
}
$columns->put($key, $definition);
}
return $columns;
}
# Model Observer
The sample uses an Eloquent observer instead of large inline callbacks in the provider:
Observers/DeviceObserver.php
Example:
class DeviceObserver
{
public function saved(Device $device): void
{
Log::info('[CustomModule] Device saved', [
'id' => $device->getKey(),
'label' => $device->label,
]);
}
public function deleted(Device $device): void
{
Log::info('[CustomModule] Device deleted', [
'id' => $device->getKey(),
'label' => $device->label,
]);
}
}
The observer is registered in CustomModuleProvider:
Device::observe(DeviceObserver::class);
Use observers for short synchronous actions. For longer work, such as calling external APIs, dispatch a queued job instead.
# Settings Page
The sample settings page is handled by:
Controller/SettingsController.php
templates/settings/form.twig
The form is built with the EasyDCIM backend form builder:
use Components\Libs\Form\Builder as FormBuilder;
$form = new FormBuilder('backend.custom.module.settings', null, 'settings', [
'data-edc-form' => 1,
'data-scope' => 'settings',
'data-ajaxRedir' => 0,
'url' => route('backend.custom.module.settings.save'),
]);
Settings are stored in the options table under namespaced keys, for example:
custom_module.notifications_enabled
custom_module.notification_email
custom_module.log_level
In EasyDCIM, the option repository is resolved through the container key:
$options = app('OptionRepository');
Avoid type-hinting Components\Libs\Options\OptionRepository directly in controller methods, because the repository is registered under the OptionRepository container key.
# API v3 Example
API v3 routes are defined in:
routes.php
The example controller is:
Controller/Api/v3/ExampleController.php
Routes are registered under:
/api/v3/admin/custom-module
Available sample endpoints:
GET /api/v3/admin/custom-module/status
GET /api/v3/admin/custom-module/stats
GET /api/v3/admin/custom-module/devices
GET /api/v3/admin/custom-module/devices/{id}
PATCH /api/v3/admin/custom-module/devices/{id}/metadata
GET /api/v3/admin/custom-module/settings
PUT /api/v3/admin/custom-module/settings
API v3 uses Sanctum bearer tokens. Include the token in every API request:
curl -H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
https://example.com/api/v3/admin/custom-module/status
A successful response uses the EasyDCIM API response helper:
return ApiResponse::success(data: [
'module' => 'CustomModule',
'api_version' => 'v3',
'auth' => 'sanctum',
'status' => 'active',
]);
Example metadata update request:
curl -X PATCH https://example.com/api/v3/admin/custom-module/devices/1/metadata \
-H "Authorization: Bearer <token>" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"Custom Module Note": "Created by API v3 example",
"Custom Module Enabled": "yes"
}
}'
# Module Migrations
The sample module contains a migration for its demo table:
database/migrations/2026_06_05_000001_create_custom_module_table.php
The migration creates:
custom_module_table
Run module migrations manually if needed:
cd /opt/easydcim
php artisan migrate --path=modules/addons/CustomModule/database/migrations --force
The sample installer seeds demo rows for the custom table:
Support/Installers/CustomModelTableInstaller.php
# Custom Model Table
The sample custom model table page is available in the backend:
/backend/custom-module/custom-model-table
It demonstrates:
- a custom Eloquent model,
- a module-owned database table,
GridTableGenerator,- custom filters,
- custom table columns.
The related files are:
Model/CustomModel.php
Controller/CustomModelController.php
config/customModel.php
templates/tab3/summary.twig
templates/tab3/filters.twig
# Artisan Commands and Scheduler
The sample extension contains two commands:
Commands/CustomModuleCommand.php
Commands/IpmiBmcResetCommand.php
Commands are registered in CustomModuleProvider:
$this->commands([
CustomModuleCommand::class,
IpmiBmcResetCommand::class,
]);
The scheduler example runs the IPMI command daily:
$schedule->command('custom-module:ipmi-bmc-reset')->daily();
You can test a command with:
cd /opt/easydcim
php artisan custom-module:ipmi-bmc-reset --help
# Email Notification Example
The sample mail helper is:
Mail/DeviceDeletedMail.php
It demonstrates sending an email through EasyDCIM's GeneralMail template:
Mail::to(app_email())->send(new GeneralMail([
'subject' => $subject,
'emailBody' => $body,
]));
Use app_email() for the main administrator email. It reads backend.settings.emailAddress and falls back to the first admin email.
# Backend Assets
The event handler contains optional examples for adding backend JavaScript and CSS assets:
// $events->listen('easydcim.assets: template.js', [$this, 'registerTemplateScripts']);
// $events->listen('easydcim.assets: template.css', [$this, 'registerTemplateStyles']);
Example asset paths:
templates/admin/default/assets/js/custom-app.js
templates/admin/default/assets/css/style.css
Uncomment these listeners only when the files are needed by your module.
# Routing
The sample module defines its routes in:
routes.php
Backend routes use the web middleware and module ACL:
Route::middleware(['web', 'acl:core.modules.addons.custom-module'])
->prefix('backend')
->group(function () {
// backend routes
});
API v3 routes use Sanctum and the API middleware:
Route::middleware(['auth:sanctum', 'api', 'acl:core.modules.addons.custom-module'])
->prefix('api/v3/admin/custom-module')
->name('api.v3.admin.custom-module.')
->group(function () {
// API v3 routes
});
# Recommended Development Pattern
When building your own addon, keep the responsibilities separated:
| File | Responsibility |
|---|---|
CustomModule.php | Module metadata and activation/deactivation hooks |
CustomModuleProvider.php | Bootstrap module services, commands, scheduler, observers |
CustomModuleEventHandler.php | EasyDCIM event listeners |
Controller/* | Backend and API request handling |
Observers/* | Eloquent model lifecycle hooks |
Widgets/* | Dashboard and page widgets |
database/migrations/* | Module database structure |
Support/* | Small helper services and installers |
templates/* | Twig views |
config/* | Grid, dashboard, and module config |
Avoid placing all logic inside the provider. A small provider and separate event handler, observers, controllers, and services are easier to test and maintain.