Skip to main content

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'.

MethodReturnDescription
identifier()stringUnique slug (kebab-case). Used in routes, cache, the module:{id} middleware, and the site_modules table. E.g.: 'indoor-location'
name()stringDisplay name (the brand). E.g.: 'Synapsys'
description()stringShort description. E.g.: 'BLE-based indoor location tracking'
version()stringSemantic 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 ≠ name

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()).

MethodReturnPurpose
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)voidRegisters scheduled tasks
reportHandlers() returns ReportContract

Despite 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:

ParameterTypeDescription
identifierstringUnique slug within the module (kebab-case)
namestringLabel shown in the configuration UI
descriptionstringShort explanation of what the feature does
defaultEnabledboolValue 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.

ParameterTypeDescription
namestringUnique slug within the module. Becomes ionguard_{module-identifier}_{name}_{deploy}
commandstringArtisan command to invoke (wrapped as php artisan {command})
envarray<string,string>Extra environment variables merged over the platform's .env. Default: []
restartstringDocker restart policy. Default: 'unless-stopped'
stopGracePeriodSecondsint|nullSeconds 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();
}
Required guard clause

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 via ModuleRegistry).
  • 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