404 Not Found

404 Not Found


nginx

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


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.

BASH
# 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

100%
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

PHP
// 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

PHP
// 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:

TEXT
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

5. Camada de Middleware Personalizado

(1) ▶ Exemplo: A Arquitetura de Middleware do ShopMetrics

PHP
// 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:

TEXT
// Execução bem-sucedida

(2) ▶ Exemplo: Configuração de Rate Limiting do ShopMetrics

PHP
// 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:

TEXT
// Execução bem-sucedida

6. Integração de Broadcasting de Eventos

(1) Eventos de Broadcast

PHP
// 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

JAVASCRIPT
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

8. Exemplo Compreensivo: API Completa da Fase 3 do ShopMetrics

PHP
// ============================================
// 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

P Quanto tempo leva o exercício da Fase 3?
R Cerca de 6-8 horas. Certificação: 1,5 horas; API Resources + Endpoints: 2 horas; Middleware: 1 hora; Broadcasting de Eventos: 1,5 horas; Upload de Arquivos: 1 hora; Testes de Integração: 1 hora. Recomendamos testar com Postman após completar cada módulo.
P Como integrar APIs de front-end e back-end?
R Use Postman ou Newman para testar todos os endpoints primeiro e salve-os como uma Collection. Ao desenvolver o front-end, use o Mock Server do Postman ou conecte diretamente ao back-end. Certifique-se de que o CORS está configurado corretamente.
P Devo usar Reverb ou Pusher para serviços WebSocket?
R O Laravel 11 inclui Reverb (um servidor WebSocket auto-hospedado) por padrão. Reverb é gratuito e rápido para uso em desenvolvimento; para ambientes de produção, escolha entre Reverb (auto-hospedado) ou Pusher (hospedado) dependendo da sua escala.
P Como gerar documentação da API?
R Recomendamos usar Scribe (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.
P Como testar rate limiting?
R Use Postman Runner ou ab (Apache Bench) para enviar requisições que excedam o limite de taxa e verifique se um status 429 e um header Retry-After são retornados. Você também pode escrever um teste PHPUnit para simular rate limiting.
P Como testar uploads S3 durante o desenvolvimento local?
R Use MinIO (um serviço local compatível com S3) como substituto do S3 real: docker run -p 9000:9000 minio/minio server /data. Basta alterar o AWS_URL para apontar para localhost:9000.

📖 Resumo


📝 Exercícios

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

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

  3. Desafio (⭐⭐⭐): Implemente broadcasting em tempo real de alterações de status de pedidos — a requisição PATCH /orders/{id}/status do back-end dispara o evento OrderStatusChanged, o Echo do front-end escuta as atualizações para atualizar a UI e, simultaneamente, registra as requisições API no banco de dados (usando o middleware terminate).

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%