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" }
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
- Declarando rotas — todas as propriedades de configuração do controller.
- JSON responses — o formato exato de cada resposta acima.
- Autenticação —
mad.api(Bearer),mad.auth+ CSRF, e a alternativa HMAC. - Tokens de API e abilities — ciclo de vida do token
mad_api_*.