Pular para o conteúdo principal

Criando um Módulo

Guia passo a passo para criar um novo módulo para a plataforma ION Guard.

Pré-requisitos

  • Familiaridade com Sistema de Módulos
  • Ambiente de desenvolvimento com Laravel Sail configurado
  • Módulo DevTools habilitado (para geração de pacotes)

Início rápido

1. Criar a estrutura de diretórios

Crie o diretório do módulo em app/Modules/{NomeDoModulo}/:

app/Modules/MeuModulo/
├── module.json
├── MeuModuloModule.php
├── MeuModuloServiceProvider.php
├── routes/
│ ├── web.php
│ └── api.php
├── database/
│ └── migrations/
└── resources/
└── js/
├── entry.ts
└── pages/

2. Criar o module.json

{
"identifier": "meu-modulo",
"name": "Meu Módulo",
"version": "1.0.0",
"description": "Descrição do que o módulo faz.",
"scope": "site",
"authors": [
{ "name": "Sua Empresa", "email": "dev@empresa.com" }
],
"requires": [],
"laravel": "^12.0"
}
CampoDescrição
identifierSlug único do módulo (kebab-case). Usado em rotas, cache, middleware
scope"site" = habilitado por site. "global" = sempre ativo
requiresDependências de outros módulos (array de identifiers)

3. Criar a classe do módulo

MeuModuloModule.php — implementa ModuleContract (ou estende BaseModule):

<?php

declare(strict_types=1);

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 'Descrição do módulo.';
}

public function version(): string
{
return '1.0.0';
}

public function scope(): string
{
return 'site';
}
}

BaseModule fornece implementações padrão (arrays vazios) para todos os métodos opcionais. Sobrescreva conforme necessário.

4. Criar o ServiceProvider

MeuModuloServiceProvider.php:

<?php

declare(strict_types=1);

namespace App\Modules\MeuModulo;

use App\Modules\ModuleRegistry;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;

final class MeuModuloServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->make(ModuleRegistry::class)
->register(new MeuModuloModule());
}

public function boot(): void
{
$this->loadMigrationsFrom(__DIR__.'/database/migrations');

Route::middleware(['web', 'auth', 'module:meu-modulo'])
->group(__DIR__.'/routes/web.php');

Route::middleware(['api', 'auth:sanctum', 'module:meu-modulo'])
->prefix('api/v1')
->group(__DIR__.'/routes/api.php');
}
}

5. Criar rotas

routes/web.php:

<?php

use App\Modules\MeuModulo\Http\Controllers\Web\MeuController;
use Illuminate\Support\Facades\Route;

Route::get('/meu-modulo', [MeuController::class, 'index'])
->name('meu-modulo.index');

O middleware module:meu-modulo garante que as rotas só são acessíveis quando o módulo está habilitado para o site atual.

6. Criar o frontend entry point

resources/js/entry.ts:

