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,9 +1,23 @@
1
1
  ---
2
2
  name: laravel-patterns
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: Laravel 12 model/controller/service/job patterns — UUIDs, Loggable
5
+ trait, mass-assignment, JSON casts, thin Service-driven controllers, idempotent
6
+ jobs, batch processing. API-first by default (controllers return Resources, NOT
7
+ `Inertia::render()`). Use for any Laravel domain code. Pairs with
8
+ `laravel-api-architecture` (the pipeline) and `laravel-octane` (worker safety).
4
9
  ---
5
10
 
6
- # Laravel Patterns & Standards
11
+ # Laravel 12 Patterns & Standards
12
+
13
+ > **Default architecture is API-first.** Controllers return `JsonResponse` /
14
+ > `JsonResource` consumed by a React+Axios SPA. The full pipeline
15
+ > (Route → Controller → FormRequest → Policy → Service → Resource → JSON) lives
16
+ > in `laravel-api-architecture`. This skill covers the building blocks
17
+ > (Models, Services, Jobs, Caching) that compose into that pipeline.
18
+ >
19
+ > **Do NOT add new `Inertia::render()` controllers.** Inertia is supported only
20
+ > for legacy projects (see `inertia-react` skill, marked LEGACY).
7
21
 
8
22
  ## Model Standards
9
23
 
@@ -66,75 +80,65 @@ $user = User::create($request->validated());
66
80
 
67
81
  ## Controller Standards
68
82
 
69
- ### Thin Controllers
70
-
71
- Controllers should ONLY handle HTTP concerns. Delegate to Services.
83
+ ### Thin API Controllers (default)
72
84
 
73
- #### Inertia Controllers (Frontend pages)
85
+ Controllers ONLY handle HTTP concerns. Delegate to Services. Validation goes
86
+ through FormRequest, authorization through Policy. Full end-to-end example
87
+ lives in `laravel-api-architecture` — this section is the contract.
74
88
 
75
89
  ```php
76
- use Inertia\Inertia;
77
- use Inertia\Response as InertiaResponse;
90
+ namespace App\Http\Controllers\Api;
91
+
92
+ use App\Http\Controllers\Controller;
93
+ use App\Http\Requests\Order\StoreOrderRequest;
94
+ use App\Http\Resources\OrderResource;
95
+ use App\Models\Order;
96
+ use App\Services\OrderService;
97
+ use Illuminate\Http\JsonResponse;
98
+ use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
78
99
 
79
100
  class OrderController extends Controller
80
101
  {
81
102
  public function __construct(
82
- private readonly OrderService $orderService,
103
+ private readonly OrderService $orders,
83
104
  ) {}
84
105
 
85
- public function index(Request $request): InertiaResponse
106
+ public function index(IndexOrderRequest $request): AnonymousResourceCollection
86
107
  {
87
- return Inertia::render('Orders/Index', [
88
- 'orders' => OrderResource::collection(
89
- $this->orderService->listForUser($request->user())
90
- ),
91
- ]);
108
+ $paginated = $this->orders->listFor($request->user(), $request->validated());
109
+ return OrderResource::collection($paginated);
92
110
  }
93
111
 
94
- public function store(StoreOrderRequest $request): RedirectResponse
112
+ public function store(StoreOrderRequest $request): JsonResponse
95
113
  {
96
- $this->orderService->create($request->validated());
97
-
98
- return redirect()
99
- ->route('orders.index')
100
- ->with('success', __('orders.created'));
114
+ $order = $this->orders->create($request->user(), $request->validated());
115
+ return OrderResource::make($order)->response()->setStatusCode(201);
101
116
  }
102
- }
103
- ```
104
-
105
- #### API Controllers (JSON responses)
106
-
107
- ```php
108
- class OrderApiController extends Controller
109
- {
110
- public function __construct(
111
- private readonly OrderService $orderService,
112
- ) {}
113
117
 
114
- public function store(StoreOrderRequest $request): JsonResponse
118
+ public function show(Order $order): OrderResource
115
119
  {
116
- $order = $this->orderService->create($request->validated());
117
-
118
- return OrderResource::make($order)
119
- ->response()
120
- ->setStatusCode(201);
120
+ $this->authorize('view', $order);
121
+ return OrderResource::make($order);
121
122
  }
122
123
 
123
- public function resetAttempts(Order $order): JsonResponse
124
+ public function resetAttempts(Order $order): OrderResource
124
125
  {
125
- $this->orderService->resetAttempts($order);
126
-
127
- return response()->json(['message' => 'Attempts reset']);
126
+ $this->authorize('update', $order);
127
+ $this->orders->resetAttempts($order);
128
+ return OrderResource::make($order->fresh());
128
129
  }
129
130
  }
130
131
  ```
