404 Not Found

404 Not Found


nginx

Phase 3 Comprehensive Exercise—ShopMetrics API and Real-Time Notifications

Phase 3 Comprehensive Exercise is "Delivering Advanced Features"—integrating authentication, APIs, middleware, events, and storage to build a production-grade API.

1. What You'll Learn


2. Alice's Phase 3 Acceptance Story

(1) Pain Point: The material from the six lessons is fragmented and cannot be assembled into a complete API

After completing Phase 3—Sanctum in Lesson 15, Resources in Lesson 16, and Middleware in Lesson 17—Alice didn't know how to put them all together into a complete API. Bob asked her to write a RESTful API, and she discovered that features like authentication, resource conversion, middleware, event broadcasting, and file uploads needed to work together. She understood each one individually, but when combined, everything became chaotic.

(2) Solutions to the Comprehensive Exercises

In this lesson, we'll build a complete RESTful API from scratch—with each endpoint featuring authentication, tenant isolation, rate limiting, resource mapping, and event broadcasting—to ultimately deliver a production-grade API system.

BASH
# Phase 3 deliverable: Complete RESTful API with real-time features
php artisan migrate:fresh --seed
php artisan queue:work &
php artisan storage:link
# → 10+ endpoints, auth, rate limiting, broadcasting all working

(3) Revenue

Once Alice finished, she had a complete ShopMetrics API—fully integrated from authentication to broadcasting—that could be delivered directly to the front-end team.


3. API Authentication Layer

(1) Sanctum Token Authentication Process

100%
flowchart TD
    A[Client] --> B["POST /api/auth/login<br/>(email+password)"]
    B --> C["Sanctum creates Token"]
    C --> D["Return plainTextToken"]
    D --> E["Client stores Token"]
    E --> F["API requests with<br/>Authorization: Bearer {token}"]
    F --> G["Sanctum middleware<br/>resolves User"]
    G --> H[TenantResolve middleware]
    H --> I[Controller]

(2) Authentication Endpoint

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 Method Authentication Description
/auth/register POST Register
/auth/login POST Get Token
/auth/user GET Current User
/auth/logout POST Revoke Token
/auth/tokens POST Create a new token
/auth/tokens GET List Tokens
/auth/tokens/{id} DELETE Delete Token

(1) ▶ Example: Complete Implementation of ShopMetrics Token Authentication

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' => ['Invalid credentials.'],
            ]);
        }

        if (!$user->tenant_id || $user->tenant->status !== 'active') {
            throw ValidationException::withMessages([
                'email' => ['Account is not active.'],
            ]);
        }

        $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 revoked.']);
    }
}

Output:

TEXT
// Execution Successful

4. API Resource Endpoints

(1) Complete List of Endpoints

Endpoint Method 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) Route Definitions

PHP
// routes/api.php
Route::middleware(['auth:sanctum', 'tenant.resolve', 'throttle:tenant-api'])
    ->prefix('v1')->name('api.v1.')->group(function () {
        // Shops
        Route::apiResource('shops', Api\V1\ShopController::class);
        Route::post('shops/{shop}/logo', Api\V1\ShopLogoController::class)->name('shops.logo');

        // Products
        Route::apiResource('products', Api\V1\ProductController::class);

        // Orders
        Route::apiResource('orders', Api\V1\OrderController::class)->only(['index', 'show']);
        Route::patch('orders/{order}/status', [Api\V1\OrderController::class, 'updateStatus']);

        // Analytics & Reports
        Route::get('analytics/overview', [Api\V1\AnalyticsController::class, 'overview']);
        Route::post('reports/generate', [Api\V1\ReportController::class, 'generate']);

        // Media
        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) ▶ Example: ShopMetrics Order API Endpoint

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());
    }
}

Output:

TEXT
// Execution Successful

5. Custom Middleware Layer

(1) ▶ Example: The ShopMetrics Middleware Architecture

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, 'No tenant.');
        $tenant = $user->tenant;
        if ($tenant->status !== 'active') abort(403, 'Tenant inactive.');
        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, "Missing ability: {$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, 'Active subscription required.');
        }
        return $next($request);
    }
}

Output:

TEXT
// Execution Successful

(2) ▶ Example: ShopMetrics Rate Limiting Configuration

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);
    });
}

Output:

TEXT
// Execution Successful

6. Event Broadcasting Integration

(1) Broadcast Events

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) Front-end Echo Listening

JAVASCRIPT
// resources/js/app.js
window.Echo.private(`tenant.${tenantId}`)
    .listen('.order.placed', (e) => {
        showNotification(`New order: ${e.order_number} ($${e.total})`);
    })
    .listen('.order.status_changed', (e) => {
        updateOrderRow(e.order_id, e.new_status);
    });

7. S3 File Upload Integration

(1) ▶ Example: ShopMetrics Image Upload Endpoint

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 uploaded.',
            '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]);
    }
}

Output:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Phase 3 Complete API

PHP
// ============================================
// Comprehensive: ShopMetrics Phase 3 Complete API
// Covers: auth, resources, middleware, events, storage
// ============================================

// routes/api.php — Complete API routes
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);

// Broadcasting configuration
// BROADCAST_CONNECTION=redis
// QUEUE_CONNECTION=redis
// Run: php artisan queue:work
// Run: php artisan reverb:start (Laravel 11 built-in WebSocket server)

❓ FAQ

Q How long does the Phase 3 exercise take?
A About 6-8 hours. Certification: 1.5 hours; API Resources + Endpoints: 2 hours; Middleware: 1 hour; Event Broadcasting: 1.5 hours; File Upload: 1 hour; Integration Testing: 1 hour. We recommend testing with Postman after completing each module.
Q How do I integrate front-end and back-end APIs?
A Use Postman or Newman to test all endpoints first and save them as a Collection. When developing the front end, use Postman's Mock Server or connect directly to the back end. Make sure CORS is configured correctly.
Q Should I use Reverb or Pusher for WebSocket services?
A Laravel 11 includes Reverb (a self-hosted WebSocket server) by default. Reverb is free and quick to use for development; for production environments, choose between Reverb (self-hosted) or Pusher (hosted) depending on your scale.
Q How do I generate API documentation?
A We recommend using Scribe (knuckleswtf/scribe) to automatically generate OpenAPI documentation from your code; this is less prone to becoming outdated than manually writing it in Postman. Simply add PHPDoc comments to each controller method.
Q How do you test rate limiting?
A Use Postman Runner or ab (Apache Bench) to send requests that exceed the rate limit, and verify that a 429 status code and a Retry-After header are returned. You can also write a PHPUnit test to simulate rate limiting.
Q How can I test S3 uploads during local development?
A Use MinIO (an S3-compatible local service) as a substitute for the actual S3: docker run -p 9000:9000 minio/minio server /data. Simply change the AWS_URL to point to localhost:9000.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Set up the ShopMetrics API authentication layer, implement the three endpoints (register, login, and logout), and use Postman to test retrieving a token and accessing protected endpoints.

  2. Advanced Exercise (⭐⭐): Implement a complete Shop + Product CRUD API, including resource conversion, TenantResolve isolation, and subscription-based rate limiting. Test all endpoints using a Postman Collection.

  3. Challenge (⭐⭐⭐): Implement real-time broadcasting of order status changes—the backend's PATCH /orders/{id}/status request triggers the OrderStatusChanged event, the frontend's Echo listens for updates to refresh the UI, and simultaneously logs API requests to the database (using the terminate middleware).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