// Auto-descoberta de páginas Inertia
const rawPages = import.meta.glob('./pages/**/*.tsx');
export const pages: Record<string, () => Promise<unknown>> = {};
for (const [filePath, resolver] of Object.entries(rawPages)) {
const name = filePath.replace(/^\.\/pages\//, '').replace(/\.tsx$/, '');
pages[name] = resolver;
}

// Registro de traduções i18n
export function registerLocales(): void {
// Importar e registrar bundles de tradução aqui
}

// Registro de slots extensíveis
export function registerSlots(): void {
// Registrar componentes em slots do core aqui
}
Nomenclatura de páginas

As pastas dentro de pages/ devem seguir kebab-case e corresponder ao identifier do módulo. Exemplo: pages/meu-modulo/index.tsx.

Pontos de extensão

Após a estrutura básica, adicione funcionalidades sobrescrevendo métodos do ModuleContract:

Alert Handlers

public function alertHandlers(): array
{
return [
MeuAlertHandler::class,
];
}

public function alertTypes(): array
{
return ['meu-alerta'];
}

Consulte Alert Handlers para implementação detalhada.

Report Handlers

public function reportHandlers(): array
{
return [
MeuRelatorioHandler::class,
];
}

Consulte Report Handlers para implementação detalhada.

Adicione itens ao menu lateral no boot() do ServiceProvider, resolvendo o NavigationRegistry do core e registrando um NavigationGroup com seus NavigationItem. Amarre o grupo/itens ao módulo via o parâmetro module: — assim o menu só aparece nos sites onde o módulo está habilitado (o NavigationRegistry checa isEnabledForSite, e isFeatureEnabledForSite quando feature: é informado):

use App\Navigation\NavigationGroup;
use App\Navigation\NavigationItem;
use App\Navigation\NavigationRegistry;

public function boot(): void
{
// ... (rotas, migrations)

$nav = $this->app->make(NavigationRegistry::class);

$group = $nav->group(new NavigationGroup(
id: 'meu-modulo',
title: 'Meu Módulo',
order: 50,
requiresSite: true,
module: 'meu-modulo',
translationKey: 'meu_modulo:nav.group',
));

$group->addItem(new NavigationItem(
title: 'Dashboard',
href: '/meu-modulo',
icon: 'layout-dashboard',
order: 10,
requiresSite: true,
module: 'meu-modulo',
translationKey: 'meu_modulo:nav.dashboard',
));
}

NavigationItem e NavigationGroup também aceitam minimumRole:, gate: e feature: para controlar a visibilidade por perfil, permissão ou sub-recurso.

Não existe navigationGroups()

A navegação é registrada imperativamente no NavigationRegistry (como acima), não por um método navigationGroups() no contrato — esse método não existe na plataforma.

Relacionamentos dinâmicos

Estenda models do core sem modificá-los:

// No boot() do ServiceProvider
Site::resolveRelationUsing('meuModuloConfig', function (Site $site) {
return $site->hasOne(MeuModuloConfig::class);
});

Tarefas agendadas

public function schedule(Schedule $schedule): void
{
$schedule->call(function (): void {
// Verificar se o módulo está habilitado para cada site
foreach (Site::query()->get() as $site) {
if (app(ModuleRegistry::class)->isEnabledForSite('meu-modulo', $site->id)) {
dispatch(new MeuJob($site->id));
}
}
})->everyFiveMinutes();
}
Guard clause obrigatório

Jobs agendados devem verificar se o módulo está habilitado para o site antes de executar. O scheduler central chama schedule() para todos os módulos registrados, independente do estado de habilitação.

Workers (processos long-running)

public function workers(): array
{
return [
new WorkerDefinition(
name: 'meu-listener',
command: 'meu-modulo:listen',
),
];
}

Workers são convertidos em containers Docker pelo action RegenerateModuleRuntimeCompose.

Testes

Estruture os testes dentro do módulo:

app/Modules/MeuModulo/
└── tests/
├── Feature/
│ └── MeuControllerTest.php
└── Unit/
└── MeuServiceTest.php

Os testes são descobertos automaticamente pelo Pest/PHPUnit. Execute com:

sail artisan test app/Modules/MeuModulo/tests/

Empacotamento

Para distribuir o módulo como pacote instalável:

  1. Acesse DevToolsModule Generator na interface
  2. Selecione o módulo
  3. O sistema faz build do frontend (se aplicável), empacota em ZIP e encripta

O pacote .enc é instalado via a interface de Módulos por um Platform Admin: o core (App\Actions\InstallModule + App\Services\ModuleReleaseEncryption) descriptografa o pacote, valida a estrutura do ZIP e publica os assets. Os workers declarados pelo módulo viram containers via RegenerateModuleRuntimeCompose.

Geração de pacotes

O fluxo de geração do pacote (build + empacotamento + encriptação, ex.: via um módulo DevTools) é uma ferramenta de desenvolvimento e pode variar conforme o ambiente. O que o core garante é o lado da instalação do .enc descrito acima.

Checklist pré-distribuição

  • module.json com metadados corretos
  • ServiceProvider registra o módulo no ModuleRegistry
  • Middleware module:{identifier} em todas as rotas
  • Migrations criam e removem tabelas corretamente
  • Morph map registrado (se usar relações polimórficas)
  • Policies registradas para todos os models
  • Alert handlers implementam AlertHandlerContract completamente
  • Jobs verificam habilitação do módulo (guard clause)
  • Frontend entry.ts exporta pages, registerLocales, registerSlots
  • Traduções i18n em pt_BR e en
  • Testes cobrindo happy path e edge cases
  • README.md com descrição e instruções
  • Documentação em docs/ (user-guide.md, api.md, architecture.md)