Docs›REST API›Exemplos práticos
REST API

Exemplos práticos

CRUD mestre-detalhe completo via REST: import-templates com sessão+CSRF e o mesmo recurso via Bearer mad_api_*.

Dois exemplos: um mínimo (mestre + dois detalhes, direto da documentação do próprio ApiResourceController) para ver a forma geral, e o walkthrough completo com ImportTemplateApiController — um controller real deste repositório, com requisições curl reais contra suas rotas.

Mínimo — model, controller, rota

// app/Models/Order.php
class Order extends Model
{
    protected $fillable = ['status', 'total', 'customer_id'];

    public function items(): HasMany    { return $this->hasMany(OrderItem::class, 'order_id'); }
    public function payments(): HasMany { return $this->hasMany(Payment::class, 'order_id'); }
    public function customer(): BelongsTo { return $this->belongsTo(Customer::class, 'customer_id'); }

    public static function rules($id = null): array
    {
        return ['status' => 'required', 'total' => 'required|numeric'];
    }
}
// app/Http/Controllers/OrderApiController.php
use Mad\Rest\ApiResourceController;

class OrderApiController extends ApiResourceController
{
    protected string $model      = \App\Models\Order::class;
    protected array  $searchable = ['status', 'customer_id'];
    protected array  $sortable   = ['created_at', 'total'];
    protected array  $with       = ['customer'];
    protected array  $details    = ['items', 'payments'];   // ambos hasMany
    protected ?string $defaultOrder = 'created_at desc';
}
// routes/modules/seu-modulo.php
Route::middleware('mad.auth')->prefix('api')->group(function () {
    Route::apiResource('orders', OrderApiController::class);
});

Walkthrough completo — import-templates (real, neste repositório)

Template de importação de dados: um master (title, database_name, mapping_json...) com dois detalhes — grupos e usuários que enxergam o template. Model real (app/Models/Sys/ImportTemplate.php):

class ImportTemplate extends Model
{
    use HasIdPolicy, HasMadAudit, HasMadSoftDeletes;

    protected $connection = 'iam';
    protected $table      = 'mad_sys_import_template';

    protected $fillable = [
        'title', 'code', 'description', 'database_name', 'mapping_json',
        'csv_model_path', 'import_mode', 'active', 'created_by', 'created_at', 'updated_at',
    ];

    public static function rules($id = null): array
    {
        return [
            'title'         => 'required|max:200',
            'database_name' => 'required',
            'mapping_json'  => 'required',
        ];
    }

    public function groups(): HasMany { return $this->hasMany(ImportTemplateGroup::class, 'template_id'); }
    public function users(): HasMany  { return $this->hasMany(ImportTemplateUser::class, 'template_id'); }
    public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); }
}

Controller (app/Http/Controllers/Sys/ImportTemplateApiController.php) — só configuração:

class ImportTemplateApiController extends ApiResourceController
{
    protected string $model = ImportTemplate::class;

    protected array $searchable = ['title', 'code', 'database_name', 'active', 'created_by'];
    protected array $sortable   = ['title', 'code', 'created_at', 'updated_at'];

    protected array $with      = ['creator'];
    protected array $withCount = ['groups', 'users', 'logs'];
    protected array $details   = ['groups', 'users'];

    protected ?string $defaultOrder = 'title asc';
}

Rota (routes/modules/admin.php):

Route::middleware('mad.auth')
    ->prefix('api')
    ->group(function () {
        Route::apiResource('import-templates', \App\Http\Controllers\Sys\ImportTemplateApiController::class);
    });

1. Listar com filtro, ordenação e paginação

curl -G 'https://seu-app.test/api/import-templates' \
  -b cookies.txt -H 'X-CSRF-TOKEN: '"$TOKEN" \
  --data-urlencode 'filters={"active":{"=":"Y"}}' \
  --data-urlencode 'sort=title' --data-urlencode 'direction=asc' \
  --data-urlencode 'per_page=10'
200 OK
{
  "data": [
    { "id": 3, "title": "Clientes", "groups_count": 2, "users_count": 1, "creator": {"id": 7, "name": "Admin"} }
  ],
  "meta": { "total": 1, "per_page": 10, "current_page": 1, "last_page": 1 }
}

2. Criar com mestre-detalhe

