Request injection
Type-hint Request, query params, body JSON, validação por Model::rules().
Os controllers REST recebem parâmetros do jeito Laravel padrão:
type-hint de Illuminate\Http\Request no método. Não é
Mad\Http\MadRequest — essa é a classe usada no mount()/ações de um
MadComponent (a camada reativa do admin, com state que sobrevive ao wire). Uma
requisição REST não tem componente nem wire por trás; é um request HTTP isolado, então o
contrato é o Request nativo do Illuminate.
// Mad\Rest\ApiResourceController — assinaturas reais
public function index(Request $request): JsonResponse { /* ... */ }
public function show(Request $request): JsonResponse { /* ... */ }
public function store(Request $request): JsonResponse { /* ... */ }
public function update(Request $request): JsonResponse { /* ... */ }
public function destroy(Request $request): JsonResponse { /* ... */ }
Query params da listagem
index() lê tudo via $request->input(...) — funciona tanto para
query string (GET) quanto para corpo JSON, então o mesmo cliente pode mandar
filtros tanto na URL quanto no body:
// dentro de ApiResourceController::index()
$this->applyFilters($query, (array) $request->input('filters', []));
$this->applyOrder($query, $request); // lê 'sort' e 'direction'
$perPage = (int) $request->input('per_page', $this->perPage); // teto em $maxPerPage
$page = max(1, (int) $request->input('page', 1));
curl -G '/api/import-templates' \
--data-urlencode 'filters={"active":{"=":"Y"},"title":{"like":"Cliente"}}' \
--data-urlencode 'sort=title' \
--data-urlencode 'direction=asc' \
--data-urlencode 'per_page=20'
Só colunas listadas em $searchable (filtros) e $sortable (ordenação) são aceitas — qualquer outra é silenciosamente ignorada, nunca vira SQL arbitrário.
Corpo JSON do write
store()/update() usam $request->all() para pegar o
payload inteiro e então separam as chaves que batem com $details (cada uma vira
um array de linhas da relação) do resto, que é o master:
// dentro de ApiResourceController::persist()
$payload = $request->all();
$detailPayload = [];
foreach ($this->details as $relation) {
if (array_key_exists($relation, $payload)) {
$detailPayload[$relation] = (array) ($payload[$relation] ?? []);
unset($payload[$relation]);
}
}
// $payload agora só tem os campos do master
{
"title": "Clientes",
"database_name": "business",
"mapping_json": "{...}",
"groups": [ { "group_id": 1 }, { "group_id": 2 } ],
"users": [ { "user_id": 7 } ]
}
O {id} da rota — não é route-model binding
resolveId() não usa o binding implícito do Eloquent ({import_template}
não vira automaticamente uma instância de ImportTemplate). Ele pega o
último parâmetro da rota corrente via $request->route()->parameters():
protected function resolveId(Request $request)
{
$params = $request->route() ? $request->route()->parameters() : [];
if ($params) {
return end($params);
}
return $request->input($this->keyName()); // fallback fora de rota (ex.: testes)
}
Por que não Eloquent binding: a base é genérica (não conhece o nome do parâmetro que Route::apiResource gerou) e precisa funcionar igual quando chamada fora de uma rota HTTP de verdade (testes de unidade do controller).
Validação — convenção Model::rules()
Master e cada linha de detalhe são validados antes de qualquer
save() — uma falha em qualquer ponto não deixa meia-gravação:
// dentro de ApiResourceController::persist()
$this->validateData($this->model, $payload, $id);
foreach ($detailPayload as $relation => $rows) {
$detailClass = get_class($this->relation($master, $relation)->getRelated());
foreach ($rows as $row) {
$this->validateData($detailClass, $row, $row[$this->keyOf($detailClass)] ?? null);
}
}
// app/Models/Sys/ImportTemplate.php
public static function rules($id = null): array
{
return [
'title' => 'required|max:200',
'database_name' => 'required',
'mapping_json' => 'required',
];
}
Sem rules() no model, a validação é pulada — o controller não exige a
convenção, mas sem ela qualquer payload passa direto pro fill() +
save() (respeitando só $fillable do model).
Mesmo que o JSON inclua "template_id": 999 dentro de uma linha de
groups, o sync sobrescreve com a FK real
($child->{$foreign} = $master->getKey()) antes de salvar. Não dá pra
"sequestrar" um registro de detalhe que pertence a outro master forjando a FK no
corpo da requisição.
Identidade do chamador sob mad.api
Numa rota atrás de mad.api não existe Auth::user() (é stateless,
sem guard de sessão). Quem foi autenticado chega em atributos do request, populados pelo
MadApiTenantMiddleware:
protected function authorize(string $action, Request $request): void
{
$user = $request->attributes->get('mad_api_user'); // App\Models\Iam\User
$token = $request->attributes->get('mad_api_token'); // App\Models\Api\Token
if ($action === 'destroy' && ! $user?->isAdmin()) {
abort(403);
}
}
O escopo (tenant/unit) não precisa ser lido nem repassado: o middleware já setou
TenantContext/UnitContext, então os models com
BelongsToTenant/BelongsToUnit filtram e carimbam sozinhos, e o
HasMadAudit encontra a identidade na session in-memory. Detalhes em
Autenticação.
Próximos passos
- JSON responses — o que cada ação devolve, incluindo o 422 de validação.
- Exemplos práticos — os mesmos params em requisições curl reais.
- MadRequest — o equivalente desta classe na camada reativa do admin (telas/MadComponent), não usado aqui.