start-vibing-stacks 2.2.0 → 2.3.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.
@@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
10
10
  class User extends Model
11
11
  {
12
12
  use HasUuids;
13
-
14
- // Auto-generates UUID for 'id' column
13
+
14
+ protected $fillable = ['name', 'email'];
15
15
  }
16
16
  ```
17
17
 
@@ -34,10 +34,11 @@ class User extends Model
34
34
  ### JSON Column Handling
35
35
 
36
36
  ```php
37
- // Defensive decoding — assume double-encoding possible
37
+ // Defensive decoding — assume double-encoding possible
38
38
  protected $casts = [
39
39
  'metadata' => 'array',
40
40
  'data' => 'array',
41
+ 'status' => OrderStatus::class, // Enum casting
41
42
  ];
42
43
 
43
44
  // For manual handling:
@@ -46,54 +47,185 @@ $data = is_string($model->data)
46
47
  : $model->data;
47
48
  ```
48
49
 
50
+ ### Mass Assignment
51
+
52
+ ```php
53
+ // ALWAYS define $fillable explicitly
54
+ protected $fillable = ['name', 'email', 'status'];
55
+
56
+ // Use validated data from Form Requests
57
+ $user = User::create($request->validated());
58
+
59
+ // NEVER use $guarded = [] (allows everything)
60
+ ```
61
+
62
+ ## Controller Standards
63
+
64
+ ### Thin Controllers
65
+
66
+ Controllers should ONLY handle HTTP concerns. Delegate to Services.
67
+
68
+ #### Inertia Controllers (Frontend pages)
69
+
70
+ ```php
71
+ use Inertia\Inertia;
72
+ use Inertia\Response as InertiaResponse;
73
+
74
+ class OrderController extends Controller
75
+ {
76
+ public function __construct(
77
+ private readonly OrderService $orderService,
78
+ ) {}
79
+
80
+ public function index(Request $request): InertiaResponse
81
+ {
82
+ return Inertia::render('Orders/Index', [
83
+ 'orders' => OrderResource::collection(
84
+ $this->orderService->listForUser($request->user())
85
+ ),
86
+ ]);
87
+ }
88
+
89
+ public function store(StoreOrderRequest $request): RedirectResponse
90
+ {
91
+ $this->orderService->create($request->validated());
92
+
93
+ return redirect()
94
+ ->route('orders.index')
95
+ ->with('success', __('orders.created'));
96
+ }
97
+ }
98
+ ```
99
+
100
+ #### API Controllers (JSON responses)
101
+
102
+ ```php
103
+ class OrderApiController extends Controller
104
+ {
105
+ public function __construct(
106
+ private readonly OrderService $orderService,
107
+ ) {}
108
+
109
+ public function store(StoreOrderRequest $request): JsonResponse
110
+ {
111
+ $order = $this->orderService->create($request->validated());
112
+
113
+ return OrderResource::make($order)
114
+ ->response()
115
+ ->setStatusCode(201);
116
+ }
117
+
118
+ public function resetAttempts(Order $order): JsonResponse
119
+ {
120
+ $this->orderService->resetAttempts($order);
121
+
122
+ return response()->json(['message' => 'Attempts reset']);
123
+ }
124
+ }
125
+ ```
126
+
127
+ **Rules:**
128
+ - No business logic in controllers
129
+ - Use Form Requests for validation
130
+ - Inertia: return `Inertia::render()` for GET, `redirect()` for POST/PUT/DELETE
131
+ - API: use API Resources for response formatting
132
+ - Use DI for services (constructor injection)
133
+
134
+ ### Form Request Validation
135
+
136
+ ```php
137
+ class StoreOrderRequest extends FormRequest
138
+ {
139
+ public function authorize(): bool
140
+ {
141
+ return $this->user()->can('create', Order::class);
142
+ }
143
+
144
+ public function rules(): array
145
+ {
146
+ return [
147
+ 'product_id' => ['required', 'uuid', 'exists:products,id'],
148
+ 'quantity' => ['required', 'integer', 'min:1', 'max:100'],
149
+ 'notes' => ['nullable', 'string', 'max:500'],
150
+ ];
151
+ }
152
+ }
153
+ ```
154
+
49
155
  ## Service Architecture
50
156
 
157
+ ### Service Layer Pattern
158
+
159
+ ```php
160
+ class OrderService
161
+ {
162
+ public function __construct(
163
+ private readonly PaymentGateway $gateway,
164
+ private readonly NotificationService $notifications,
165
+ ) {}
166
+
167
+ public function create(array $data): Order
168
+ {
169
+ return DB::transaction(function () use ($data) {
170
+ $order = Order::create($data);
171
+ $this->gateway->authorize($order);
172
+ $this->notifications->orderCreated($order);
173
+
174
+ return $order;
175
+ });
176
+ }
177
+
178
+ public function resetAttempts(Order $order): void
179
+ {
180
+ $order->update([
181
+ 'status' => OrderStatus::Pending,
182
+ 'attempts' => 0,
183
+ ]);
184
+ }
185
+ }
186
+ ```
187
+
51
188
  ### Helpers for Complex Services
52
189
 
53
190
  ```
