404 Not Found

404 Not Found


nginx

Project Development — Coding Implementation of the ShopMetrics SaaS Platform

The design diagrams have finally been turned into code—but don't rush. Implement them one module at a time, and test each module as you finish it.

1. What You'll Learn


2. A True Story of Collaborative Development

(1) Pain Point: Three people are each writing their own code, which leads to conflicts when merging the code.

Bob assigned tasks: Alice was to write the tenant management code, and Charlie was to write the data analysis code. Three days later, when they merged the code, the relationships between Alice's Tenant model and Charlie's Order model didn't align, and their middleware logic conflicted with each other, resulting in 500 errors across the entire site after the merge. Bob said, "We need a development process that ensures everyone's code can run independently."

(2) Solutions for Modular Development

Develop in the following order of priority: Authentication → Tenant → Store → Orders → Analytics → Billing. After completing each module, run tests and merge the code to ensure the main branch is always in a runnable state.

TEXT
Week 1: Auth + Tenant + Middleware (Basic Framework)
Week 2: Shop + Order + Product (Core Business)
Week 3: Dashboard + Analytics (Data Analysis)
Week 4: Subscription + Stripe (Billing System)

(3) Revenue

After adopting modular development, the three developers merged their code once a day, reducing the conflict rate from 40% to 5%, and the main branch remained consistently runnable.


3. Multi-tenant Middleware and Scope

(1) Request Processing Workflow

100%
sequenceDiagram
    participant Client
    participant Nginx
    participant Middleware
    participant TenantScope
    participant Controller
    participant DB

    Client->>Nginx: GET alice.shopmetrics.io/api/v1/shops
    Nginx->>Middleware: X-Tenant: alice
    Middleware->>DB: SELECT * FROM tenants WHERE slug=alice
    DB-->>Middleware: Tenant{id:1, slug:alice}
    Middleware->>TenantScope: Set current tenant
    Middleware->>Controller: Request with tenant context
    Controller->>DB: SELECT * FROM shops WHERE tenant_id=1
    DB-->>Controller: Alice's shops only
    Controller-->>Client: 200 OK {data: [...]}

(2) Middleware Implementation

PHP
// app/Http/Middleware/SetTenantContext.php
class SetTenantContext
{
    public function handle(Request $request, Closure $next): Response
    {
        if (!$user = $request->user()) {
            return $next($request);
        }

        $tenant = $user->tenant;

        if (!$tenant || !$tenant->isActive()) {
            abort(403, 'Tenant account is inactive or suspended.');
        }

        // Check plan limits
        if ($tenant->isOverLimit()) {
            abort(402, 'Plan limit exceeded. Please upgrade your subscription.');
        }

        Tenant::setCurrent($tenant);
        $request->attributes->set('tenant', $tenant);

        return $next($request);
    }
}

// app/Http/Kernel.php - Register middleware
protected $middlewareAliases = [
    'tenant' => SetTenantContext::class,
];

(1) ▶ Example: Complete Implementation of the BelongsToTenant Trait

PHP
// app/Models/Traits/BelongsToTenant.php
namespace App\Models\Traits;

use App\Models\Tenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

trait BelongsToTenant
{
    public static function bootBelongsToTenant(): void
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            $tenant = Tenant::current();
            if ($tenant) {
                $builder->where($builder->getModel()->getTable() . '.tenant_id', $tenant->id);
            }
        });

        static::creating(function (Model $model) {
            $tenant = Tenant::current();
            if ($tenant && !$model->isDirty('tenant_id')) {
                $model->tenant_id = $tenant->id;
            }
        });

        static::saving(function (Model $model) {
            $tenant = Tenant::current();
            if ($tenant) {
                $currentTenantId = $model->getOriginal('tenant_id') ?? $model->tenant_id;
                if ((int) $currentTenantId !== $tenant->id) {
                    throw new \Illuminate\Auth\Access\AuthorizationException(
                        'Cannot modify data belonging to another tenant.'
                    );
                }
            }
        });
    }

    public function tenant(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    public function scopeForTenant(Builder $query, Tenant $tenant): Builder
    {
        return $query->withoutGlobalScope('tenant')
            ->where('tenant_id', $tenant->id);
    }

    public function scopeAllTenants(Builder $query): Builder
    {
        return $query->withoutGlobalScope('tenant');
    }
}

Output:

TEXT
// Execution Successful

