Rotas públicas
routes/web.php, MadRoutes, módulos, parâmetros de URL.
O app interno é um Laravel padrão: um único routes/web.php
como orquestrador, sem entry point separado e sem catch-all. Toda tela do admin
(/app/*) precisa de uma linha explícita registrada via o helper
Mad\Routing\MadRoutes; rotas verdadeiramente públicas (sem login) usam a
facade nativa Illuminate\Support\Facades\Route direto, do jeito Laravel
de sempre.
Dois jeitos de declarar uma rota
| Quando usar | Como | Exemplo real |
|---|---|---|
Tela do admin ligada a um MadComponent (/app/*) |
MadRoutes::screen() / expose() / resource() |
MadRoutes::resource('users', 'UserList', 'UserForm') |
| Endpoint/página HTML "normal" (controller comum, anônimo ou não) | Route::get() / post() / etc. nativos |
Route::get('/docs/{section?}/{page?}', [MadFrameworkDocs::class, 'showPage']) |
As duas formas convivem no mesmo routes/web.php — o helper MadRoutes
é só açúcar sintático em cima do Router nativo (ele mesmo chama
Route::get()/Route::match() por baixo) que também alimenta o mapa
de URLs amigáveis consumido por MadAction/MadResponse
(MadRoutes::toFriendlyUrl()).
Rotas realmente públicas — exemplos reais deste repositório
Duas rotas deste próprio app rodam sem nenhum middleware de auth — o portal de documentação que você está lendo agora, e o link público de documentos do GED:
// routes/web.php
// Portal de documentação do framework (MadSitePage) — público, sem auth.
Route::get('/docs/{section?}/{page?}', [\App\Control\Docs\MadFrameworkDocs::class, 'showPage'])
->where('section', '[a-z0-9-]+')->where('page', '[a-z0-9-]+')->name('docs.show');
// GED: link público de documento (anônimo, sem login). O token (64 hex) é o
// segredo; senha opcional do link é tratada pelo controller.
Route::get('/public/ged/{token}', [\App\Http\Controllers\GedPublicLinkController::class, 'show'])
->where('token', '[A-Fa-f0-9]{64}')->name('ged.public');
Route::post('/public/ged/{token}', [\App\Http\Controllers\GedPublicLinkController::class, 'unlock'])
->where('token', '[A-Fa-f0-9]{64}')->name('ged.public.unlock');
"Sem auth" não quer dizer "sem nada": as duas continuam dentro do grupo web padrão
do Laravel (sessão, CSRF, etc — ver Middleware stack),
porque routes/web.php inteiro já é auto-embrulhado nesse grupo pelo
bootstrap/app.php. O que elas não têm é os middlewares
mad.auth / mad.permission do app interno.
Parâmetros de URL — binding nativo do Laravel
Nada de injeção por reflection custom: parâmetros de rota usam o
{nome} nativo do Illuminate Router, com ->where() para travar o
formato (essencial em rotas anônimas, como o token de 64 caracteres do GED acima).
Em controllers de classe, o valor chega como argumento normal do método:
class GedPublicLinkController
{
// GET /public/ged/{token} — o ?dl=1 vem do Request, o {token} do binding da rota
public function show(Request $request, string $token): Response { /* ... */ }
}
Nas telas do admin (MadRoutes), o parâmetro vem dos ->defaults()
da rota (class/method) — quem resolve isso é o
MadAppController::run(), não reflection sobre a assinatura do método.
MadRoutes — registrando telas do admin
| Método | Gera | Uso típico |
|---|---|---|
screen($key, $class) | GET /app/{slug} | Tela única, sem sub-método (dashboards, views) |
expose($key, $class) | GET+POST /app/{slug}/{method?} | Classe com ações dinâmicas (onSave, onEdit...) |
resource($key, $list, $form) | /{slug}, /{slug}/novo, /{slug}/{id}/editar | CRUD completo (list + form) |
exposeMethod($path, $class, $method) | GET+POST /app/{path} | UM método num caminho próprio (/app/clientes/aprovar) |
exposeClass($class) | GET+POST /app/{Classe}/{method?} | Serviço AJAX cru, sem slug amigável |
exposeService($slug, $class) | GET+POST /app/services/{slug}/{method?} | Serviço AJAX com slug que não vaza o nome da classe |
exposeMethod() — a ordem de registro importa. O
{method?} do expose() casa um segmento qualquer e o Laravel é
first-match-wins: registrado DEPOIS do expose() da mesma classe,
/app/clientes/aprovar cairia no expose com method=aprovar — que
não é o nome real do método (onAprovar) e dá 404 no dispatch. Registre as
linhas de exposeMethod() antes das de expose().
Ele complementa o expose, não o substitui: /app/{slug}/onAprovar continua
respondendo; só o outbound (urlFor) passa a render o caminho amigável.
O caminho é literal, sem locale — slug de método é único project-wide, diferente do slug
de PÁGINA, que é traduzido.
MadRoutes::exposeMethod('clientes/aprovar', 'ClienteList', 'onAprovar'); // ANTES
MadRoutes::expose('clientes', 'ClienteList'); // DEPOIS
Montando URL em PHP — MadRoutes::urlFor()
O mesmo mapa que registra as rotas serve de mapa outbound. Para montar a URL
de um endpoint declarado em código PHP (botão do notchbar, ação server-side, link em
template) a API direta é urlFor($class, $method = null, $params = []):
MadRoutes::urlFor('LoginForm', 'onLogout', ['static' => 1]);
// → /app/login/onLogout?static=1
MadRoutes::urlFor('UserForm', 'onEdit', ['id' => 42]);
// → /app/usuarios/42/editar (resource: o editParam sai da query e entra no caminho)
MadRoutes::urlFor('UserList');
// → /app/usuarios
| Situação da classe no mapa | URL resultante |
|---|---|
exposeMethod declarado p/ o par (classe, método) | O caminho literal registrado — tem precedência sobre o mapa por classe |
expose() (aceita {method?}) | /app/{slug}/{method} |
screen() / list de resource() + método | /app/{Classe}/{método} — a rota por slug é só GET /{slug}, sem {method?}; slug+método daria 404 |
form de resource() | /{slug}/{novo}, ou /{slug}/{id}/{editar} quando há onEdit/id |
| Classe sem entrada no mapa | Fallback /app/{Classe}[/{método}] — no modo allowlist só resolve se houver exposeClass(); senão 404, por design |
urlFor() para montar; toFriendlyUrl() só para converter.
MadRoutes::toFriendlyUrl($url) existe para traduzir URL legada já
existente como string (index.php?class=X&method=Y gravada em menu.xml
ou em actions assadas em dados) — ele apenas faz o parse e delega para o
urlFor(). Nunca construa uma string index.php?class=… só para
passá-la pelo toFriendlyUrl(). O mapa outbound não é
exportado para o client (evita enumeração da superfície admin), então quem precisa de uma
URL no JS recebe ela pronta do server (ex.: data-url no botão do notchbar —
ver CSRF protection).
Método com parênteses vazios ('onShow()', forma que o studio antigo gravava) é
normalizado antes de virar segmento de URL — sem isso viraria onShow%28%29, que a
constraint [A-Za-z][A-Za-z0-9_]* do expose() rejeita com 404.
Os slugs vêm de lang/{locale}/routes.php (chave routes.{key}.slug),
registrados para os 3 locales suportados no sentido inbound; o mapa outbound
(toFriendlyUrl) usa só o locale ativo. Sem entrada de slug, a rota
simplesmente não é registrada naquele locale — não existe fallback silencioso.
// routes/modules/admin.php
Route::middleware(['mad.auth', 'mad.permission'])->group(function () {
MadRoutes::resource('users', 'UserList', 'UserForm'); // /app/usuarios
MadRoutes::screen('admin_dashboard', 'AdministrationDashboard'); // /app/admin/painel
MadRoutes::expose('db_explorer', 'DatabaseExplorer'); // /app/admin/banco-dados
});
Módulos — cada arquivo é autocontido
Conteúdo autenticado não vive inteiro em web.php: fica fatiado em
routes/modules/*.php, um arquivo por domínio (admin,
builder, communication, documents, logs).
Cada módulo declara seu próprio grupo de middleware — não herda nada
implícito do arquivo que o carrega — então a ordem dos require() é livre:
// routes/web.php — final do arquivo
require base_path('routes/modules/admin.php');
require base_path('routes/modules/builder.php');
require base_path('routes/modules/communication.php');
require base_path('routes/modules/documents.php');
require base_path('routes/modules/logs.php');
// Rotas GERADAS do projeto (MadBuilder) — ausentes no template base.
if (is_file($madGenerated = base_path('routes/modules/generated.php'))) {
require $madGenerated;
}
Um módulo pode misturar telas (MadRoutes, atrás de
mad.auth+mad.permission) com endpoints JSON internos
(Route::apiResource/Route::post crus, atrás de só
mad.auth) — ver o bloco de API REST interna em
routes/modules/admin.php e builder.php como exemplo.
As três camadas de acesso
| Camada | Middleware | Exemplo |
|---|---|---|
| Pública — qualquer um, sem sessão de usuário | nenhum (só o grupo web padrão) |
/docs/*, /public/ged/{token}, /app/login |
| Autenticada — exige usuário logado, sem checar programa | mad.auth |
/app/_mad-wire, /app/conta/configuracoes |
| Autorizada — logado + permissão do programa/ação | mad.auth + mad.permission |
Qualquer tela de routes/modules/*.php |
| API pública — stateless, sem cookie de sessão | mad.api (opcionalmente mad.api:orders.read) |
/api/* (routes/api.php, grupo api: sem sessão, sem CSRF) |
A quarta camada é a mais nova: mad.api autentica por
Authorization: Bearer mad_api_… e resolve token → unit → tenant → banco num passo
só, fora do grupo web. Detalhes (abilities por rota, códigos de erro, emissão de
token) em REST API — Autenticação.
Detalhes de cada middleware (o que cada um valida, como negar e em que ordem aplicar)
estão em Middleware stack; a autenticação
propriamente dita (sessão, login, PermissionGate) está em
Auth no portal público.
/app/* mesmo que exista no código; ela
fica só com URL amigável calculada no menu/MadAction e devolve 404 até
alguém registrar a rota. Isso é proposital (superfície de ataque mínima), não um bug.
Próximos passos
- Middleware stack — pipeline completo, aliases e como criar middleware custom.
- CSRF protection — como o token é gerado, validado e enviado em forms/AJAX/MadWire.
- Auth no portal público — sessão, login,
PermissionGate. - REST API — endpoints JSON via
Route::apiResource: internos, na sessãomad.authdo admin, ou públicos e stateless atrás demad.api(Bearer + escopo de tenant).