131
132
 
132
133
  **Rules:**
133
- - No business logic in controllers
134
- - Use Form Requests for validation
135
- - Inertia: return `Inertia::render()` for GET, `redirect()` for POST/PUT/DELETE
136
- - API: use API Resources for response formatting
137
- - Use DI for services (constructor injection)
134
+ - No business logic in controllers — delegate to Services.
135
+ - One Service per controller method (composition: a controller may use multiple
136
+ services overall, but each method calls one entry point).
137
+ - Use FormRequest for validation; use `$this->authorize()` only on routes that
138
+ rely on RouteModelBinding without a FormRequest.
139
+ - API: always wrap responses in `JsonResource` / `JsonResource::collection`.
140
+ - DI in constructors only — never `app()` / `resolve()` / `new`.
141
+ - Action endpoints get `POST /resource/{id}/action` (NOT generic `PATCH`).
138
142
 
139
143
  ### Form Request Validation
140
144
 
@@ -143,20 +147,63 @@ class StoreOrderRequest extends FormRequest
143
147
  {
144
148
  public function authorize(): bool
145
149
  {
146
- return $this->user()->can('create', Order::class);
150
+ return $this->user()->can('create', Order::class); // routes to Policy
147
151
  }
148
152
 
149
153
  public function rules(): array
150
154
  {
151
155
  return [
152
156
  'product_id' => ['required', 'uuid', 'exists:products,id'],
153
- 'quantity' => ['required', 'integer', 'min:1', 'max:100'],
154
- 'notes' => ['nullable', 'string', 'max:500'],
157
+ 'quantity' => ['required', 'integer', 'min:1', 'max:100'],
158
+ 'notes' => ['nullable', 'string', 'max:500'],
155
159
  ];
156
160
  }
161
+
162
+ protected function prepareForValidation(): void
163
+ {
164
+ $this->merge(['notes' => $this->notes ? trim($this->notes) : null]);
165
+ }
157
166
  }
158
167
  ```
159
168
 
169
+ **Rules:**
170
+ - `authorize()` MUST call a Policy via `$user->can(...)`. Returning `true`
171
+ without policy is allowed ONLY for `index` actions where Service-level
172
+ user scoping is the gate.
173
+ - `rules()` exhaustive: type, length, allowed enum values, FK existence
174
+ (`exists:table,id`), file mime/size where applicable.
175
+ - `prepareForValidation()` for input normalization (trim, lowercase email).
176
+ - One FormRequest per Controller action: `StoreOrderRequest`,
177
+ `UpdateOrderRequest`, `IndexOrderRequest`, etc. — group under
178
+ `app/Http/Requests/Order/`.
179
+
180
+ ### Policy (mandatory for protected resources)
181
+
182
+ ```php
183
+ namespace App\Policies;
184
+
185
+ use App\Models\Order;
186
+ use App\Models\User;
187
+
188
+ class OrderPolicy
189
+ {
190
+ // Super-admin bypass — return null (NOT false) to fall through
191
+ public function before(User $user, string $ability): ?bool
192
+ {
193
+ return $user->isSuperAdmin() ? true : null;
194
+ }
195
+
196
+ public function viewAny(User $user): bool { return true; } // scope in Service
197
+ public function view(User $user, Order $o): bool { return $user->isAdmin() || $o->user_id === $user->id; }
198
+ public function create(User $user): bool { return $user->isAdmin() || $user->isUser(); }
199
+ public function update(User $user, Order $o): bool { return $user->isAdmin() || $o->user_id === $user->id; }
200
+ public function delete(User $user, Order $o): bool { return $user->isAdmin(); }
201
+ }
202
+ ```
203
+
204
+ **Rule pattern:** `superadmin` → bypass via `before()`; `admin` → broad access;
205
+ `user` → only own resources (`$model->user_id === $user->id`).
206
+
160
207
  ## Service Architecture
161
208
 
162
209
  ### Service Layer Pattern
@@ -401,19 +448,34 @@ return [
401
448
  - Always add to BOTH `lang/en/*.php` and `lang/pt/*.php`
402
449
  - Error strings in `lang/*/errors.php`
403
450
 
404
- ### Translations with Inertia (On-Demand)
451
+ ### Translations for API-First Frontend (default)
405
452
 
406
- Translations are sent to the frontend per-page via `config/translations_inertia.php`:
453
+ The React SPA owns its own i18n bundle (e.g. `react-intl`, `i18next`). Backend
454
+ exposes translations via a single API endpoint that the frontend caches:
407
455
 
408
456
  ```php
