Docs›REST API›Request injection
REST API

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

A FK do detalhe vem da relação, nunca do payload

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.