4. Stripe Subscription Billing Integration

(1) Subscription Lifecycle

100%
stateDiagram-v2
    [*] --> Trial: New Signup
    Trial --> Active: Trial→Paid
    Trial --> Cancelled: Cancel during trial
    Active --> PastDue: Payment failed
    PastDue --> Active: Payment retry success
    PastDue --> Cancelled: Retry exhausted
    Active --> Cancelled: User cancels
    Cancelled --> Active: Resubscribe

(2) The Plan and Subscription Models

PHP
// app/Models/Plan.php
class Plan extends Model
{
    protected $fillable = [
        'name', 'slug', 'shop_limit', 'user_limit',
        'price_cents', 'stripe_price_id', 'is_active',
    ];

    protected $casts = [
        'price_cents' => 'integer',
        'shop_limit' => 'integer',
        'user_limit' => 'integer',
        'is_active' => 'boolean',
    ];

    public function subscriptions(): HasMany
    {
        return $this->hasMany(Subscription::class);
    }

    public function isFree(): bool
    {
        return $this->price_cents === 0;
    }

    public function formattedPrice(): string
    {
        return $this->isFree() ? 'Free' : '$' . number_format($this->price_cents / 100, 2) . '/mo';
    }
}

// app/Models/Subscription.php
class Subscription extends Model
{
    protected $fillable = [
        'tenant_id', 'plan_id', 'stripe_id',
        'stripe_status', 'trial_ends_at', 'ends_at',
    ];

    protected $casts = [
        'trial_ends_at' => 'datetime',
        'ends_at' => 'datetime',
    ];

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    public function plan(): BelongsTo
    {
        return $this->belongsTo(Plan::class);
    }

    public function isActive(): bool
    {
        return in_array($this->stripe_status, ['active', 'trialing']);
    }

    public function onTrial(): bool
    {
        return $this->stripe_status === 'trialing' ||
               ($this->trial_ends_at && $this->trial_ends_at->isFuture());
    }

    public function onGracePeriod(): bool
    {
        return $this->ends_at && $this->ends_at->isFuture();
    }
}

(1) ▶ Example: Stripe Checkout and Webhook Processing

PHP
// app/Http/Controllers/SubscriptionController.php
class SubscriptionController extends Controller
{
    public function store(Request $request)
    {
        $request->validate(['plan_slug' => 'required|exists:plans,slug']);
        $plan = Plan::whereSlug($request->plan_slug)->firstOrFail();
        $tenant = $request->attributes->get('tenant');

        if ($plan->isFree()) {
            $tenant->updateToFreePlan($plan);
            return response()->json(['message' => 'Subscribed to free plan']);
        }

        // Create Stripe Checkout session
        $checkout = Stripe::checkout()->sessions()->create([
            'customer' => $tenant->stripe_customer_id ?? $this->createStripeCustomer($tenant),
            'mode' => 'subscription',
            'line_items' => [[
                'price' => $plan->stripe_price_id,
                'quantity' => 1,
            ]],
            'subscription_data' => [
                'trial_period_days' => config('shopmetrics.tenant.trial_days'),
                'metadata' => ['tenant_id' => $tenant->id],
            ],
            'success_url' => config('app.url') . '/billing?success=1',
            'cancel_url' => config('app.url') . '/billing?cancel=1',
        ]);

        return response()->json(['checkout_url' => $checkout->url]);
    }

    public function cancel(Request $request)
    {
        $tenant = $request->attributes->get('tenant');
        $subscription = $tenant->subscription;

        Stripe::subscriptions()->update($subscription->stripe_id, [
            'cancel_at_period_end' => true,
        ]);

        $subscription->update(['ends_at' => $subscription->current_period_end]);

        return response()->json(['message' => 'Subscription will cancel at period end']);
    }
}

// app/Http/Controllers/WebhookController.php
class WebhookController extends Controller
{
    public function handleStripe(Request $request)
    {
        $payload = $request->all();
        $event = Stripe::webhooks()->constructEvent(
            $payload,
            $request->header('Stripe-Signature'),
            config('services.stripe.webhook_secret')
        );

        match ($event->type) {
            'customer.subscription.created' => $this->handleSubscriptionCreated($event),
            'customer.subscription.updated' => $this->handleSubscriptionUpdated($event),
            'customer.subscription.deleted' => $this->handleSubscriptionDeleted($event),
            'invoice.payment_failed' => $this->handlePaymentFailed($event),
            default => null,
        };

        return response()->json(['received' => true]);
    }

