start-vibing-stacks 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,24 @@
1
1
  ---
2
2
  name: inertia-react
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: LEGACY skill — Inertia.js + React with Laravel-rendered pages. Use
5
+ ONLY in pre-existing projects already built on Inertia. For NEW projects use
6
+ `laravel-api-architecture` + `axios-laravel-api` + `react-api-standards`
7
+ (API-first React SPA, no controller-rendered pages).
4
8
  ---
5
9
 
6
- # Inertia.js + React — Laravel Frontend
10
+ # Inertia.js + React — Laravel Frontend (LEGACY)
7
11
 
8
- **ALWAYS invoke when writing Inertia.js pages, components, or shared data.**
12
+ > **STATUS: LEGACY.** Do NOT pick this stack for new projects. Inertia couples
13
+ > the controller's first byte to a database query, blocking the page render.
14
+ > The modern default is **Laravel API + Axios + React SPA** (see the skills
15
+ > `laravel-api-architecture`, `axios-laravel-api`, `react-api-standards`).
16
+ >
17
+ > Keep this skill loaded **only** when extending an existing Inertia codebase
18
+ > that you cannot rewrite in the same change.
19
+
20
+ **ALWAYS invoke when writing Inertia.js pages, components, or shared data in
21
+ LEGACY projects only.**
9
22
 
10
23
  ## How Inertia Works
11
24
 
