Exercício Compreensivo da Fase 3 — ShopMetrics API e Notificações em Tempo Real
O Exercício Compreensivo da Fase 3 é "Entregando Funcionalidades Avançadas" — integrando autenticação, APIs, middleware, eventos e armazenamento para construir uma API de nível production.
1. O Que Você Vai Aprender
- Processo Completo de Autenticação API com Sanctum Token
- 10+ endpoints de recursos API: CRUD de Tenant/Product/Order/Subscription
- Middleware personalizado: TenantResolver/RateLimiter/CorsHandler
- Broadcasting de eventos de pedido: Notificações push WebSocket em tempo real para Alice, Bob e Charlie
- Upload de Imagens de Produto para S3 e Download de URLs Pré-assinadas
2. A História de Aceitação da Fase 3 da Alice
(1) Dor: O material das seis aulas está fragmentado e não pode ser montado em uma API completa
Após concluir a Fase 3 — Sanctum na Aula 15, Resources na Aula 16 e Middleware na Aula 17 — Alice não sabia como juntar tudo em uma API completa. Bob pediu que ela escrevesse uma API RESTful, e ela descobriu que funcionalidades como autenticação, conversão de recursos, middleware, broadcasting de eventos e upload de arquivos precisavam trabalhar juntos. Ela entendia cada um individualmente, mas quando combinados, tudo ficava caótico.
(2) Soluções para os Exercícios Compreensivos
Nesta aula, vamos construir uma API RESTful completa do zero — com cada endpoint featuring autenticação, isolamento de tenant, rate limiting, mapeamento de recursos e broadcasting de eventos — para entregar finalmente um sistema API de nível production.
# Entregável da Fase 3: API RESTful completa com funcionalidades em tempo real
php artisan migrate:fresh --seed
php artisan queue:work &
php artisan storage:link
# → 10+ endpoints, auth, rate limiting, broadcasting todos funcionando
(3) Resultado
Depois que Alice terminou, ela tinha uma API ShopMetrics completa — totalmente integrada da autenticação ao broadcasting — que poderia ser entregue diretamente à equipe de front-end.
3. Camada de Autenticação API
(1) Processo de Autenticação com Sanctum Token
flowchart TD
A[Cliente] --> B["POST /api/auth/login<br/>(email+senha)"]
B --> C["Sanctum cria Token"]
C --> D["Retorna plainTextToken"]
D --> E["Cliente armazena Token"]
E --> F["Requisições API com<br/>Authorization: Bearer {token}"]
F --> G["Middleware Sanctum<br/>resolve User"]
G --> H[Middleware TenantResolve]
H --> I[Controller]
(2) Endpoint de Autenticação
// routes/api.php
Route::prefix('auth')->group(function () {
Route::post('/register', [Auth\RegisterController::class, 'register']);
Route::post('/login', [Auth\ApiTokenController::class, 'login']);
Route::post('/forgot-password', [Auth\PasswordResetController::class, 'sendResetLink']);
Route::post('/reset-password', [Auth\PasswordResetController::class, 'reset']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $r) => new UserResource($r->user()));
Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
Route::apiResource('tokens', Auth\TokenController::class)->only(['store', 'index', 'destroy']);
});
});
| Endpoint | Método | Autenticação | Descrição |
|---|---|---|---|
| /auth/register | POST | ❌ | Registrar |
| /auth/login | POST | ❌ | Obter Token |
| /auth/user | GET | ✅ | Usuário Atual |
| /auth/logout | POST | ✅ | Revogar Token |
| /auth/tokens | POST | ✅ | Criar novo token |
| /auth/tokens | GET | ✅ | Listar Tokens |
| /auth/tokens/{id} | DELETE | ✅ | Excluir Token |
(1) ▶ Exemplo: Implementação Completa da Autenticação Token do ShopMetrics
// app/Http/Controllers/Auth/ApiTokenController.php
class ApiTokenController extends Controller
{
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
'device_name' => 'sometimes|string|max:255',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['Credenciais inválidas.'],
]);
}
if (!$user->tenant_id || $user->tenant->status !== 'active') {
throw ValidationException::withMessages([
'email' => ['Conta não está ativa.'],
]);
}
$abilities = match ($user->role) {
'super_admin' => ['*'],
'tenant_owner' => ['read', 'write', 'manage-users'],
'analyst' => ['read'],
default => [],
};
$token = $user->createToken(
$request->device_name ?? 'api-token',
$abilities,
);
return response()->json([
'user' => new UserResource($user),
'token' => $token->plainTextToken,
'abilities' => $abilities,
]);
}
public function logout(Request $request): JsonResponse
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Token revogado.']);
}
}
Saída:
// Execução bem-sucedida
4. Endpoints de Recursos API
(1) Lista Completa de Endpoints
| Endpoint | Método | Resource | Middleware |
|---|---|---|---|
| /v1/shops | GET | ShopResource::collection | auth,tenant,throttle |
| /v1/shops | POST | ShopResource | auth,tenant,throttle,role:owner |
| /v1/shops/{id} | GET | ShopResource | auth,tenant |
| /v1/shops/{id} | PUT | ShopResource | auth,tenant,role:owner |
| /v1/shops/{id} | DELETE | - | auth,tenant,role:owner |
| /v1/shops/{id}/products | GET | ProductResource::collection | auth,tenant |
| /v1/products | POST | ProductResource | auth,tenant,role:owner |
| /v1/products/{id} | GET | ProductResource | auth,tenant |
| /v1/products/{id} | PUT | ProductResource | auth,tenant,role:owner |
| /v1/orders | GET | OrderResource::collection | auth,tenant |
| /v1/orders/{id} | GET | OrderResource | auth,tenant |
| /v1/orders/{id}/status | PATCH | OrderResource | auth,tenant,role:owner |
| /v1/analytics/overview | GET | AnalyticsResource | auth,tenant,ability:read |
| /v1/reports/generate | POST | ReportResource | auth,tenant,ability:write |
(2) Definição de Rotas
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api'])
->prefix('v1')->name('api.v1.')->group(function () {
// Lojas
Route::apiResource('shops', Api\V1\ShopController::class);
Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class)->name('shops.logo');
// Produtos
Route::apiResource('products', Api\V1\ProductController::class);
// Pedidos
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);
// Analytics e Relatórios
Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
Route::post('reports/generate', [Api\V1\ReportController::class, 'generate']);
// Mídia
Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
});
(1) ▶ Exemplo: Endpoint da API de Pedidos do ShopMetrics
// app/Http/Controllers/Api/V1/OrderController.php
class OrderController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Order::where('tenant_id', tenant()->id)
->with(['shop', 'user'])
->withCount('items');
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'));
}
$orders = $query->latest()->paginate($request->integer('per_page', 15));
return OrderResource::collection($orders);
}
public function show(Order $order): JsonResponse
{
$this->authorize('view', $order);
$order->load(['items.product', 'shop', 'user']);
return new OrderResource($order);
}
public function updateStatus(Request $request, Order $order): JsonResponse
{
$this->authorize('update', $order);
$validated = $request->validate([
'status' => 'required|in:processing,completed,cancelled,refunded',
]);
$oldStatus = $order->status;
$order->update(['status' => $validated['status']]);
event(new OrderStatusChanged($order, $oldStatus, $validated['status']));
return new OrderResource($order->fresh());
}
}
Saída:
// Execução bem-sucedida
5. Camada de Middleware Personalizado
(1) ▶ Exemplo: A Arquitetura de Middleware do ShopMetrics
// app/Http/Middleware/TenantResolve.php
class TenantResolve
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user?->tenant_id) abort(403, 'Sem tenant.');
$tenant = $user->tenant;
if ($tenant->status !== 'active') abort(403, 'Tenant inativo.');
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}
// app/Http/Middleware/CheckAbility.php
class CheckAbility
{
public function handle(Request $request, Closure $next, string $ability): Response
{
if ($request->user()->tokenCan('*') || $request->user()->tokenCan($ability)) {
return $next($request);
}
abort(403, "Ability ausente: {$ability}");
}
}
// app/Http/Middleware/EnsureSubscriptionActive.php
class EnsureSubscriptionActive
{
public function handle(Request $request, Closure $next): Response
{
$tenant = app()->make(Tenant::class);
if (!$tenant->subscription?->isActive()) {
abort(402, 'Assinatura ativa necessária.');
}
return $next($request);
}
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Configuração de Rate Limiting do ShopMetrics
// app/Providers/AppServiceProvider.php
public function boot(): void
{
RateLimiter::for('tenant-api', function (Request $request) {
$tenant = app()->make(Tenant::class);
$plan = $tenant->plan ?? null;
return Limit::perMinute(match ($plan->slug ?? 'starter') {
'enterprise' => 600,
'pro' => 120,
default => 60,
})->by($tenant->id);
});
}
Saída:
// Execução bem-sucedida
6. Integração de Broadcasting de Eventos
(1) Eventos de Broadcast
// app/Events/OrderPlaced.php
class OrderPlaced implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel('tenant.' . $this->order->tenant_id)];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'total' => (float) $this->order->total,
'shop_name' => $this->order->shop->name,
];
}
public function broadcastAs(): string { return 'order.placed'; }
}
(2) Escuta Echo no Front-end
// resources/js/app.js
window.Echo.private(`tenant.${tenantId}`)
.listen('.order.placed', (e) => {
showNotification(`Novo pedido: ${e.order_number} ($${e.total})`);
})
.listen('.order.status_changed', (e) => {
updateOrderRow(e.order_id, e.new_status);
});
7. Integração de Upload de Arquivos S3
(1) ▶ Exemplo: Endpoint de Upload de Imagem do ShopMetrics
// app/Http/Controllers/Api/V1/ShopLogoController.php
class ShopLogoController extends Controller
{
public function __invoke(Request $request, Shop $shop): JsonResponse
{
$this->authorize('update', $shop);
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
if ($shop->logo_path) {
Storage::disk('s3')->delete($shop->logo_path);
}
$path = $request->file('logo')->store(
"shops/{$shop->id}/logos",
's3',
);
$shop->update(['logo_path' => $path]);
return response()->json([
'message' => 'Logo enviado.',
'logo_url' => Storage::disk('s3')->url($path),
]);
}
}
// app/Http/Controllers/Api/V1/MediaController.php
class MediaController extends Controller
{
public function presign(Request $request): JsonResponse
{
$validated = $request->validate([
'filename' => 'required|string',
'mime_type' => 'required|in:image/jpeg,image/png,image/webp',
]);
$path = 'uploads/' . tenant()->id . '/' . Str::uuid() . '/' . $validated['filename'];
$url = Storage::disk('s3')->temporaryUploadUrl($path, now()->addMinutes(30));
return response()->json(['upload_url' => $url, 'path' => $path]);
}
}
Saída:
// Execução bem-sucedida
8. Exemplo Compreensivo: API Completa da Fase 3 do ShopMetrics
// ============================================
// Compreensivo: API Completa da Fase 3 do ShopMetrics
// Abrange: auth, resources, middleware, events, storage
// ============================================
// routes/api.php — Rotas API completas
Route::prefix('auth')->group(function () {
Route::post('/register', [Auth\RegisterController::class, 'register']);
Route::post('/login', [Auth\ApiTokenController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $r) => new UserResource($r->user()));
Route::post('/logout', [Auth\ApiTokenController::class, 'logout']);
});
});
Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api', 'subscription.active'])
->prefix('v1')->group(function () {
Route::apiResource('shops', Api\V1\ShopController::class);
Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class);
Route::apiResource('products', Api\V1\ProductController::class);
Route::post('products/{product}/images', Api\V1\ProductImageController::class);
Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);
Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview'])
->middleware('ability:read');
Route::post('reports/generate', [Api\V1\ReportController::class, 'generate'])
->middleware('ability:write');
Route::post('media/upload', [Api\V1\MediaController::class, 'upload']);
Route::post('media/presign', [Api\V1\MediaController::class, 'presign']);
Route::get('media/{media}/download', [Api\V1\MediaController::class, 'download']);
});
// routes/channels.php
Broadcast::channel('tenant.{tenantId}', fn ($user, $tenantId) => $user->tenant_id === (int) $tenantId);
// Configuração de broadcasting
// BROADCAST_CONNECTION=redis
// QUEUE_CONNECTION=redis
// Executar: php artisan queue:work
// Executar: php artisan reverb:start (servidor WebSocket integrado do Laravel 11)
❓ Perguntas Frequentes
knuckleswtf/scribe) para gerar automaticamente a documentação OpenAPI a partir do seu código; isso é menos propenso a ficar desatualizado do que escrever manualmente no Postman. Basta adicionar comentários PHPDoc a cada método do controller.docker run -p 9000:9000 minio/minio server /data. Basta alterar o AWS_URL para apontar para localhost:9000.📖 Resumo
- Sanctum Token fornece autenticação stateless para APIs
- Formato de saída JSON padronizado para API Resources; associações aninhadas usando a condição
whenLoaded - Stack de middleware: CORS → TenantResolve → Auth → Throttle → Ability
- Broadcasting de eventos permite notificações push em tempo real de alterações de pedidos para o front-end
- Uploads de arquivos S3 suportam uploads diretos (URLs pré-assinadas) e uploads mediados pelo servidor
- Ao concluir a Fase 3, você terá uma API RESTful pronta para produção
📝 Exercícios
-
Exercício Básico (⭐): Configure a camada de autenticação API do ShopMetrics, implemente os três endpoints (register, login e logout) e use Postman para testar a obtenção de um token e o acesso a endpoints protegidos.
-
Exercício Avançado (⭐⭐): Implemente uma API CRUD completa de Shop + Product, incluindo conversão de recursos, isolamento TenantResolve e rate limiting baseado em assinatura. Teste todos os endpoints usando uma Postman Collection.
-
Desafio (⭐⭐⭐): Implemente broadcasting em tempo real de alterações de status de pedidos — a requisição
PATCH /orders/{id}/statusdo back-end dispara o eventoOrderStatusChanged, oEchodo front-end escuta as atualizações para atualizar a UI e, simultaneamente, registra as requisições API no banco de dados (usando o middlewareterminate).