    private function handleSubscriptionUpdated($event): void
    {
        $stripeSubscription = $event->data->object;
        $subscription = Subscription::whereStripeId($stripeSubscription->id)->firstOrFail();
        $subscription->update([
            'stripe_status' => $stripeSubscription->status,
            'trial_ends_at' => $stripeSubscription->trial_end
                ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->trial_end)
                : null,
            'ends_at' => $stripeSubscription->cancel_at_period_end
                ? \Carbon\Carbon::createFromTimestamp($stripeSubscription->current_period_end)
                : null,
        ]);
    }

    private function handlePaymentFailed($event): void
    {
        $stripeSubscription = $event->data->object->subscription;
        $subscription = Subscription::whereStripeId($stripeSubscription)->first();
        if ($subscription) {
            $subscription->tenant->users->each(
                fn ($user) => $user->notify(new PaymentFailedNotification())
            );
        }
    }
}

Output:

TEXT
// Execution Successful

5. Data Analytics Dashboard

(1) Dashboard API Design

Endpoint Returned Data Cache TTL
GET /dashboard/overview Total Revenue/Number of Orders/Number of Customers/Year-over-Year 15 minutes
GET /dashboard/revenue Revenue Trend Chart Data 30 minutes
GET /dashboard/top-products Top 10 Products 1 hour
GET /dashboard/shop-comparison Store Comparison 1 hour

(1) ▶ Example: ShopMetrics Dashboard Service

PHP
// app/Services/DashboardService.php
class DashboardService
{
    public function getOverview(Tenant $tenant): array
    {
        return Cache::remember(
            "dashboard:{$tenant->id}:overview",
            now()->addMinutes(15),
            fn () => $this->calculateOverview($tenant)
        );
    }

    private function calculateOverview(Tenant $tenant): array
    {
        $currentMonth = now()->startOfMonth();
        $lastMonth = now()->subMonth()->startOfMonth();

        $current = DB::table('orders')
            ->where('tenant_id', $tenant->id)
            ->where('ordered_at', '>=', $currentMonth)
            ->select([
                DB::raw('SUM(total_cents) as revenue_cents'),
                DB::raw('COUNT(*) as order_count'),
                DB::raw('COUNT(DISTINCT customer_email) as customer_count'),
            ])
            ->first();

        $previous = DB::table('orders')
            ->where('tenant_id', $tenant->id)
            ->whereBetween('ordered_at', [$lastMonth, $currentMonth])
            ->select([
                DB::raw('SUM(total_cents) as revenue_cents'),
                DB::raw('COUNT(*) as order_count'),
            ])
            ->first();

        return [
            'revenue' => [
                'current' => (float) ($current->revenue_cents / 100),
                'previous' => (float) ($previous->revenue_cents / 100),
                'change_percent' => $this->percentChange(
                    $previous->revenue_cents, $current->revenue_cents
                ),
            ],
            'orders' => [
                'current' => (int) $current->order_count,
                'previous' => (int) $previous->order_count,
                'change_percent' => $this->percentChange(
                    $previous->order_count, $current->order_count
                ),
            ],
            'customers' => (int) $current->customer_count,
        ];
    }

    public function getRevenueChart(Tenant $tenant, string $period = '30d'): array
    {
        $days = match ($period) {
            '7d' => 7, '30d' => 30, '90d' => 90, '1y' => 365,
            default => 30,
        };

        return Cache::remember(
            "dashboard:{$tenant->id}:revenue:{$period}",
            now()->addMinutes(30),
            fn () => $this->calculateRevenueChart($tenant, $days)
        );
    }

    private function calculateRevenueChart(Tenant $tenant, int $days): array
    {
        $data = DB::table('orders')
            ->where('tenant_id', $tenant->id)
            ->where('ordered_at', '>=', now()->subDays($days))
            ->groupBy('date')
            ->orderBy('date')
            ->select([
                DB::raw('DATE(ordered_at) as date'),
                DB::raw('SUM(total_cents) as revenue_cents'),
                DB::raw('COUNT(*) as order_count'),
            ])
            ->get();

        return [
            'labels' => $data->pluck('date')->map(fn ($d) => \Carbon\Carbon::parse($d)->format('M d')),
            'revenue' => $data->pluck('revenue_cents')->map(fn ($v) => $v / 100),
            'orders' => $data->pluck('order_count'),
        ];
    }