@@ -0,0 +1,650 @@
1
+ ---
2
+ name: laravel-api-architecture
3
+ version: 1.0.0
4
+ description: Laravel 12 + Octane + Sanctum API-first architecture pipeline —
5
+ Route → Controller → FormRequest → Policy → Service → Resource → JSON.
6
+ Single-responsibility per layer, role-based bypass via Policy `before()`,
7
+ thin controllers, no DB queries before first byte. Use as the default
8
+ blueprint for every new API endpoint. Pairs with `axios-laravel-api` and
9
+ `react-api-standards`.
10
+ ---
11
+
12
+ # Laravel 12 API-First Architecture (the One Pipeline)
13
+
14
+ **ALWAYS invoke when designing or implementing any Laravel API endpoint that
15
+ will be consumed by a React (Vite/Axios) SPA.**
16
+
17
+ ## The Pipeline
18
+
19
+ ```
20
+ React (api.get) → Route → Controller → FormRequest (rules + Policy)
21
+
22
+ ├── Service (business logic, transactions)
23
+ │ └── Eloquent / external APIs
24
+
25
+ └── Resource (Eloquent → safe JSON)
26
+
27
+ JsonResponse
28
+ ```
29
+
30
+ | Layer | File | Single Responsibility |
31
+ |-------|------|------------------------|
32
+ | **Route** | `routes/api.php` | URL ↔ Controller; group by middleware (auth, throttle) |
33
+ | **Controller** | `app/Http/Controllers/Api/*Controller.php` | Receive `Request`, delegate to Service, return `Resource` |
34
+ | **FormRequest** | `app/Http/Requests/*Request.php` | `rules()` validation + `authorize()` calling Policy |
35
+ | **Policy** | `app/Policies/*Policy.php` | `before()` super-admin bypass; per-action checks |
36
+ | **Service** | `app/Services/*Service.php` | Business logic, transactions, side-effects, NO HTTP |
37
+ | **Resource** | `app/Http/Resources/*Resource.php` | Eloquent → JSON; whitelist fields; format dates |
38
+
39
+ **Rule:** Each layer does ONE thing. A Controller method that runs an Eloquent
40
+ query, formats dates, and returns JSON is wrong on three counts.
41
+
42
+ ## Rule of Thumb
43
+
44
+ - Controller method body: ≤ 10 lines.
45
+ - Service method: any length, but extract helpers in a `Helpers/` sub-namespace
46
+ past 200 lines.
47
+ - FormRequest: ONLY `rules()` + `authorize()` + (optionally) `prepareForValidation()` /
48
+ `passedValidation()`.
49
+ - Policy: `before()` for role bypass; one method per ability.
50
+ - Resource: pure mapper. NO DB queries inside.
51
+
52
+ ## End-to-End Example — `GET /api/orders`
53
+
54
+ ### 1. Route — `routes/api.php`
55
+
56
+ ```php
57
+ use App\Http\Controllers\Api\OrderController;
58
+ use Illuminate\Support\Facades\Route;
59
+
60
+ Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
61
+ Route::apiResource('orders', OrderController::class);
62
+ Route::post('orders/{order}/cancel', [OrderController::class, 'cancel']);
63
+ });
64
+ ```
65
+
66
+ **Rules:**
67
+
68
+ - API routes go in `routes/api.php` (auto-prefixed with `/api`).
69
+ - Group by middleware — DRY for `auth:sanctum` and rate limits.
70
+ - Use `apiResource` for the 5 standard verbs; `Route::post` for action endpoints.
71
+ - Action endpoints are `POST /resource/{id}/action` (NOT a generic `PATCH`).
72
+
73
+ ### 2. Controller — `app/Http/Controllers/Api/OrderController.php`
74
+
75
+ ```php
76
+ namespace App\Http\Controllers\Api;
77
+
78
+ use App\Http\Controllers\Controller;
79
+ use App\Http\Requests\Order\IndexOrderRequest;
80
+ use App\Http\Requests\Order\StoreOrderRequest;
81
+ use App\Http\Resources\OrderResource;
82
+ use App\Models\Order;
83
+ use App\Services\OrderService;
84
+ use Illuminate\Http\JsonResponse;
85
+ use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
86
+
87
+ class OrderController extends Controller
88
+ {
89
+ public function __construct(
90
+ private readonly OrderService $orders,
91
+ ) {}
92
+
93
+ public function index(IndexOrderRequest $request): AnonymousResourceCollection
94
+ {
95
+ $orders = $this->orders->listFor(
96
+ user: $request->user(),
97
+ filters: $request->validated(),
98
+ );
99
+ return OrderResource::collection($orders);
100
+ }
101
+
102
+ public function store(StoreOrderRequest $request): JsonResponse
103
+ {
104
+ $order = $this->orders->create(
105
+ user: $request->user(),
106
+ data: $request->validated(),
107
+ );
108
+ return OrderResource::make($order)->response()->setStatusCode(201);
109
+ }
110
+
111
+ public function show(Order $order): OrderResource
112
+ {
113
+ $this->authorize('view', $order);
114
+ return OrderResource::make($order->load('items'));
115
+ }
116
+
117
+ public function cancel(Order $order): OrderResource
118
+ {
119
+ $this->authorize('cancel', $order);
120
+ $this->orders->cancel($order);
121
+ return OrderResource::make($order->fresh());
122
+ }
123
+ }
124
+ ```
125
+
126
+ **Rules:**
127
+
128
+ - Constructor injection — never `app()` / `resolve()` / `new`.
129
+ - One Service per controller (composition: a Controller can use multiple
130
+ Services if needed, but each method calls ONE Service entry point).
131
+ - `$this->authorize()` for RouteModelBinding paths (no FormRequest).
132
+ - Return type hints on every method — PHPStan level 6+ requires it.
133
+
134
+ ### 3. FormRequest — `app/Http/Requests/Order/StoreOrderRequest.php`
135
+
136
+ ```php
137
+ namespace App\Http\Requests\Order;
138
+
139
+ use App\Models\Order;
140
+ use Illuminate\Foundation\Http\FormRequest;
141
+
142
+ class StoreOrderRequest extends FormRequest
143
+ {
144
+ public function authorize(): bool
145
+ {
146
+ return $this->user()->can('create', Order::class);
147
+ }
148
+
149
+ public function rules(): array
150
+ {
151
+ return [
152
+ 'product_id' => ['required', 'uuid', 'exists:products,id'],
153
+ 'quantity' => ['required', 'integer', 'min:1', 'max:100'],
154
+ 'notes' => ['nullable', 'string', 'max:500'],
155
+ ];
156
+ }
157
+
158
+ public function messages(): array
159
+ {
160
+ return [
161
+ 'product_id.exists' => 'The selected product does not exist.',
162
+ ];
163
+ }
164
+
165
+ protected function prepareForValidation(): void
166
+ {
167
+ $this->merge([
168
+ 'notes' => $this->notes ? trim($this->notes) : null,
169
+ ]);
170
+ }
171
+ }
172
+ ```
173
+
174
+ ```php
175
+ namespace App\Http\Requests\Order;
176
+
177
+ use Illuminate\Foundation\Http\FormRequest;
178
+
179
+ class IndexOrderRequest extends FormRequest
180
+ {
181
+ public function authorize(): bool
182
+ {
183
+ return $this->user() !== null; // policy is enforced via Service scope
184
+ }
185
+
186
+ public function rules(): array
187
+ {
188
+ return [
189
+ 'status' => ['nullable', 'string', 'in:pending,paid,shipped,cancelled'],
190
+ 'q' => ['nullable', 'string', 'max:255'],
191
+ 'page' => ['nullable', 'integer', 'min:1'],
192
+ 'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
193
+ 'sort' => ['nullable', 'string', 'in:created_at,-created_at,total,-total'],
194
+ ];
195
+ }
196
+ }
197
+ ```
198
+
199
+ **Rules:**
200
+
201
+ - `authorize()` MUST call a Policy — never return `true` blindly except on
202
+ `index` where Service-level user scoping is the gate.
203
+ - `rules()` exhaustive: type, length, allowed enum values, FK existence.
204
+ - `prepareForValidation()` for input normalization (trim, lowercase email).
205
+ - `passedValidation()` for cross-field invariants (`if A then B required`).
206
+ - Custom messages stay in the FormRequest, NOT translated globally.
207
+
208
+ ### 4. Policy — `app/Policies/OrderPolicy.php`
209
+
210
+ ```php
211
+ namespace App\Policies;
212
+
213
+ use App\Models\Order;
214
+ use App\Models\User;
215
+
216
+ class OrderPolicy
217
+ {
218
+ public function before(User $user, string $ability): ?bool
219
+ {
220
+ return $user->isSuperAdmin() ? true : null;
221
+ }
222
+
223
+ public function viewAny(User $user): bool
224
+ {
225
+ return true; // scoping in Service
226
+ }
227
+
228
+ public function view(User $user, Order $order): bool
229
+ {
230
+ return $user->isAdmin() || $order->user_id === $user->id;
231
+ }
232
+
233
+ public function create(User $user): bool
234
+ {
235
+ return $user->isAdmin() || $user->isUser();
236
+ }
237
+
238
+ public function update(User $user, Order $order): bool
239
+ {
240
+ return $user->isAdmin() || $order->user_id === $user->id;
241
+ }
242
+
243
+ public function delete(User $user, Order $order): bool
244
+ {
245
+ return $user->isAdmin();
246
+ }
247
+
248
+ public function cancel(User $user, Order $order): bool
249
+ {
250
+ return $user->isAdmin() || ($order->user_id === $user->id && $order->isCancellable());
251
+ }
252
+ }
253
+ ```
254
+
255
+ **Rules:**
256
+
257
+ - `before()` returning `true` bypasses every ability for the role — use for
258
+ `superadmin`. Return `null` (NOT `false`) to fall through to per-method checks.
259
+ - `admin` may have blanket access in some methods; regular `user` only owns
260
+ their own resources (`$order->user_id === $user->id`).
261
+ - Register Policy in `AuthServiceProvider::$policies` OR rely on Laravel's
262
+ auto-discovery (model `Order` ↔ `OrderPolicy` in `app/Policies/`).
263
+
264
+ ### 5. Service — `app/Services/OrderService.php`
265
+
266
+ ```php
267
+ namespace App\Services;
268
+
269
+ use App\Models\Order;
270
+ use App\Models\User;
271
+ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
272
+ use Illuminate\Support\Facades\DB;
273
+
274
+ class OrderService
275
+ {
276
+ public function listFor(User $user, array $filters = []): LengthAwarePaginator
277
+ {
278
+ $query = Order::query()->with('items');
279
+
280
+ if (! $user->isAdmin()) {
281
+ $query->where('user_id', $user->id);
282
+ }
283
+
284
+ if ($status = $filters['status'] ?? null) {
285
+ $query->where('status', $status);
286
+ }
287
+
288
+ if ($q = $filters['q'] ?? null) {
289
+ $query->where('reference', 'like', "%{$q}%");
290
+ }
291
+
292
+ if ($sort = $filters['sort'] ?? null) {
293
+ $direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
294
+ $column = ltrim($sort, '-');
295
+ $query->orderBy($column, $direction);
296
+ } else {
297
+ $query->latest();
298
+ }
299
+
300
+ return $query->paginate($filters['per_page'] ?? 25)->withQueryString();
301
+ }
302
+
303
+ public function create(User $user, array $data): Order
304
+ {
305
+ return DB::transaction(function () use ($user, $data) {
306
+ $order = Order::create([
307
+ ...$data,
308
+ 'user_id' => $user->id,
309
+ 'status' => 'pending',
310
+ ]);
311
+ // dispatch jobs, fire events, etc.
312
+ return $order;
313
+ });
314
+ }
315
+
316
+ public function cancel(Order $order): void
317
+ {
318
+ DB::transaction(function () use ($order) {
319
+ $order->update(['status' => 'cancelled', 'cancelled_at' => now()]);
320
+ // refund, notify, etc.
321
+ });
322
+ }
323
+ }
324
+ ```
325
+
326
+ **Rules:**
327
+
328
+ - Service knows nothing about HTTP (`$request`, `response()`, `redirect()`).
329
+ - Always use `DB::transaction()` when there are 2+ writes.
330
+ - Scoping by user happens HERE (not in Controller, not in Resource).
331
+ - Return Eloquent models or paginators — let the Controller wrap in Resource.
332
+
333
+ ### 6. Resource — `app/Http/Resources/OrderResource.php`
334
+
335
+ ```php
336
+ namespace App\Http\Resources;
337
+
338
+ use App\Traits\FormatsDatesForApi;
339
+ use Illuminate\Http\Request;
340
+ use Illuminate\Http\Resources\Json\JsonResource;
341
+
342
+ class OrderResource extends JsonResource
343
+ {
344
+ use FormatsDatesForApi;
345
+
346
+ public function toArray(Request $request): array
347
+ {
348
+ return [
349
+ 'id' => $this->id,
350
+ 'reference' => $this->reference,
351
+ 'status' => $this->status->value,
352
+ 'total' => (float) $this->total,
353
+ 'quantity' => $this->quantity,
354
+ 'notes' => $this->notes,
355
+ 'user' => UserResource::make($this->whenLoaded('user')),
356
+ 'items' => OrderItemResource::collection($this->whenLoaded('items')),
357
+ 'created_at' => $this->formatDateTime($this->created_at, $request),
358
+ 'cancelled_at' => $this->formatDateTime($this->cancelled_at, $request),
359
+ // NEVER include: secret_token, internal_notes, user.password
360
+ ];
361
+ }
362
+ }
363
+ ```
364
+
365
+ **Rules:**
366
+
367
+ - Whitelist explicitly — never `return $this->resource->toArray()`.
368
+ - Use `whenLoaded()` to avoid N+1 (only includes if `->with()` was called).
369
+ - Format dates with `FormatsDatesForApi` trait (timezone via `X-Timezone` header).
370
+ - NEVER include secrets, password hashes, internal IDs, or PII not approved
371
+ for the consumer.
372
+
373
+ ## Sanctum SPA Backend Configuration
374
+
375
+ ### `bootstrap/app.php`
376
+
377
+ ```php
378
+ return Application::configure(basePath: dirname(__DIR__))
379
+ ->withRouting(
380
+ web: __DIR__.'/../routes/web.php',
381
+ api: __DIR__.'/../routes/api.php',
382
+ commands: __DIR__.'/../routes/console.php',
383
+ health: '/up',
384
+ )
385
+ ->withMiddleware(function (Middleware $middleware) {
386
+ $middleware->statefulApi(); // <-- enables Sanctum SPA cookie
387
+ $middleware->throttleApi('60,1');
388
+ $middleware->validateCsrfTokens(except: [
389
+ 'webhooks/*', // exempt INCOMING webhooks only
390
+ ]);
391
+ $middleware->append(\App\Http\Middleware\SecurityHeaders::class);
392
+ })
393
+ ->withExceptions(function (Exceptions $exceptions) {
394
+ $exceptions->shouldRenderJsonWhen(
395
+ fn ($request) => $request->is('api/*') || $request->expectsJson(),
396
+ );
397
+ })
398
+ ->create();
399
+ ```
400
+
401
+ ### `config/sanctum.php`
402
+
403
+ ```php
404
+ 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
405
+ '%s%s,localhost,localhost:5173,127.0.0.1,127.0.0.1:8000',
406
+ Sanctum::currentApplicationUrlWithPort(),
407
+ Sanctum::currentRequestHost() ? ','.Sanctum::currentRequestHost() : '',
408
+ ))),
409
+
410
+ 'guard' => ['web'],
411
+
412
+ 'expiration' => 60 * 24, // 24h for token-based clients (mobile / 3rd party)
413
+
414
+ 'middleware' => [
415
+ 'authenticate_session' => Authenticate::class,
416
+ 'encrypt_cookies' => EncryptCookies::class,
417
+ 'validate_csrf_token' => ValidateCsrfToken::class,
418
+ ],
419
+ ```
420
+
421
+ ### `config/cors.php`
422
+
423
+ ```php
424
+ return [
425
+ 'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
426
+ 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
427
+ 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:5173')],
428
+ 'allowed_headers' => ['*'],
429
+ 'exposed_headers' => ['X-Request-ID'],
430
+ 'max_age' => 86400,
431
+ 'supports_credentials' => true, // MANDATORY for cookie auth
432
+ ];
433
+ ```
434
+
435
+ ### `config/session.php` (production)
436
+
437
+ ```php
438
+ 'driver' => env('SESSION_DRIVER', 'cookie'),
439
+ 'lifetime' => env('SESSION_LIFETIME', 120),
440
+ 'encrypt' => true,
441
+ 'http_only' => true, // MANDATORY
442
+ 'same_site' => env('SESSION_SAME_SITE', 'lax'),
443
+ 'secure' => env('SESSION_SECURE_COOKIE', false), // true in HTTPS prod
444
+ 'domain' => env('SESSION_DOMAIN'), // .example.com for cross-subdomain
445
+ ```
446
+
447
+ ### `.env` (production sketch)
448
+
449
+ ```
450
+ APP_URL=https://app.example.com
451
+ SESSION_DRIVER=cookie
452
+ SESSION_LIFETIME=120
453
+ SESSION_DOMAIN=.example.com
454
+ SESSION_SECURE_COOKIE=true
455
+ SESSION_SAME_SITE=lax
456
+ SANCTUM_STATEFUL_DOMAINS=app.example.com
457
+ FRONTEND_URL=https://app.example.com
458
+ ```
459
+
460
+ ## Login & Logout Endpoints (Sanctum SPA)
461
+
462
+ ```php
463
+ // app/Http/Controllers/Api/AuthController.php
464
+ namespace App\Http\Controllers\Api;
465
+
466
+ use App\Http\Controllers\Controller;
467
+ use App\Http\Requests\Auth\LoginRequest;
468
+ use App\Http\Resources\UserResource;
469
+ use Illuminate\Http\JsonResponse;
470
+ use Illuminate\Http\Request;
471
+ use Illuminate\Support\Facades\Auth;
472
+
473
+ class AuthController extends Controller
474
+ {
475
+ public function login(LoginRequest $request): JsonResponse
476
+ {
477
+ if (! Auth::attempt($request->validated(), remember: true)) {
478
+ return response()->json([
479
+ 'message' => 'Invalid credentials',
480
+ ], 422);
481
+ }
482
+
483
+ $request->session()->regenerate();
484
+ return response()->json([
485
+ 'data' => UserResource::make($request->user()),
486
+ ]);
487
+ }
488
+
489
+ public function logout(Request $request): JsonResponse
490
+ {
491
+ Auth::guard('web')->logout();
492
+ $request->session()->invalidate();
493
+ $request->session()->regenerateToken();
494
+ return response()->json(['message' => 'Logged out']);
495
+ }
496
+
497
+ public function me(Request $request): UserResource
498
+ {
499
+ return UserResource::make($request->user());
500
+ }
501
+ }
502
+ ```
503
+
504
+ ```php
505
+ // routes/web.php — login/logout MUST be on web (session middleware)
506
+ Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:auth');
507
+ Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum');
508
+
509
+ // routes/api.php
510
+ Route::middleware('auth:sanctum')->get('/user', [AuthController::class, 'me']);
511
+ ```
512
+
513
+ **Rules:**
514
+
515
+ - `/login` and `/logout` go in `routes/web.php` so the session middleware runs
516
+ and the cookie is created/destroyed.
517
+ - Always `regenerate()` after login to prevent session fixation.
518
+ - Throttle `/login` (5/min/IP) — see `api-security` skill.
519
+
520
+ ## Folder Map
521
+
522
+ ```
523
+ app/
524
+ ├── Http/
525
+ │ ├── Controllers/
526
+ │ │ └── Api/ # JSON-only controllers
527
+ │ │ ├── OrderController.php
528
+ │ │ └── UserController.php
529
+ │ ├── Requests/
530
+ │ │ ├── Order/
531
+ │ │ │ ├── IndexOrderRequest.php
532
+ │ │ │ ├── StoreOrderRequest.php
533
+ │ │ │ └── UpdateOrderRequest.php
534
+ │ │ └── Auth/
535
+ │ │ └── LoginRequest.php
536
+ │ ├── Resources/
537
+ │ │ ├── OrderResource.php
538
+ │ │ ├── OrderItemResource.php
539
+ │ │ └── UserResource.php
540
+ │ └── Middleware/
541
+ │ ├── SecurityHeaders.php
542
+ │ └── RequestId.php
543
+ ├── Policies/
544
+ │ ├── OrderPolicy.php
545
+ │ └── UserPolicy.php
546
+ ├── Services/
547
+ │ ├── OrderService.php
548
+ │ ├── UserService.php
549
+ │ └── External/
550
+ │ └── StripeService.php
551
+ ├── Models/
552
+ │ ├── Order.php
553
+ │ └── User.php
554
+ └── Traits/
555
+ ├── FormatsDatesForApi.php
556
+ └── ApiResponse.php
557
+ ```
558
+
559
+ ## Pagination Contract (consumed by React)
560
+
561
+ Laravel default `->paginate()` returns:
562
+
563
+ ```json
564
+ {
565
+ "data": [ /* OrderResource[] */ ],
566
+ "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
567
+ "meta": {
568
+ "current_page": 1,
569
+ "from": 1,
570
+ "last_page": 4,
571
+ "per_page": 25,
572
+ "to": 25,
573
+ "total": 92,
574
+ "path": "https://api.example.com/api/orders"
575
+ }
576
+ }
577
+ ```
578
+
579
+ **Rule:** Frontend reads `data` for items and `meta.current_page` /
580
+ `meta.last_page` for the pager. Don't reinvent — use `->paginate()`.
581
+
582
+ ## Action Endpoints — When (Not) to Use
583
+
584
+ ```php
585
+ // ✅ Specific business action with side-effects
586
+ POST /api/orders/{order}/cancel → OrderController::cancel
587
+ POST /api/users/{user}/reset-password → UserController::resetPassword
588
+
589
+ // ❌ Generic PATCH for business logic
590
+ PATCH /api/orders/{order} { "status": "cancelled" } // hides side-effects
591
+ ```
592
+
593
+ ## Routing Cheatsheet
594
+
595
+ | Verb | URI | Controller method | Returns |
596
+ |------|-----|--------------------|---------|
597
+ | GET | `/api/orders` | `index` | `OrderResource::collection` (paginated) |
598
+ | POST | `/api/orders` | `store` | `OrderResource` (201) |
599
+ | GET | `/api/orders/{order}` | `show` | `OrderResource` |
600
+ | PUT | `/api/orders/{order}` | `update` | `OrderResource` |
601
+ | DELETE | `/api/orders/{order}` | `destroy` | 204 No Content |
602
+ | POST | `/api/orders/{id}/cancel` | `cancel` | `OrderResource` |
603
+
604
+ ## Octane Safety Checklist (per-endpoint)
605
+
606
+ - [ ] Controller depends on Services via `__construct()`, not `app()`
607
+ - [ ] Service has no `static` properties
608
+ - [ ] Service does not mutate `config()` at runtime
609
+ - [ ] No superglobals (`$_GET`, `$_SESSION`, `$_SERVER`)
610
+ - [ ] No `dd()` / `die()` / `exit()` (kills the worker)
611
+ - [ ] Service-level memoization is instance-scoped, not static
612
+ - [ ] DB transactions wrap multi-write business actions
613
+
614
+ ## End-to-End Checklist — Before Merging an Endpoint
615
+
616
+ - [ ] Route in `routes/api.php`, behind `auth:sanctum` + `throttle`
617
+ - [ ] Controller method ≤ 10 lines, only delegates
618
+ - [ ] FormRequest with strict `rules()` AND `authorize()` calling Policy
619
+ - [ ] Policy has `before()` for super-admin bypass + per-action checks
620
+ - [ ] Service holds the business logic, transactions, side-effects
621
+ - [ ] Resource whitelists fields; uses `whenLoaded()` for relations
622
+ - [ ] Dates formatted via `FormatsDatesForApi`
623
+ - [ ] Pagination via `->paginate()` returning `data + meta + links`
624
+ - [ ] PHPUnit feature test for happy path + 422 + 403 + 401 cases
625
+ - [ ] PHPStan level 6+ passes (no `mixed`, no `array` returns)
626
+
627
+ ## FORBIDDEN
628
+
629
+ | Action | Reason |
630
+ |--------|--------|
631
+ | Eloquent query inside a Controller | Move it to a Service |
632
+ | FormRequest `authorize()` returning `true` blindly | Must call Policy or be index |
633
+ | Resource performing DB queries | Use `whenLoaded()` and pre-load in Service |
634
+ | Returning `Inertia::render()` from new endpoints | Defeats the API-first model |
635
+ | Cross-cutting business logic in middleware | Middleware = HTTP concerns only |
636
+ | `->get()->map()->...` followed by `->paginate()` | Forces full hydration; paginate first |
637
+ | Skipping the FormRequest layer | Validation drift; CVE waiting to happen |
638
+ | Adding new "smart" abilities without Policy methods | Hidden access logic |
639
+ | Service instantiating a Service via `new` | Use DI all the way down |
640
+ | `static $cache` in a Service | Octane state leak across requests |
641
+ | `env()` outside `config/*.php` | Returns `null` once `config:cache` runs |
642
+
643
+ ## See Also
644
+
645
+ - `axios-laravel-api` — frontend client + CSRF/cookie flow
646
+ - `react-api-standards` — React page contracts
647
+ - `api-design` — generic REST principles, pagination, dates
648
+ - `api-security` — Sanctum hardening, rate limits, CORS, audit log
649
+ - `laravel-octane` — worker memory, no statics, persistent connections
650
+ - `laravel-patterns` — UUID models, Loggable trait, JSON casts
@@ -1,11 +1,19 @@
1
1
  ---
2
2
  name: laravel-inertia-i18n
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: LEGACY — Inertia-specific i18n (translations shared per page via
5
+ Inertia props). Use ONLY in pre-existing Inertia projects. For NEW projects
6
+ see the i18n section in `laravel-patterns` (single `/api/i18n/{locale}`
7
+ endpoint consumed by the React SPA).
4
8
  ---
5
9
 
6
- # Laravel + Inertia i18n — Centralized Translations
10
+ # Laravel + Inertia i18n — Centralized Translations (LEGACY)
7
11
 
8
- **ALWAYS invoke when adding translations, creating new pages, or working with multilingual content.**
12
+ > **STATUS: LEGACY.** New projects use a single `/api/i18n/{locale}` endpoint
13
+ > consumed by the SPA at boot (see `laravel-patterns` → "Translations for
14
+ > API-First Frontend"). Keep this loaded only for legacy Inertia codebases.
15
+
16
+ **ALWAYS invoke when adding translations, creating new pages, or working with multilingual content in LEGACY Inertia projects only.**
9
17
 
10
18
  ## Architecture
11
19