Declarando rotas
routes/api.php + mad.api:<ability> vs routes/modules + mad.auth; Route::apiResource, propriedades do controller, only/except.
Declarar um recurso REST é sempre o mesmo par: uma classe que estende
Mad\Rest\ApiResourceController com a configuração, e uma linha
Route::apiResource() que liga a URL às cinco ações padrão. O que muda é
onde você declara a rota, e isso decide a autenticação:
| Arquivo | Grupo | Middleware | Prefixo |
|---|---|---|---|
routes/api.php |
api (só SubstituteBindings: sem sessão, sem CSRF) |
mad.api — Bearer mad_api_* |
/api automático (vem do withRouting(api: …) em bootstrap/app.php) |
routes/modules/*.php |
web (sessão + CSRF) |
mad.auth |
->prefix('api') explícito |
O controller — só configuração, sem lógica de transporte
A subclasse declara propriedades protegidas; a base resolve query, paginação, validação, sync de detalhes e serialização. Tabela completa:
| Propriedade | Tipo | Default | Função |
|---|---|---|---|
$model | string | — (obrigatório) | FQCN do model Eloquent. |
$primaryKey | ?string | null | PK do recurso; null deriva de getKeyName(). |
$searchable | array | [] | Whitelist de colunas filtráveis (DSL de filtros). |
$sortable | array | [] | Whitelist de colunas ordenáveis (sort). |
$with | array | [] | Eager-load default. Os $details entram junto (sem N+1). |
$withCount | array | [] | withCount default. |
$appends | array | [] | Accessors computados a incluir (Model::append). |
$indexFields | array | [] | Projeção da listagem. Vazio = todos menos $hidden. |
$showFields | array | [] | Projeção do show/respostas de write. |
$hidden | array | [] | Campos sempre removidos da resposta. |
$defaultOrder | ?string | null | Ordem default "coluna asc|desc" quando não vem sort. |
$perPage | int | 15 | Itens por página default. |
$maxPerPage | int | 200 | Teto de per_page (defesa contra abuso). |
$details | array | [] | Relações hasMany tratadas como detalhe. Múltiplas. |
$transformers | array | [] | campo => fn($value, $model). Ver addTransformer(). |
O único requisito do model: declarar as relações hasMany usadas em $details e, opcionalmente, Model::rules($id = null): array para validação.
A rota pública — routes/api.php + mad.api
routes/api.php é registrado por withRouting(api: …) em
bootstrap/app.php: tudo ali já ganha o prefixo /api e o grupo
api do Laravel. A rota só precisa do mad.api:
// routes/api.php
use App\Http\Controllers\Sales\OrderApiController;
// Só auth + escopo de tenant (qualquer token válido passa):
Route::middleware('mad.api')->apiResource('orders', OrderApiController::class);
// Com abilities — o token precisa cobrir a permissão exigida por CADA rota:
Route::middleware('mad.api:orders.read')->get('/orders', [OrderApiController::class, 'index']);
Route::middleware('mad.api:orders.write')->post('/orders', [OrderApiController::class, 'store']);
// Vírgula = TODAS exigidas (E lógico, não OU):
Route::middleware('mad.api:orders.write,orders.approve')
->post('/orders/{order}/approve', [OrderApiController::class, 'approve']);
A tela admin de emissão de tokens (/app/tokens-api) monta a checklist de
abilities varrendo a route table atrás de mad.api:<ability> —
MadApiTokenService::declaredAbilities(). Nada é digitado à mão: se a
ability não está declarada em nenhuma rota, ela não aparece para ser concedida.
Escolha nomes estáveis (dominio.acao, segmentos a-z0-9_-
separados por ponto) — eles viram contrato com quem já tem token emitido.
A rota interna — Route::apiResource atrás de mad.auth
// routes/modules/admin.php
Route::middleware('mad.auth')
->prefix('api')
->group(function () {
Route::apiResource('import-templates', \App\Http\Controllers\Sys\ImportTemplateApiController::class);
});
Route::apiResource é o helper nativo do Laravel — não tem nada
de mágica MAD aqui. Ele gera as cinco rotas REST padrão para o nome dado, mapeando o verbo
HTTP + path para o método correspondente do controller:
| Verbo + URI | Método chamado | Nome da rota |
|---|---|---|
GET /import-templates | index() | import-templates.index |
POST /import-templates | store() | import-templates.store |
GET /import-templates/{import_template} | show() | import-templates.show |
PUT|PATCH /import-templates/{import_template} | update() | import-templates.update |
DELETE /import-templates/{import_template} | destroy() | import-templates.destroy |
Como é o Route::apiResource nativo, as opções nativas funcionam normalmente:
only/except para expor um subconjunto das ações, names
para customizar nomes de rota, múltiplos recursos no mesmo grupo de middleware/prefixo.
// só leitura — sem store/update/destroy
Route::apiResource('reports', ReportApiController::class)->only(['index', 'show']);
// vários recursos no mesmo grupo
Route::middleware('mad.auth')->prefix('api')->group(function () {
Route::apiResource('import-templates', ImportTemplateApiController::class);
Route::apiResource('orders', OrderApiController::class);
});
$details = ['items', 'payments'] não gera
GET /orders/{order}/items. Os detalhes só existem
embutidos no payload do master (eager-loaded no show/
index, sincronizados no store/update). Se você
precisa de um endpoint independente para a tabela de detalhe, declare outro
ApiResourceController para ela.
Resolução do ID — não é route-model binding do Eloquent
O parâmetro {import_template} chega como string/int cru, não como instância
do model resolvida por binding implícito. resolveId() pega o
último parâmetro da rota corrente (independente do nome que
apiResource gerou) e, fora de uma rota (ex.: chamada direta em teste), cai para
a PK no corpo/query string. Isso significa que renomear o parâmetro da rota não quebra nada.
Múltiplos módulos, mesmo padrão
Cada arquivo em routes/modules/*.php é autocontido — declara seu próprio grupo
de middleware, sem herdar nada implícito do arquivo que o carrega. Um módulo costuma misturar
telas do admin (MadRoutes, atrás de mad.auth+mad.permission)
com um bloco de API REST interna (atrás de só mad.auth — endpoints JSON não
passam pelo gate de permissão de programa):
// routes/modules/admin.php
Route::middleware(['mad.auth', 'mad.permission'])->group(function () {
MadRoutes::resource('users', 'UserList', 'UserForm');
// ...telas do admin
});
// ─── API REST (Mad\Rest\ApiResourceController) ──────────────────────────────
Route::middleware('mad.auth')
->prefix('api')
->group(function () {
Route::apiResource('import-templates', \App\Http\Controllers\Sys\ImportTemplateApiController::class);
});
Próximos passos
- Autenticação —
mad.api(Bearer stateless + escopo de tenant) emad.auth(sessão + CSRF), lado a lado. - Tokens de API e abilities — emitir o token que a rota
mad.api:…vai exigir. - JSON responses — formato de cada resposta, projeção e status codes.
- Exemplos práticos — o mesmo
ImportTemplateApiControllerem curl, do zero ao mestre-detalhe.