Docs›Roteamento›Rotas públicas
Roteamento

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 usarComoExemplo 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étodoGeraUso 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}/editarCRUD 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 mapaURL 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 mapaFallback /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

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

Modelo allowlist — não existe catch-all. Uma classe sem rota declarada não responde em /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ão mad.auth do admin, ou públicos e stateless atrás de mad.api (Bearer + escopo de tenant).