409
- // config/translations_inertia.php
410
- return [
411
- 'global' => ['common', 'errors', 'validation'],
412
- 'pages' => [
413
- 'dashboard' => ['dashboard'],
414
- 'orders/*' => ['orders', 'products'],
415
- ],
416
- ];
457
+ // routes/api.php
458
+ Route::get('/i18n/{locale}', [I18nController::class, 'show'])
459
+ ->whereIn('locale', ['en', 'pt']);
460
+
461
+ // app/Http/Controllers/Api/I18nController.php
462
+ public function show(string $locale): JsonResponse
463
+ {
464
+ return response()
465
+ ->json(Cache::rememberForever("i18n:{$locale}", function () use ($locale) {
466
+ $bag = [];
467
+ foreach (File::files(lang_path($locale)) as $file) {
468
+ $bag[$file->getFilenameWithoutExtension()] = require $file->getPathname();
469
+ }
470
+ return $bag;
471
+ }))
472
+ ->setMaxAge(3600);
473
+ }
417
474
  ```
418
475
 
419
- The `InertiaShare` class loads and caches translations per locale + route, merging global files with page-specific files. Frontend accesses them via the `__()` helper (see inertia-react skill).
476
+ **Rules:**
477
+ - Centralize ALL user-facing strings in `lang/{locale}/*.php` (single source of truth).
478
+ - Frontend pulls them once on boot, caches in memory + `localStorage`.
479
+ - Cache invalidation: bump a version number or clear cache on deploy.
480
+ - For projects already using Inertia, see the **legacy** `inertia-react` and
481
+ `laravel-inertia-i18n` skills.
@@ -24,22 +24,26 @@
24
24
  "frameworks": [
25
25
  {
26
26
  "id": "laravel-octane",
27
- "name": "Laravel 12 + Octane (RoadRunner) + Inertia.js",
27
+ "name": "Laravel 12 + Octane (RoadRunner) + Sanctum SPA API",
28
28
  "icon": "🚀",
29
29
  "detectFiles": ["artisan", "rr.yaml"],
30
30
  "default": true,
31
31
  "skills": [
32
32
  "laravel-patterns",
33
- "laravel-octane"
33
+ "laravel-octane",
34
+ "laravel-api-architecture",
35
+ "api-security"
34
36
  ]
35
37
  },
36
38
  {
37
39
  "id": "laravel",
38
- "name": "Laravel 12 (standard)",
40
+ "name": "Laravel 12 (standard) + Sanctum SPA API",
39
41
  "icon": "🏗️",
40
42
  "detectFiles": ["artisan", "bootstrap/app.php"],
41
43
  "skills": [
42
- "laravel-patterns"
44
+ "laravel-patterns",
45
+ "laravel-api-architecture",
46
+ "api-security"
43
47
  ]
44
48
  }
45
49
  ],
@@ -50,10 +54,17 @@
50
54
  ],
51
55
  "frontendOptions": [
52
56
  {
53
- "id": "react-inertia",
54
- "name": "ReactJS 19 + Inertia.js + TailwindCSS 4",
57
+ "id": "react-api",
58
+ "name": "ReactJS 19 + Vite + Axios (Sanctum SPA API)",
55
59
  "icon": "⚛️",
56
60
  "default": true,
61
+ "frameworks": ["laravel", "laravel-octane"],
62
+ "baseSkillsDir": "react"
63
+ },
64
+ {
65
+ "id": "react-inertia",
66
+ "name": "ReactJS 19 + Inertia.js (LEGACY)",
67
+ "icon": "🪦",
57
68
  "frameworks": ["laravel", "laravel-octane"]
58
69
  },
59
70
  {
@@ -84,6 +95,8 @@
84
95
  "composer-workflow",
85
96
  "security-scan-php",
86
97
  "api-design",
98
+ "api-security",
99
+ "laravel-api-architecture",
87
100
  "openapi-design"
88
101
  ],
89
102
  "requirements": [