# Sample EasyDCIM Extension

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
    });

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.