    private function percentChange(float|int $old, float|int $new): float
    {
        if ($old == 0) return $new > 0 ? 100.0 : 0.0;
        return round((($new - $old) / $old) * 100, 1);
    }
}

Output:

TEXT
// Execution Successful

6. Asynchronous Report Generation

(1) Report Generation Process

100%
sequenceDiagram
    participant User as Alice
    participant API as API Server
    participant Queue as Redis Queue
    participant Worker as Queue Worker
    participant S3 as S3 Storage
    participant Mail as Email Service

    User->>API: POST /reports {type: monthly, format: xlsx}
    API->>Queue: Dispatch GenerateReportJob
    API-->>User: 202 {report_id, status: pending}

    Queue->>Worker: Pick up job
    Worker->>Worker: Query & aggregate data
    Worker->>S3: Upload report file
    Worker->>Worker: Update report status=completed
    Worker->>Mail: Send notification email
    Mail-->>User: Report ready email

    User->>API: GET /reports/{id}
    API-->>User: {status: completed, download_url}

(1) ▶ Example: ShopMetrics Report Generation Job

PHP
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 600;

    public function __construct(
        public Tenant $tenant,
        public string $type,
        public string $format,
        public array $filters = [],
    ) {}

    public function handle(
        ReportGenerator $generator,
        ReportStorage $storage,
    ): void {
        $report = Report::create([
            'tenant_id' => $this->tenant->id,
            'type' => $this->type,
            'format' => $this->format,
            'status' => 'processing',
            'filters' => $this->filters,
        ]);

        try {
            $data = $this->fetchData();
            $filePath = $generator->generate($data, $this->format, $report);
            $storagePath = $storage->store($filePath, $this->tenant, $report);

            $report->update([
                'status' => 'completed',
                'storage_path' => $storagePath,
                'generated_at' => now(),
            ]);

            $this->notifyUser($report);

        } catch (\Throwable $e) {
            $report->update(['status' => 'failed']);
            throw $e;
        } finally {
            Cache::forget("dashboard:{$this->tenant->id}:*");
        }
    }

    private function fetchData(): Collection
    {
        return match ($this->type) {
            'monthly' => $this->fetchMonthlyData(),
            'product_comparison' => $this->fetchProductComparison(),
            'shop_performance' => $this->fetchShopPerformance(),
            default => throw new \InvalidArgumentException("Unknown report type: {$this->type}"),
        };
    }

    private function fetchMonthlyData(): Collection
    {
        return Order::with('items.product')
            ->where('tenant_id', $this->tenant->id)
            ->whereBetween('ordered_at', [
                $this->filters['date_from'] ?? now()->subMonth(),
                $this->filters['date_to'] ?? now(),
            ])
            ->orderBy('ordered_at')
            ->get();
    }

    private function notifyUser(Report $report): void
    {
        $user = User::find($this->filters['user_id'] ?? $this->tenant->users()->first()->id);
        $user?->notify(new ReportReadyNotification($report));
    }
}

// app/Services/ReportGenerator.php
class ReportGenerator
{
    public function generate(Collection $data, string $format, Report $report): string
    {
        return match ($format) {
            'csv' => $this->generateCsv($data, $report),
            'xlsx' => $this->generateExcel($data, $report),
            'json' => $this->generateJson($data, $report),
            default => throw new \InvalidArgumentException("Unsupported format: {$format}"),
        };
    }

    private function generateCsv(Collection $data, Report $report): string
    {
        $path = tempnam(sys_get_temp_dir(), 'report_');
        $file = fopen($path, 'w');

        fputcsv($file, ['Order ID', 'Date', 'Customer', 'Total', 'Status']);

        foreach ($data as $order) {
            fputcsv($file, [
                $order->external_id,
                $order->ordered_at->format('Y-m-d'),
                $order->customer_email,
                number_format($order->total_cents / 100, 2),
                $order->status,
            ]);
        }

        fclose($file);
        return $path;
    }
}

Output:

TEXT
// Execution Successful

7. Full Implementation of the API Layer

(1) API Resource Output