54
191
  App\Services\
55
- ├── UserService.php # Core service
192
+ ├── UserService.php # Core service
56
193
  ├── PaymentService.php
57
194
  └── AdPlatforms\
58
- ├── AdPlatformService.php # Main service
195
+ ├── AdPlatformService.php # Main service
59
196
  └── Helpers\
60
197
  ├── GoogleAdsHelper.php # Extracted logic
61
198
  └── MetaAdsHelper.php
62
199
  ```
63
200
 
64
- **Rule:** Extract specific logic into `Helpers` sub-namespaces. Avoid bloated service classes.
65
-
66
- ### Service Naming
67
-
68
- - **Convention:** `snake_case` for service container bindings
69
- - **Class names:** `PascalCase` as standard
201
+ **Rule:** Extract into `Helpers` sub-namespace when service exceeds ~200 lines.
70
202
 
71
203
  ## API Standards
72
204
 
73
205
  ### Date Formatting
74
206
 
75
207
  ```php
76
- // Trait: FormatsDatesForApi
77
208
  trait FormatsDatesForApi
78
209
  {
79
- protected function formatDateTime($date, Request $request): ?string
80
- {
210
+ protected function formatDateTime(
211
+ ?Carbon $date,
212
+ Request $request,
213
+ ): ?string {
81
214
  if (!$date) return null;
82
- // DB stores UTC, convert to user timezone in API Resource only
83
215
  $tz = $request->header('X-Timezone', 'UTC');
84
216
  return $date->setTimezone($tz)->toISOString();
85
217
  }
86
218
  }
87
219
 
88
- // Usage in API Resource:
89
220
  class UserResource extends JsonResource
90
221
  {
91
222
  use FormatsDatesForApi;
92
-
223
+
93
224
  public function toArray(Request $request): array
94
225
  {
95
226
  return [
96
227
  'id' => $this->id,
228
+ 'name' => $this->name,
97
229
  'created_at' => $this->formatDateTime($this->created_at, $request),
98
230
  ];
99
231
  }
@@ -123,57 +255,89 @@ trait FormatsDatesForApi
123
255
  ### Action Endpoints
124
256
 
125
257
  ```php
126
- // Dedicated POST endpoints for specific business actions
258
+ // Dedicated POST endpoints for specific business actions
127
259
  Route::post('/leads/{lead}/reset-attempts', [LeadController::class, 'resetAttempts']);
128
260
  Route::post('/domains/{domain}/refresh-list', [DomainController::class, 'refreshList']);
129
261
 
130
- // ❌ Don't use generic PATCH for specific actions
131
- Route::patch('/leads/{lead}', ...); // Too generic for business logic
262
+ // DON'T use generic PATCH for business logic
263
+ ```
264
+
265
+ ### API Resources (Always Use)
266
+
267
+ ```php
268
+ class LeadResource extends JsonResource
269
+ {
270
+ public function toArray(Request $request): array
271
+ {
272
+ return [
273
+ 'id' => $this->id,
274
+ 'name' => $this->name,
275
+ 'status' => $this->status->value,
276
+ 'domain' => DomainResource::make($this->whenLoaded('domain')),
277
+ 'created_at' => $this->formatDateTime($this->created_at, $request),
278
+ ];
279
+ }
280
+ }
132
281
  ```
133
282
 
134
- ### Job Design
283
+ ## Job Design
135
284
 
136
285
  ```php
137
- // ✅ Idempotent jobs — safe to retry
138
286
  class SendConversionJob implements ShouldQueue
139
287
  {
140
- public function handle(): void
288
+ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
289
+
290
+ public int $tries = 3;
291
+ public int $backoff = 60;
292
+
293
+ public function handle(ConversionGateway $gateway): void
141
294
  {
142
- // Check status BEFORE processing
295
+ // Idempotent: check status BEFORE processing
143
296
  if ($this->lead->conversion_sent) {
144
- return; // Already processed
297
+ return;
145
298
  }
146
-
147
- // Use unique keys for external APIs
148
- $api->sendConversion(
299
+
300
+ $gateway->send(
149
301
  unique_key: $this->lead->order_id,
150
- data: $this->lead->toArray(),
302
+ data: $this->lead->toConversionArray(),
151
303
  );
152
-
304
+
153
305
  $this->lead->update(['conversion_sent' => true]);
154
306
  }
155
307
  }
156
308
  ```
157
309
 
158
310
  **Rules:**
159
- - Jobs MUST be idempotent
311
+ - Jobs MUST be idempotent (safe to retry)
312
+ - Use unique keys for external API calls
160
313
  - Reset jobs: set `status = pending`, reset counters
161
- - Batch processing for high-volume data (chunks)
314
+ - Batch/chunk for high-volume data
315
+
316
+ ### Batch Processing
317
+
318
+ ```php
319
+ Lead::query()
320
+ ->where('status', OrderStatus::Pending)
321
+ ->chunkById(100, function ($leads) {
322
+ foreach ($leads as $lead) {
323
+ ProcessLeadJob::dispatch($lead);
324
+ }
325
+ });
326
+ ```
162
327
 
163
328
  ## Authorization & Caching
164
329
 
165
330
  ### User-Scoped Queries
166
331
 
167
332
  ```php
168
- // ✅ Always filter by authenticated user
169
333
  public function index(Request $request): JsonResponse
170
334
  {
171
335
  $query = Domain::query();
172
-
336
+
173
337
  if (!$request->user()->isAdmin()) {
174
338
  $query->where('user_id', $request->user()->id);
175
339
  }
176
-
340
+
177
341
  return DomainResource::collection($query->paginate());
178
342
  }
179
343
  ```
@@ -181,13 +345,15 @@ public function index(Request $request): JsonResponse
181
345
  ### Redis Caching
182
346
 
183
347
  ```php
184
- // User-specific cache keys for data isolation
185
348
  $domains = Cache::store('redis')
186
349
  ->remember(
187
350
  "domains:user:{$userId}",
188
351
  now()->addMinutes(15),
189
352
  fn () => Domain::where('user_id', $userId)->get()
190
353
  );
354
+
355
+ // Invalidate on write
356
+ Cache::forget("domains:user:{$userId}");
191
357
  ```
192
358
 
193
359
  **Rules:**
@@ -198,12 +364,51 @@ $domains = Cache::store('redis')
198
364
  ## Migration Safety
199
365
 
200
366
  ```bash
201
- # Always incremental
367
+ # ALWAYS incremental
202
368
  php artisan make:migration add_status_to_leads_table
203
369
 
204
- # NEVER (destroys data)
370
+ # NEVER (destroys data)
205
371
  php artisan migrate:fresh
206
- php artisan migrate:refresh
372
+ php artisan migrate:refresh
207
373
  php artisan db:wipe
208
374
  php artisan db:reset
209
375
  ```
376
+
377
+ ## Translations
378
+
379
+ ```php
380
+ // Store in lang/en/*.php and lang/pt/*.php
381
+ // Organize by category within files
382
+ return [
383
+ 'orders' => [
384
+ 'created' => 'Order created successfully',
385
+ 'not_found' => 'Order not found',
386
+ ],
387
+ 'errors' => [
388
+ 'unauthorized' => 'You are not authorized',
389
+ 'rate_limited' => 'Too many attempts',
390
+ ],
391
+ ];
392
+ ```
393
+
394
+ **Rules:**
395
+ - Centralize all user-facing strings in lang files. No hardcoded strings.
396
+ - Always add to BOTH `lang/en/*.php` and `lang/pt/*.php`
397
+ - Error strings in `lang/*/errors.php`
398
+
399
+ ### Translations with Inertia (On-Demand)
400
+
401
+ Translations are sent to the frontend per-page via `config/translations_inertia.php`:
402
+
403
+ ```php
404
+ // config/translations_inertia.php
405
+ return [
406
+ 'global' => ['common', 'errors', 'validation'],
407
+ 'pages' => [
408
+ 'dashboard' => ['dashboard'],
409
+ 'orders/*' => ['orders', 'products'],
410
+ ],
411
+ ];
412
+ ```
413
+
414
+ 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).
@@ -1,8 +1,8 @@
1
- # PHP 8.3+ Patterns
1
+ # PHP 8.3+ Patterns for Laravel
2
2
 
3
3
  ## Version Requirements
4
4
 
5
- - **PHP >= 8.3** — MANDATORY. Do not use deprecated patterns.
5
+ - **PHP >= 8.3** — MANDATORY. Use all modern features.
6
6
  - **Composer >= 2.0** — For dependency management.
7
7
  - Use strict types: `declare(strict_types=1);` in EVERY file.
8
8
 
@@ -11,32 +11,48 @@
11
11
  ### Typed Properties & Constructor Promotion
12
12
 
13
13
  ```php
14
- class User {
14
+ class CreateUserDTO {
15
15
  public function __construct(
16
- private readonly string $name,
17
- private readonly string $email,
18
- private readonly int $age,
16
+ public readonly string $name,
17
+ public readonly string $email,
18
+ public readonly int $age,
19
19
  ) {}
20
20
  }
21
21
  ```
22
22
 
23
- ### Enums
23
+ ### Enums (Use for Status, Types, Roles)
24
24
 
25
25
  ```php
26
- enum Status: string {
27
- case Active = 'active';
28
- case Inactive = 'inactive';
29
- case Suspended = 'suspended';
26
+ enum OrderStatus: string {
27
+ case Pending = 'pending';
28
+ case Processing = 'processing';
29
+ case Completed = 'completed';
30
+ case Cancelled = 'cancelled';
31
+
32
+ public function label(): string {
33
+ return match($this) {
34
+ self::Pending => 'Awaiting Processing',
35
+ self::Processing => 'In Progress',
36
+ self::Completed => 'Done',
37
+ self::Cancelled => 'Cancelled',
38
+ };
39
+ }
30
40
  }
41
+
42
+ // In Eloquent model:
43
+ protected $casts = [
44
+ 'status' => OrderStatus::class,
45
+ ];
31
46
  ```
32
47
 
33
- ### Readonly Classes (8.2+)
48
+ ### Readonly Classes for DTOs
34
49
 
35
50
  ```php
36
- readonly class UserDTO {
51
+ readonly class PaymentResult {
37
52
  public function __construct(
38
- public string $name,
39
- public string $email,
53
+ public string $transactionId,
54
+ public float $amount,
55
+ public bool $success,
40
56
  ) {}
41
57
  }
42
58
  ```
@@ -44,45 +60,103 @@ readonly class UserDTO {
44
60
  ### Typed Class Constants (8.3+)
45
61
 
46
62
  ```php
47
- class Config {
48
- public const string APP_NAME = 'MyApp';
49
- public const int MAX_RETRIES = 3;
63
+ class RateLimiter {
64
+ public const int MAX_ATTEMPTS = 5;
65
+ public const int DECAY_SECONDS = 60;
66
+ public const string CACHE_PREFIX = 'rate_limit';
50
67
  }
51
68
  ```
52
69
 
53
- ### Match Expression
70
+ ### Match Expression (Prefer over switch)
54
71
 
55
72
  ```php
56
- $result = match($status) {
57
- 'active' => handleActive(),
58
- 'inactive' => handleInactive(),
59
- default => throw new \InvalidArgumentException("Unknown status: {$status}"),
73
+ $result = match($request->input('action')) {
74
+ 'approve' => $service->approve($record),
75
+ 'reject' => $service->reject($record),
76
+ 'escalate' => $service->escalate($record),
77
+ default => throw new \InvalidArgumentException("Unknown action"),
60
78
  };
61
79
  ```
62
80
 
63
81
  ### Named Arguments
64
82
 
65
83
  ```php
66
- $response = new Response(
67
- content: $html,
84
+ $response = Response::json(
85
+ data: $collection,
68
86
  status: 200,
69
- headers: ['Content-Type' => 'text/html'],
87
+ headers: ['X-Total-Count' => $total],
70
88
  );
71
89
  ```
72
90
 
73
91
  ### Null-safe Operator
74
92
 
75
93
  ```php
76
- $country = $user?->address?->country?->name;
94
+ $country = $user?->address?->country?->name ?? 'Unknown';
95
+ ```
96
+
97
+ ### First-class Callable Syntax
98
+
99
+ ```php
100
+ $filtered = $collection->filter($this->isEligible(...));
101
+ ```
102
+
103
+ ## Clean Code Patterns
104
+
105
+ ### Dependency Injection (Octane-safe)
106
+
107
+ ```php
108
+ // CORRECT: Constructor injection
109
+ class OrderService {
110
+ public function __construct(
111
+ private readonly PaymentGateway $gateway,
112
+ private readonly OrderRepository $orders,
113
+ private readonly LoggerInterface $logger,
114
+ ) {}
115
+ }
116
+
117
+ // WRONG: Service locator
118
+ class OrderService {
119
+ public function process(): void {
120
+ $gateway = app(PaymentGateway::class); // Avoid in Octane
121
+ }
122
+ }
77
123
  ```
78
124
 
79
- ### Fibers (8.1+)
125
+ ### Service Architecture
126
+
127
+ ```
128
+ App\Services\
129
+ ├── UserService.php # Core user operations
130
+ ├── PaymentService.php # Payment processing
131
+ └── AdPlatforms\
132
+ ├── AdPlatformService.php # Main service
133
+ └── Helpers\
134
+ ├── GoogleAdsHelper.php # Extracted complex logic
135
+ └── MetaAdsHelper.php
136
+ ```
137
+
138
+ **Rule:** When a service class exceeds ~200 lines, extract specific logic into `Helpers` sub-namespace.
139
+
140
+ ### Return Types & Exceptions
80
141
 
81
142
  ```php
82
- $fiber = new Fiber(function (): void {
83
- $value = Fiber::suspend('fiber started');
84
- echo "Resumed with: {$value}";
85
- });
143
+ public function findOrFail(string $id): User
144
+ {
145
+ return User::findOrFail($id);
146
+ }
147
+
148
+ public function process(Order $order): PaymentResult
149
+ {
150
+ try {
151
+ return $this->gateway->charge($order);
152
+ } catch (GatewayException $e) {
153
+ $this->logger->error('Payment failed', [
154
+ 'order_id' => $order->id,
155
+ 'error' => $e->getMessage(),
156
+ ]);
157
+ throw new PaymentFailedException($order, $e);
158
+ }
159
+ }
86
160
  ```
87
161
 
88
162
  ## FORBIDDEN Patterns
@@ -92,28 +166,14 @@ $fiber = new Fiber(function (): void {
92
166
  | `$var = isset($x) ? $x : $default` | `$var = $x ?? $default` |
93
167
  | `function foo($x)` (no types) | `function foo(string $x): void` |
94
168
  | `array()` | `[]` |
95
- | `mysql_*` functions | PDO or Doctrine |
96
- | Global variables | Dependency injection |
97
- | `eval()` | Never use eval |
98
169
  | Untyped properties | Always type properties |
99
-
100
- ## Error Handling
101
-
102
- ```php
103
- try {
104
- $result = riskyOperation();
105
- } catch (SpecificException $e) {
106
- logger()->error('Operation failed', [
107
- 'error' => $e->getMessage(),
108
- 'trace' => $e->getTraceAsString(),
109
- ]);
110
- throw new DomainException('Friendly message', 0, $e);
111
- }
112
- ```
170
+ | `static` properties on services | Instance properties (Octane-safe) |
171
+ | Global variables | Dependency injection |
172
+ | `switch` with simple mapping | `match` expression |
173
+ | Large service classes (200+ lines) | Extract into Helpers |
174
+ | `mixed` type without justification | Use specific types or union types |
113
175
 
114
176
  ## PSR Standards
115
177
 
116
- - **PSR-4**: Autoloading
117
- - **PSR-7**: HTTP Messages (if not using framework)
118
- - **PSR-12**: Coding Style
119
- - **PSR-15**: HTTP Handlers
178
+ - **PSR-4**: Autoloading (Laravel default)
179
+ - **PSR-12**: Coding Style (enforced by PHP-CS-Fixer)