Module Contract
Every ION Guard module implements the App\Contracts\ModuleContract interface. It is the single contract between the core and the module: the core only knows the module through these methods. The abstract class App\Contracts\BaseModule implements the entire contract with empty defaults, leaving only the four identity methods required — all others are optional and you override only what the module uses.
namespace App\Modules\MeuModulo;
use App\Contracts\BaseModule;
final class MeuModuloModule extends BaseModule
{
public function identifier(): string { return 'meu-modulo'; }
public function name(): string { return 'Meu Módulo'; }
public function description(): string { return 'O que o módulo faz.'; }
public function version(): string { return '1.0.0'; }
// scope() and all other methods inherit the defaults from BaseModule
}
Identity
Methods that describe the module. identifier(), name(), description(), and version() are abstract in BaseModule — always implemented. scope() defaults to 'site'.
| Method | Return | Description |
|---|---|---|
identifier() | string | Unique slug (kebab-case). Used in routes, cache, the module:{id} middleware, and the site_modules table. E.g.: 'indoor-location' |
name() | string | Display name (the brand). E.g.: 'Synapsys' |
description() | string | Short description. E.g.: 'BLE-based indoor location tracking' |
version() | string | Semantic version of the module. E.g.: '1.0.0' |
scope() | string | 'site' (enabled per site via site_modules) or 'global' (always active). Default: 'site' |
identifier() is the technical slug and does not change — it appears in routes, middleware, and database keys. name() is the brand label, which can change without breaking anything. In the tracking module, identifier() = 'indoor-location' but name() = 'Synapsys'.
Extensions
Optional methods that the core collects during boot to extend the platform. In BaseModule they all return an empty array (or do nothing, in the case of schedule()).
| Method | Return | Purpose |
|---|---|---|
alertHandlers() | list<class-string<AlertHandlerContract>> | Alert handler classes to register |
alertTypes() | list<string> | Alert types the module defines |
eventTypes() | list<string> | Domain event types the module emits |
reportHandlers() | list<class-string<ReportContract>> | Report handler classes |
features() | list<ModuleFeature> | Sub-resources that can be enabled/disabled per site |
morphMap() | array<string, class-string> | Morph map for polymorphic relationships |
policies() | array<class-string, class-string> | Model → Policy bindings |
gates() | array<string, Closure> | Authorization gates |
seeders() | list<class-string<Seeder>> | Seeders (demo data) |
workers() | list<WorkerDefinition> | Long-running processes (MQTT listeners, etc.) |
schedule(Schedule $schedule) | void | Registers scheduled tasks |
reportHandlers() returns ReportContractDespite the method's name, the classes returned by reportHandlers() implement the App\Contracts\ReportContract interface (there is no ReportHandlerContract interface). Likewise, alertHandlers() returns classes that implement App\Contracts\AlertHandlerContract.
Sub-resources: features()
features() returns a list of App\Contracts\Modules\ModuleFeature — sub-capabilities that a Platform Admin can turn on/off per site, without disabling the entire module. This is how the Synapsys module exposes panic-button, asset-tracking, and cold-room (Cold room).
ModuleFeature is a readonly object with these constructor parameters:
| Parameter | Type | Description |
|---|---|---|
identifier | string | Unique slug within the module (kebab-case) |
name | string | Label shown in the configuration UI |
description | string | Short explanation of what the feature does |
defaultEnabled | bool | Value used when the site does not yet have a persisted preference. Default: true |
use App\Contracts\Modules\ModuleFeature;
public function features(): array
{
return [
new ModuleFeature(
identifier: 'cold-room',
name: 'Câmara Fria',
description: 'Monitoramento de exposição em ambientes refrigerados.',
defaultEnabled: false,
),
];
}
Persistence lives in site_modules.settings.features.{identifier} as a boolean. When the key is absent, defaultEnabled is used. Alert handlers and report handlers can declare a dependency on a sub-resource via their feature() methods; the central dispatch checks ModuleRegistry::isFeatureEnabledForSite() before invoking them.
Workers: workers()
workers() returns a list of App\Contracts\Workers\WorkerDefinition — long-running processes that the platform keeps alive. On install/uninstall, the App\Actions\RegenerateModuleRuntimeCompose action materializes each worker as a dedicated Docker Compose container (mirroring the ionguard_horizon / ionguard_scheduler / ionguard_reverb pattern).
The module describes what to run, never how to containerize it — image, volumes, and networks are provided by the renderer.
| Parameter | Type | Description |
|---|---|---|
name | string | Unique slug within the module. Becomes ionguard_{module-identifier}_{name}_{deploy} |
command | string | Artisan command to invoke (wrapped as php artisan {command}) |
env | array<string,string> | Extra environment variables merged over the platform's .env. Default: [] |
restart | string | Docker restart policy. Default: 'unless-stopped' |
stopGracePeriodSeconds | int|null | Seconds Docker waits after SIGTERM before SIGKILL. Default: 30 |
use App\Contracts\Workers\WorkerDefinition;
public function workers(): array
{
return [
new WorkerDefinition(
name: 'mqtt-listener',
command: 'indoor-location:mqtt-listen',
),
];
}
Scheduled tasks: schedule()
The core's routes/console.php file iterates over all registered modules and calls $module->schedule($schedule) — regardless of whether the module is enabled for any site.
use Illuminate\Console\Scheduling\Schedule;
public function schedule(Schedule $schedule): void
{
$schedule->call(function (): void {
foreach (Site::query()->get() as $site) {
if (app(ModuleRegistry::class)->isEnabledForSite('meu-modulo', $site->id)) {
dispatch(new MeuJob($site->id));
}
}
})->everyFiveMinutes();
}
Since schedule() is called for all registered modules regardless of enablement state, scheduled jobs must check isEnabledForSite() (and, where applicable, isFeatureEnabledForSite()) before executing for each site.
Collection cycle by the core
The core queries the contract at distinct moments:
- Register/Boot (
ModuleServiceProvider):identifier(),name(),scope(),morphMap(),policies(),gates(),seeders()are applied; the module's routes and migrations are loaded. - Alert dispatch:
alertTypes()/alertHandlers()(filtered by site and sub-resource viaModuleRegistry). - Reports:
reportHandlers(). - Scheduler (
routes/console.php):schedule(). - Install/Uninstall:
workers()→RegenerateModuleRuntimeCompose.
The ModuleRegistry exposes scope-aware methods that respect enablement per site and per sub-resource:
$registry->isEnabledForSite('indoor-location', $siteId); // bool
$registry->enabledModuleIdentifiersForSite($siteId); // Collection
$registry->alertTypesForSite($siteId); // array
$registry->isFeatureEnabledForSite('indoor-location', 'cold-room', $siteId); // bool
Next steps
- Module System — Architecture and lifecycle
- Creating a Module — Step-by-step guide
- Alert Handlers —
AlertHandlerContract - Report Handlers —
ReportContract