PHP
// app/Http/Resources/ShopResource.php
class ShopResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'platform' => $this->platform,
            'status' => $this->status,
            'metadata' => $this->metadata,
            'order_count' => $this->whenCounted('orders'),
            'revenue_cents' => $this->whenAggregated('orders', 'total_cents', 'sum'),
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

// app/Http/Resources/OrderResource.php
class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'external_id' => $this->external_id,
            'customer_email' => $this->customer_email,
            'total' => [
                'cents' => $this->total_cents,
                'formatted' => '$' . number_format($this->total_cents / 100, 2),
            ],
            'status' => $this->status,
            'items' => OrderItemResource::collection($this->whenLoaded('items')),
            'ordered_at' => $this->ordered_at->toIso8601String(),
        ];
    }
}

(2) Filtering and Pagination

PHP
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
    public function index(Request $request)
    {
        $query = Order::with(['items.product'])
            ->where('shop_id', $request->route('shop')->id);

        // Filters
        if ($status = $request->query('status')) {
            $query->where('status', $status);
        }

        if ($dateFrom = $request->query('date_from')) {
            $query->where('ordered_at', '>=', $dateFrom);
        }

        if ($dateTo = $request->query('date_to')) {
            $query->where('ordered_at', '<=', $dateTo);
        }

        if ($minTotal = $request->query('min_total')) {
            $query->where('total_cents', '>=', $minTotal * 100);
        }

        if ($search = $request->query('search')) {
            $query->where('external_id', 'like', "%{$search}%")
                ->orWhere('customer_email', 'like', "%{$search}%");
        }

        // Sort
        $sortField = $request->query('sort_by', 'ordered_at');
        $sortDir = $request->query('sort_dir', 'desc');
        $query->orderBy($sortField, $sortDir);

        // Paginate
        $orders = $query->paginate($request->query('per_page', 25));

        return OrderResource::collection($orders);
    }
}

(1) ▶ Example: ShopMetrics Complete API Routes and Controllers

PHP
// routes/api.php
Route::prefix('v1')->group(function () {
    Route::post('auth/login', [AuthController::class, 'login']);
    Route::post('auth/register', [AuthController::class, 'register']);

    Route::middleware(['auth:sanctum', 'tenant'])->group(function () {
        Route::post('auth/logout', [AuthController::class, 'logout']);
        Route::get('auth/me', [AuthController::class, 'me']);

        Route::apiResource('shops', ShopController::class);
        Route::prefix('shops/{shop}')->group(function () {
            Route::apiResource('orders', OrderController::class)->only(['index', 'show']);
            Route::apiResource('products', ProductController::class);
        });

        Route::prefix('dashboard')->group(function () {
            Route::get('overview', [DashboardController::class, 'overview']);
            Route::get('revenue', [DashboardController::class, 'revenue']);
            Route::get('top-products', [DashboardController::class, 'topProducts']);
        });

        Route::apiResource('reports', ReportController::class)->only(['index', 'store', 'show']);
        Route::get('reports/{report}/download', [ReportController::class, 'download']);

        Route::apiResource('alerts', AlertController::class);

        Route::get('plans', [PlanController::class, 'index']);
        Route::post('subscriptions', [SubscriptionController::class, 'store']);
        Route::get('subscription', [SubscriptionController::class, 'show']);
        Route::delete('subscription', [SubscriptionController::class, 'cancel']);

        Route::middleware('role:tenant_owner')->prefix('team')->group(function () {
            Route::apiResource('members', TeamController::class)->only(['index', 'destroy']);
            Route::post('invite', [TeamController::class, 'invite']);
        });
    });

    Route::post('webhooks/stripe', [WebhookController::class, 'handleStripe']);
});

Output:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Full Feature Integration

PHP
// ============================================
// Comprehensive: End-to-end request flow
// From login → dashboard → report → notification
// ============================================

// Step 1: Alice logs in
// POST /api/v1/auth/login {email, password}
class AuthController extends Controller
{
    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        if (!Auth::attempt($credentials)) {
            return response()->json(['message' => 'Invalid credentials'], 401);
        }

        $user = Auth::user();
        $token = $user->createToken('auth-token')->plainTextToken;

        return response()->json([
            'user' => new UserResource($user),
            'token' => $token,
            'tenant' => new TenantResource($user->tenant),
        ]);
    }
}

