Modular Architecture (internachi/modular + Filament + RBAC) · MemphisLab Docs
MemphisLab Docs
License Manager

Modular Architecture (internachi/modular + Filament + RBAC)

Como os módulos de domínio são isolados como pacotes Composer, e como o RBAC é derivado do painel automaticamente

internachi/modular: módulo como pacote Composer local

Num projeto Laravel comum, todo o código de domínio vive dentro de app/, num namespace só, sem fronteira imposta pelo próprio PHP entre “módulo de licença” e “módulo de tenant” — a separação, se existir, é só convenção de pasta. internachi/modular torna essa fronteira real: cada módulo (access, canary, license, oauth, recovery, tenancy) é um pacote Composer de verdade, com o próprio composer.json, resolvido via repositories: [{type: path, url: "app-modules/*"}] no composer.json raiz — o mesmo mecanismo que resolve uma dependência do Packagist, só que apontando para uma pasta local em vez de baixar de um registro.

// app-modules/license/composer.json
{
  "name": "modules/license",
  "autoload": {
    "psr-4": {
      "Modules\\License\\": "src/",
      "Modules\\License\\Database\\Seeders\\": "database/seeders/"
    }
  },
  "extra": {
    "laravel": {
      "providers": ["Modules\\License\\Providers\\LicenseServiceProvider"]
    }
  }
}

O efeito prático de ser um pacote Composer, não só uma pasta: namespace próprio (Modules\License\, não App\), autoload isolado, e principalmente — um módulo só pode usar código de outro módulo se declarar a dependência no próprio composer.json, do mesmo jeito que declarar uma lib de terceiro. Isso torna um acoplamento indevido entre módulos (ex.: oauth chamando direto uma classe interna de tenancy sem essa relação ser intencional) visível na revisão do composer.json, em vez de escondido dentro de um use statement qualquer.

extra.laravel.providers é o que registra o ServiceProvider do módulo automaticamente na aplicação Laravel — cada módulo é, na prática, um mini pacote Laravel com autodiscovery, do mesmo jeito que qualquer pacote de terceiro instalado via Composer.

Filament: admin resource-driven

Filament gera o CRUD do painel a partir de uma declaração — um Resource (ex.: LicenseResource) descreve os campos de um formulário e as colunas de uma tabela uma vez, e o Filament produz as páginas de listar, criar, editar e visualizar a partir dessa única declaração, sem view escrita à mão para cada uma. Isso é o oposto de scaffolding tradicional (gerar arquivos de controller/view que depois divergem da declaração original): a declaração é a fonte, não um ponto de partida copiado.

Essa característica — o catálogo de Resources ser enumerável em runtime, via Filament::getPanel('admin')->getResources() — é o que o RBAC deste projeto explora diretamente, na seção seguinte.

RBAC derivado do painel: AccessProvisioner

Um sistema de permissões convencional exige alguém escrever manualmente cada permissão nova ("license.view", "license.create"…) toda vez que uma tela nova é adicionada — e esquecer de fazer isso é o jeito mais comum de uma tela nova ficar sem controle de acesso algum. Este projeto inverte isso: as permissões não são escritas, são derivadas do painel Filament em runtime.

// app-modules/access/src/Support/AccessProvisioner.php
public static function subjects(): array
{
    $subjects = [];
    foreach (Filament::getPanel(self::panel())->getResources() as $resource) {
        $model = $resource::getModel();
        $subject = Str::snake(class_basename($model));
        $subjects[$subject] = self::actionsFor($subject, $model);
    }
    return [...$subjects, ...self::customSubjects()];
}

Para cada Resource registrado no painel, o nome do subject vem do model dele (LicenseResource → model License → subject license), e as ações vêm de PermissionAction (view, create, update, delete, mais restore/forceDelete se o model usa SoftDeletes) — cada combinação vira uma permissão {subject}.{action} (license.view, license.create…). Group é o nome interno para Role do spatie/laravel-permission — o pacote de terceiro que armazena e verifica papel/permissão via tabelas pivot (model_has_roles, role_has_permissions); AccessProvisioner é a camada própria do projeto que popula esse catálogo automaticamente, não substitui o spatie/permission.

A consequência direta: adicionar um Resource novo ao painel cria as permissões dele automaticamente na próxima execução de access:sync-permissions — não existe um segundo passo manual de “agora registra a permissão desse Resource novo”, e não existe risco de uma tela nova ficar sem controle de acesso por esquecimento. O comando é chamado automaticamente a cada deploy (entrypoint.sh), então esse catálogo nunca fica desatualizado em produção por mais que o tempo de um deploy.