curl -X POST 'https://seu-app.test/api/import-templates' \
  -b cookies.txt -H 'X-CSRF-TOKEN: '"$TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "title": "Clientes",
    "database_name": "business",
    "mapping_json": "{\"nome\":\"A\",\"email\":\"B\"}",
    "groups": [ { "group_id": 1 }, { "group_id": 3 } ],
    "users":  [ { "user_id": 7 } ]
  }'
201 Created
{
  "id": 42,
  "title": "Clientes",
  "database_name": "business",
  "groups": [ { "id": 101, "template_id": 42, "group_id": 1 }, { "id": 102, "template_id": 42, "group_id": 3 } ],
  "users":  [ { "id": 55, "template_id": 42, "user_id": 7 } ]
}

3. Atualizar — sync sobrescreve os detalhes (cria/atualiza/apaga)

Mande de volta SÓ as linhas que devem sobreviver. Uma linha sem id é criada; com id existente é atualizada; qualquer id que estava no banco e não veio no payload é apagada (diff, não merge):

curl -X PUT 'https://seu-app.test/api/import-templates/42' \
  -b cookies.txt -H 'X-CSRF-TOKEN: '"$TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "title": "Clientes (ativos)",
    "database_name": "business",
    "mapping_json": "{\"nome\":\"A\",\"email\":\"B\"}",
    "groups": [ { "id": 101, "group_id": 1 } ],
    "users":  [ { "user_id": 7 }, { "user_id": 9 } ]
  }'

Resultado: o grupo id 102 (não veio no payload) é apagado; o grupo id 101 é atualizado; em users, a linha existente (user_id: 7) some do payload com id mas como ImportTemplateUser não tem coluna única além do id, duas linhas sem id = duas linhas novas — o padrão real para "substituir pelo conjunto inteiro" é sempre mandar os ids que devem persistir.

4. Erro de validação

curl -X POST 'https://seu-app.test/api/import-templates' \
  -b cookies.txt -H 'X-CSRF-TOKEN: '"$TOKEN" -H 'Content-Type: application/json' \
  -d '{ "title": "" }'
422 Unprocessable Content
{
  "message": "The title field is required. (and 2 more errors)",
  "errors": {
    "title": ["The title field is required."],
    "database_name": ["The database name field is required."],
    "mapping_json": ["The mapping json field is required."]
  }
}

5. Apagar (cascata para os detalhes)

curl -X DELETE 'https://seu-app.test/api/import-templates/42' \
  -b cookies.txt -H 'X-CSRF-TOKEN: '"$TOKEN"
200 OK
{ "message": "Resource deleted successfully" }
Por que os curls levam cookie + CSRF

Estas rotas estão atrás de mad.auth (sessão), dentro do grupo web padrão — não de um guard stateless. Sem cookie de sessão válido, todo request acima volta 401 antes mesmo de chegar no controller; sem X-CSRF-TOKEN, toda escrita (POST/PUT/ DELETE) volta 419 do ValidateCsrfToken nativo. Detalhe completo em Autenticação.

O mesmo recurso, sem sessão: mad.api + Bearer

Para um consumidor externo (script, job, outro sistema) o controller é exatamente o mesmo — muda a declaração da rota e some todo o ritual de cookie/CSRF:

// routes/api.php — prefixo /api vem do bootstrap; grupo `api` (sem sessão, sem CSRF)
Route::middleware('mad.api:import-templates.read')
    ->get('/import-templates', [ImportTemplateApiController::class, 'index']);

Route::middleware('mad.api:import-templates.write')
    ->post('/import-templates', [ImportTemplateApiController::class, 'store']);

Emita o token com escopo user + unit e as abilities correspondentes:

php artisan mad:api-token admin 1 \
  --name="integracao ERP X" \
  --abilities="import-templates.read,import-templates.write"
# imprime o token em claro UMA vez: mad_api_3f9c...
TOKEN='mad_api_3f9c...'

curl -G 'https://app.exemplo.com/api/import-templates' \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'filters={"active":{"=":"Y"}}' \
  --data-urlencode 'per_page=20'

curl -X POST 'https://app.exemplo.com/api/import-templates' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Clientes CSV","code":"cli_csv","active":"Y"}'

Nenhum -b cookies.txt, nenhum X-CSRF-TOKEN. Em compensação: o escopo de tenant/unit não é escolhido pelo cliente — vem carimbado no token, e o middleware o revalida a cada requisição. Token sem a ability da rota volta 403, mesmo sendo válido.

Próximos passos