Requisições e Respostas do Laravel
Requisições e respostas são as "entradas e saídas" do Laravel—dados são extraídos quando uma requisição entra, e dados são empacotados quando uma resposta sai; ambos os processos exigem controle preciso.
1. O Que Você Vai Aprender
- Objeto Request: input/query/has/only/except e Conversão de Tipo
- Processamento de Upload de Arquivos: store/storedAs/Integração S3
- Construtor de Resposta: json/view/download/redirect/stream
- Paginação de Recursos de API: LengthAwarePaginator e Paginação por Cursor
- Padronização de Macros de Resposta e Formatos de Resposta Globais
2. Uma História Real de um Desenvolvedor de API
(1) Problema: Cada endpoint retorna dados em formato diferente
Bob escreveu 10 endpoints para a API do ShopMetrics—alguns retornam {data: [...]}, alguns retornam {shops: [...]}, e outros retornam arrays diretamente. Ao integrar a API no frontend, Alice teve que escrever lógica de análise diferente para cada endpoint. Para piorar, dados paginados eram retornados como total_pages ou last_page, impossibilitando a padronização do componente de paginação do frontend.
(2) Métodos para Resolver Respostas Padronizadas
O API Resource e as macros de resposta do Laravel padronizam o formato de saída—todos os endpoints retornam a mesma estrutura JSON, e dados paginados seguem um formato consistente.
// Resposta padronizada — todo endpoint segue este formato
return ShopResource::collection($shops);
// {
// "data": [...],
// "meta": {"current_page": 1, "total": 50},
// "links": {"next": "...", "prev": "..."}
// }
(3) Resultado
Após Alice adotar um formato unificado, o número de conjuntos de lógica de análise do frontend foi reduzido de 10 para 1, e a taxa de reutilização do componente de paginação atingiu 100%.
3. O Objeto Request
(1) Obter Entrada
// Obter entrada única
$name = $request->input('name');
$name = $request->input('name', 'valor padrão');
// Obter apenas da string de consulta
$sort = $request->query('sort', 'created_at');
// Obter múltiplas entradas
$filtered = $request->only(['name', 'email', 'status']);
$filtered = $request->except(['password', '_token']);
// Verificar existência
if ($request->has('search')) { ... }
if ($request->filled('search')) { ... } // tem + não vazio
if ($request->missing('search')) { ... }
// Conversão de tipo
$page = $request->integer('page', 1);
$active = $request->boolean('active', false);
$date = $request->date('published_at', 'Y-m-d');
| Método | Descrição | Diferença |
|---|---|---|
input() |
Todas as entradas (query+body) | Mais comum |
query() |
Apenas string de consulta | Parâmetros GET |
post() |
Apenas body POST | Dados de formulário |
only() |
Extrair Campos Especificados | Lista Branca |
except() |
Excluir Campos Especificados | Lista Negra |
has() |
Campo existe | Inclui valores null |
filled() |
Campo existe e não está vazio | Exclui valores null |
(1) ▶ Exemplo: Processando Requisições de API do ShopMetrics
// app/Http/Controllers/Api/ShopController.php
public function index(Request $request): JsonResponse
{
$query = Shop::where('tenant_id', tenant()->id);
// Filtro de busca
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
// Filtro de status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Ordenação
$sortField = $request->input('sort', 'created_at');
$sortDir = $request->input('direction', 'desc');
$query->orderBy($sortField, $sortDir);
// Paginação
$perPage = $request->integer('per_page', 15);
$shops = $query->paginate($perPage);
return ShopResource::collection($shops);
}
Saída:
// Execução bem-sucedida
4. Upload de Arquivos
(1) Processamento Básico de Upload
// Validar upload de arquivo
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Armazenar arquivo
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123.jpg"
// Armazenar com nome personalizado
$path = $request->file('logo')->storeAs(
'shops/logos',
$shop->slug . '.' . $request->file('logo')->extension(),
'public',
);
// Obter informações do arquivo
$file = $request->file('logo');
$file->getClientOriginalName();
$file->getClientOriginalExtension();
$file->getSize(); // bytes
$file->getMimeType();
| Método | Descrição |
|---|---|
store() |
Salvar no diretório especificado com nome de arquivo aleatório |
storeAs() |
Salvar no diretório especificado com nome de arquivo personalizado |
storePublicly() |
Salvar no diretório público |
isValid() |
Verificar se o upload foi bem-sucedido |
(1) ▶ Exemplo: Upload do Logo da Loja do ShopMetrics
// app/Http/Controllers/ShopLogoController.php
class ShopLogoController extends Controller
{
public function update(Request $request, Shop $shop): RedirectResponse
{
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Excluir logo antigo se existir
if ($shop->logo_path) {
Storage::disk('public')->delete($shop->logo_path);
}
// Armazenar novo logo
$path = $request->file('logo')->store("shops/{$shop->id}/logos", 'public');
$shop->update(['logo_path' => $path]);
return back()->with('success', 'Logo atualizado.');
}
}
Saída:
// Execução bem-sucedida
5. Construtor de Resposta
(1) Tipo de Resposta
// Resposta JSON
return response()->json(['message' => 'Criado', 'data' => $shop], 201);
// Resposta de View
return response()->view('shops.show', compact('shop'), 200);
// Redirecionamento
return redirect()->route('shops.show', $shop);
return back()->withInput()->withErrors($errors);
return redirect()->away('https://external-site.com');
// Download de arquivo
return response()->download(storage_path('app/reports/report.pdf'));
return response()->streamDownload(function () {
echo generateCsv();
}, 'orders.csv', ['Content-Type' => 'text/csv']);
// Sem conteúdo
return response()->noContent(); // 204
| Tipo de Resposta | Método | Código de Status HTTP |
|---|---|---|
| JSON | response()->json() |
200/201/422 |
| View | response()->view() |
200 |
| Redirecionamento | redirect() |
302 |
| Download | response()->download() |
200 |
| Download Stream | response()->streamDownload() |
200 |
| Sem conteúdo | response()->noContent() |
204 |
(1) ▶ Exemplo: Resposta de Streaming de Exportação CSV do ShopMetrics
// app/Http/Controllers/ExportController.php
class ExportController extends Controller
{
public function exportOrders(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
$csv = fopen('php://output', 'w');
fputcsv($csv, ['ID do Pedido', 'Cliente', 'Total', 'Status', 'Data']);
$shop->orders()
->with('user')
->orderBy('created_at', 'desc')
->chunk(500, function ($orders) use ($csv) {
foreach ($orders as $order) {
fputcsv($csv, [
$order->order_number,
$order->user->name,
$order->total,
$order->status,
$order->created_at->format('Y-m-d'),
]);
}
});
fclose($csv);
}, "orders-{$shop->slug}-" . now()->format('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
}
Saída:
// Execução bem-sucedida
6. Paginação de API
(1) Comparação de Métodos de Paginação
| Método | Número de Consultas | Informação Retornada | Casos de Uso |
|---|---|---|---|
paginate() |
2 (COUNT+SELECT) | total/last_page/links | Número total de páginas necessário |
simplePaginate() |
1 (SELECT) | links next/prev | Número total de páginas não necessário |
cursorPaginate() |
1 (SELECT+WHERE) | cursor next/prev | Grande conjunto de dados |
(2) Paginação em Formato JSON
{
"data": [
{"id": 1, "name": "Alice Store"},
{"id": 2, "name": "Bob Electronics"}
],
"current_page": 1,
"per_page": 15,
"total": 50,
"last_page": 4,
"from": 1,
"to": 15,
"links": {
"first": "/api/v1/shops?page=1",
"last": "/api/v1/shops?page=4",
"next": "/api/v1/shops?page=2",
"prev": null
}
}
(3) Paginação por Cursor
// Mais eficiente para grandes conjuntos de dados
$shops = Shop::cursorPaginate(15);
// Usa parâmetro "cursor" em vez de "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9
// URL da próxima página
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9
(1) ▶ Exemplo: Paginação e Filtragem de API do ShopMetrics
// app/Http/Controllers/Api/OrderController.php
public function index(Request $request): JsonResponse
{
$query = Order::where('tenant_id', tenant()->id)
->with(['shop', 'user', 'items.product']);
// Filtros
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('shop_id')) {
$query->where('shop_id', $request->shop_id);
}
if ($request->filled('date_from')) {
$query->where('created_at', '>=', $request->date('date_from'));
}
if ($request->filled('date_to')) {
$query->where('created_at', '<=', $request->date('date_to'));
}
// Ordenação
$query->orderBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_dir', 'desc'),
);
// Escolher estratégia de paginação
$perPage = $request->integer('per_page', 15);
$orders = $request->boolean('cursor', false)
? $query->cursorPaginate($perPage)
: $query->paginate($perPage);
return OrderResource::collection($orders);
}
Saída:
// Execução bem-sucedida
7. Macros de Resposta e Padronização de Formato
(1) Definir uma macro de resposta
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Response::macro('apiSuccess', function (mixed $data, string $message = 'Sucesso', int $status = 200) {
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
});
Response::macro('apiError', function (string $message, int $status = 400, array $errors = []) {
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $status);
});
}
(2) Usando Macros de Resposta
// No controlador
return response()->apiSuccess($shop, 'Loja criada', 201);
return response()->apiError('Loja não encontrada', 404);
return response()->apiError('Validação falhou', 422, $validator->errors()->toArray());
(1) ▶ Exemplo: Formato de Resposta Padrão da API do ShopMetrics
// app/Traits/ApiResponse.php
trait ApiResponse
{
protected function success(mixed $data, string $message = 'Sucesso', int $status = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => now()->toISOString(),
], $status);
}
protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
'timestamp' => now()->toISOString(),
], $status);
}
protected function paginated($resource, string $message = 'Sucesso'): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $resource->resolve(),
'meta' => [
'current_page' => $resource->resource->currentPage(),
'per_page' => $resource->resource->perPage(),
'total' => $resource->resource->total(),
'last_page' => $resource->resource->lastPage(),
],
'links' => [
'first' => $resource->resource->url(1),
'last' => $resource->resource->url($resource->resource->lastPage()),
'next' => $resource->resource->nextPageUrl(),
'prev' => $resource->resource->previousPageUrl(),
],
]);
}
}
Saída:
// Execução bem-sucedida
8. Exemplo Completo: O Processo Completo de Tratamento de Requisições de API do ShopMetrics
// ============================================
// Completo: Requisição-Resposta de API do ShopMetrics
// Abrange: tratamento de entrada, upload de arquivo, paginação, formato de resposta
// ============================================
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$query = Product::where('shop_id', $request->shop_id)
->with('categories');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
if ($request->filled('category')) {
$query->whereHas('categories', fn ($q) => $q->where('slug', $request->category));
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->float('min_price'));
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->float('max_price'));
}
if ($request->filled('in_stock') && $request->boolean('in_stock')) {
$query->where('stock', '>', 0);
}
$query->orderBy(
$request->input('sort', 'created_at'),
$request->input('direction', 'desc'),
);
$products = $query->paginate($request->integer('per_page', 15));
return $this->paginated(ProductResource::collection($products));
}
public function store(StoreProductRequest $request): JsonResponse
{
$product = Product::create($request->validated());
if ($request->hasFile('image')) {
$path = $request->file('image')->store("products/{$product->id}", 's3');
$product->update(['image_path' => $path]);
}
return $this->success(new ProductResource($product), 'Produto criado', 201);
}
public function export(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
echo ProductExporter::toCsv($shop);
}, "products-{$shop->slug}.csv", ['Content-Type' => 'text/csv']);
}
}
❓ Perguntas Frequentes
input() e query()?input() recupera dados de todas as fontes (string de consulta + corpo da requisição), enquanto query() recupera dados apenas da string de consulta da URL. É mais claro usar input() para dados de formulário POST e query() para parâmetros GET.cursorPaginate e paginate?paginate para conjuntos de dados menores que 100.000 (requer exibição do número total de páginas); use cursorPaginate para conjuntos de dados maiores que 100.000 (sem consulta COUNT necessária, melhor desempenho). UI de rolagem infinita é mais adequada para cursor.ApiResponse ou macro de resposta para encapsular os métodos success, error e paginated. Todos os controladores devem usar esses métodos de resposta padronizados para garantir consistência em nomes de campos, códigos de status e formatos de paginação.download() ou streamDownload() para baixar arquivos grandes?download() para lê-los diretamente na memória; para arquivos grandes, use streamDownload() para streaming, que mantém o uso de memória constante. streamDownload() deve ser usado para cenários como exportação CSV.📖 Resumo
- O objeto Request fornece métodos como input, query, only e except para recuperar entrada
- Use
store()oustoreAs()para salvar arquivos carregados em um local de disco especificado - Response suporta múltiplos tipos: json/view/redirect/download/stream
- Há três estratégias de paginação para a API: paginate, simplePaginate e cursorPaginate
- Adote um formato de saída de API unificado por macros para evitar inconsistências no formato de cada endpoint
- Use
streamDownloadpara streaming de exportação de arquivos grandes para prevenir overflow de memória
📝 Exercícios
-
Exercício Básico (⭐): Implemente o endpoint GET /api/v1/shops para a API do ShopMetrics, suportando os parâmetros de filtro search, status, sort e direction, e use paginate para retornar JSON em blocos paginados.
-
Exercício Avançado (⭐⭐): Crie um trait
ApiResponseque encapsule os métodossuccess,errorepaginated, e use-o em todos os controladores de API para garantir um formato de resposta consistente. -
Desafio (⭐⭐⭐): Implemente uma API de lista de pedidos paginada por cursor (GET /api/v1/orders?cursor=xxx), e compare as diferenças de desempenho de consulta entre
paginateecursorPaginateao processar 100.000 registros.