// Step 2: Alice views dashboard (tenant middleware auto-sets context)
// GET /api/v1/dashboard/overview
class DashboardController extends Controller
{
    public function overview(Request $request)
    {
        $tenant = $request->attributes->get('tenant');
        return response()->json(
            app(DashboardService::class)->getOverview($tenant)
        );
    }
}

// Step 3: Alice generates a monthly report
// POST /api/v1/reports {type: monthly, format: xlsx, date_from, date_to}
class ReportController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'type' => 'required|in:monthly,product_comparison,shop_performance',
            'format' => 'required|in:csv,xlsx,json',
            'date_from' => 'nullable|date',
            'date_to' => 'nullable|date|after:date_from',
        ]);

        $tenant = $request->attributes->get('tenant');

        // Check plan limits
        $monthlyReports = Report::where('tenant_id', $tenant->id)
            ->whereMonth('created_at', now()->month)
            ->count();
        $limit = $tenant->subscription->plan->limits['reports_per_month'] ?? 10;

        if ($monthlyReports >= $limit && $limit !== -1) {
            return response()->json([
                'message' => 'Monthly report limit reached. Upgrade your plan.',
            ], 402);
        }

        GenerateReportJob::dispatch(
            $tenant,
            $validated['type'],
            $validated['format'],
            [...$validated, 'user_id' => $request->user()->id]
        );

        return response()->json([
            'message' => 'Report generation started',
            'estimated_time' => '2-5 minutes',
        ], 202);
    }
}

// Step 4: Worker processes the job, uploads to S3, sends email
// (See GenerateReportJob in Section 6)

// Step 5: Alice downloads the report
// GET /api/v1/reports/{report}/download
class ReportController extends Controller
{
    public function download(Report $report, Request $request)
    {
        $tenant = $request->attributes->get('tenant');
        if ($report->tenant_id !== $tenant->id) {
            abort(403);
        }

        if ($report->status !== 'completed') {
            return response()->json(['message' => 'Report not ready', 'status' => $report->status], 202);
        }

        // Generate temporary S3 URL (5 minutes)
        $url = Storage::disk('s3')->temporaryUrl(
            $report->storage_path,
            now()->addMinutes(5)
        );

        return redirect($url);
    }
}

❓ FAQ

Q Can Global Scope be bypassed?
A Yes, it can be bypassed using raw SQL and withoutGlobalScope(). Mitigation measures: Verify tenant_id ownership at the API layer, implement Row-Level Security (RLS) policies, and log bypass attempts in audit logs.
Q How does Stripe Webhook ensure security?
A Verify the Stripe-Signature header (HMAC-SHA256); never trust the raw payload. The webhook endpoint does not require auth middleware, but it must verify the signature.
Q What should I do if a report generation job times out?
A Set the job's timeout to 600 (10 minutes), tries to 3, and backoff to 60. After a timeout, the job fails and retries; after 3 attempts, it is marked as "failed," and the user is notified to retry. Large datasets should be processed in shards first.
Q How should cache keys be designed in a multi-tenant environment?
A Prefix all cache keys with the tenant_id: dashboard:{tenant_id}:overview. When checking for expiry, use the prefix to scan the cache (Redis SCAN); do not use KEYS (which will cause a block).
Q When should whenLoaded be used for an API resource?
A It is only output when the relationship has been eager-loaded; otherwise, it results in an N+1 query. The client requests the relationship via ?include=items.product, and the controller determines with() based on the include parameter.
Q How do three developers coordinate their code during development?
A We merge changes to the main branch daily and use feature flags to control features that aren't yet complete. Each module is tested independently (Feature Test), and after merging, we run a full suite of tests. CI blocks merge requests that fail.

📖 Summary


📝 Exercises

  1. Basic Exercise (⭐): Implement ShopResource and OrderResource, including associated data (whenLoaded), and write the corresponding feature tests to verify the JSON structure.

  2. Advanced Exercise (⭐⭐): Fully implement the Stripe subscription workflow—creating a Checkout Session + handling webhooks (3 events: created/updated/deleted) + synchronizing subscription status + sending email notifications for payment failures.

  3. Challenge (⭐⭐⭐): Implement a complete report generation system—GenerateReportJob—that supports both CSV and JSON formats, progress tracking (storing progress percentages in Redis), temporary S3 download URLs (valid for 5 minutes), scheduled job limit checks, and email notifications upon completion. Write feature tests to cover the entire workflow: Create → In Progress → Complete → Download.

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%

🙏 帮我们做得更好

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

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