start-vibing-stacks 2.1.1 → 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.
@@ -1,92 +1,170 @@
1
1
  # Laravel Octane (RoadRunner) Patterns
2
2
 
3
- ## Critical Rules
3
+ ## How Octane Works
4
4
 
5
- Octane runs your app in a **long-lived process** different from traditional PHP.
5
+ Octane runs your app in a **long-lived worker process**. The application boots ONCE and handles many requests without restarting. This is fundamentally different from traditional PHP where each request boots a fresh process.
6
6
 
7
- ### DO
7
+ **Consequence:** Any state stored in static properties, globals, or singletons persists across requests and can leak between users.
8
8
 
9
- ```php
10
- // Dependency Injection over resolve()
11
- public function __construct(
12
- private readonly UserService $userService,
13
- ) {}
9
+ ## Critical Rules
14
10
 
15
- // Use Request object
16
- public function store(Request $request): JsonResponse
11
+ ### DO: Dependency Injection
12
+
13
+ ```php
14
+ // CORRECT: Constructor injection (resolved fresh per request scope)
15
+ class OrderController extends Controller
17
16
  {
18
- $name = $request->input('name');
17
+ public function __construct(
18
+ private readonly OrderService $orderService,
19
+ private readonly CacheManager $cache,
20
+ ) {}
19
21
  }
20
22
 
21
- // Persistent DB connections with flush
22
- // config/database.php
23
- 'options' => [PDO::ATTR_PERSISTENT => true]
24
- // Octane::prepare to flush connections between requests
23
+ // CORRECT: Method injection in controller actions
24
+ public function store(StoreOrderRequest $request, PaymentGateway $gateway): JsonResponse
25
+ {
26
+ $result = $gateway->charge($request->validated());
27
+ return response()->json($result, 201);
28
+ }
25
29
  ```
26
30
 
27
- ### NEVER
31
+ ### DO: Use the Request Object
28
32
 
29
33
  ```php
30
- // Static state (leaks between requests!)
31
- class Service {
32
- private static array $cache = []; // ❌ LEAKS
34
+ public function store(Request $request): JsonResponse
35
+ {
36
+ $name = $request->input('name');
37
+ $token = $request->bearerToken();
38
+ $ip = $request->ip();
39
+ $user = $request->user();
33
40
  }
34
-
35
- // Global variables
36
- global $user; // ❌
37
-
38
- // Modifying config at runtime
39
- config(['app.name' => 'New']); // ❌ Affects ALL requests
40
-
41
- // die() or exit()
42
- die('error'); // ❌ Kills the worker
43
-
44
- // Superglobals
45
- $_GET['id']; // ❌ Stale between requests
46
- $_POST['name']; // ❌
47
- $_SESSION['user']; // ❌
48
41
  ```
49
42
 
50
- ### AppServiceProvider Connection Flush
43
+ ### DO: Persistent DB Connections with Flush
51
44
 
52
45
  ```php
53
- // app/Providers/AppServiceProvider.php
54
- use Laravel\Octane\Facades\Octane;
55
-
56
- public function boot(): void
57
- {
58
- // MANDATORY: flush DB connections between requests
59
- Octane::prepare(fn ($sandbox) => $sandbox->flushDatabaseConnections());
60
- }
46
+ // config/database.php
47
+ 'mysql' => [
48
+ 'options' => [
49
+ PDO::ATTR_PERSISTENT => true,
50
+ ],
51
+ ],
52
+
53
+ // Octane automatically flushes connections between requests
54
+ // via Octane::prepare() — no manual work needed
61
55
  ```
62
56
 
63
- **Rule:** ALWAYS pair persistent connections with `Octane::prepare` flush in AppServiceProvider.
64
-
65
- ### Memoization in Workers
57
+ ### DO: Instance-scoped Memoization
66
58
 
67
59
  ```php
68
60
  // ✅ Scoped to instance, cleared per request
69
61
  class ProcessLeadsJob implements ShouldQueue
70
62
  {
71
63
  private array $lookupCache = [];
72
-
64
+
73
65
  public function handle(): void
74
66
  {
75
67
  foreach ($this->leads as $lead) {
76
- $result = $this->lookupCache[$lead->key]
68
+ $result = $this->lookupCache[$lead->key]
77
69
  ??= $this->expensiveLookup($lead->key);
78
70
  }
79
- // Cache is GC'd when job instance is destroyed
71
+ // Cache is garbage collected when job instance is destroyed
80
72
  }
81
73
  }
74
+ ```
75
+
76
+ ## NEVER Do These in Octane
77
+
78
+ ### No Static State
79
+
80
+ ```php
81
+ // WRONG: Static properties persist across ALL requests
82
+ class AuthService {
83
+ private static ?User $currentUser = null; // LEAKS between users!
84
+ private static array $cache = []; // Grows forever!
85
+ }
82
86
 
83
- // Static cache (persists across jobs in Octane!)
84
- private static array $cache = [];
87
+ // CORRECT: Instance properties (scoped to request)
88
+ class AuthService {
89
+ private ?User $currentUser = null;
90
+ private array $cache = [];
91
+ }
92
+ ```
93
+
94
+ ### No Global Variables
95
+
96
+ ```php
97
+ // WRONG
98
+ global $config;
99
+ $GLOBALS['user'] = $user;
100
+
101
+ // CORRECT: Use DI or config()
102
+ $value = config('app.name');
103
+ ```
104
+
105
+ ### No Runtime Config Mutation
106
+
107
+ ```php
108
+ // WRONG: Affects ALL concurrent requests in the worker
109
+ config(['app.timezone' => 'America/Sao_Paulo']);
110
+ config(['services.stripe.key' => $newKey]);
111
+
112
+ // CORRECT: Pass values explicitly
113
+ $this->service->processWithTimezone('America/Sao_Paulo');
114
+ ```
115
+
116
+ ### No Process Termination
117
+
118
+ ```php
119
+ // WRONG: Kills the entire worker process
120
+ die('Something went wrong');
121
+ exit(1);
122
+ dd($variable);
123
+
124
+ // CORRECT: Throw exceptions (handled by Laravel)
125
+ throw new RuntimeException('Something went wrong');
126
+ abort(500, 'Internal error');
127
+
128
+ // For debugging: use dump() without die
129
+ dump($variable); // Outputs without killing worker
130
+ Log::debug('Debug info', ['var' => $variable]);
131
+ ```
132
+
133
+ ### No Superglobals
134
+
135
+ ```php
136
+ // WRONG: Stale data from previous requests
137
+ $_GET['id'];
138
+ $_POST['name'];
139
+ $_SESSION['user'];
140
+ $_SERVER['HTTP_AUTHORIZATION'];
141
+ $_COOKIE['token'];
142
+
143
+ // CORRECT: Use Request object
144
+ $request->input('id');
145
+ $request->post('name');
146
+ $request->session()->get('user');
147
+ $request->header('Authorization');
148
+ $request->cookie('token');
149
+ ```
150
+
151
+ ### No Singleton Abuse
152
+
153
+ ```php
154
+ // WRONG: Singleton state leaks
155
+ app()->singleton('cart', fn () => new Cart());
156
+ // The same Cart instance serves ALL users!
157
+
158
+ // CORRECT: Use scoped bindings
159
+ app()->scoped('cart', fn () => new Cart());
160
+ // Fresh instance per request, cleared between requests
85
161
  ```
86
162
 
87
163
  ## RoadRunner Configuration (rr.yaml)
88
164
 
89
165
  ```yaml
166
+ version: '3'
167
+
90
168
  server:
91
169
  command: "php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000"
92
170
 
@@ -94,17 +172,41 @@ http:
94
172
  address: 0.0.0.0:8000
95
173
  pool:
96
174
  num_workers: 4
97
- max_jobs: 500 # Restart worker after 500 requests (memory safety)
175
+ max_jobs: 500
98
176
  supervisor:
99
- max_worker_memory: 128 # MB
177
+ max_worker_memory: 128
100
178
 
101
179
  logs:
102
180
  level: info
181
+ encoding: json
103
182
  ```
104
183
 
184
+ ### Worker Tuning
185
+
186
+ | Setting | Default | Description |
187
+ |---------|---------|-------------|
188
+ | `num_workers` | CPU cores | Number of worker processes |
189
+ | `max_jobs` | 500 | Restart worker after N requests (memory safety) |
190
+ | `max_worker_memory` | 128 MB | Kill worker if exceeds memory |
191
+
192
+ **Rule:** Always set `max_jobs` to prevent memory leaks from accumulating.
193
+
105
194
  ## Database Safety
106
195
 
107
196
  - **NEVER** execute `db:wipe`, `migrate:fresh`, `migrate:refresh`, `db:reset`
108
197
  - **ALWAYS** use incremental migrations (`make:migration`)
109
- - If migration fails fix the file or create a new one
198
+ - If migration fails, fix the file or create a new one
110
199
  - Assume **all environments contain critical data**
200
+
201
+ ## Octane Checklist
202
+
203
+ - [ ] No `static` properties on service classes
204
+ - [ ] No global variables or `$GLOBALS`
205
+ - [ ] No `config()` mutations at runtime
206
+ - [ ] No `die()`, `exit()`, or `dd()` in production code
207
+ - [ ] No superglobals (`$_GET`, `$_POST`, `$_SESSION`, `$_SERVER`)
208
+ - [ ] No `app()->singleton()` for request-scoped data (use `scoped`)
209
+ - [ ] All services use constructor DI (not `app()` or `resolve()`)
210
+ - [ ] Memoization scoped to instance, not static
211
+ - [ ] `max_jobs` configured in `rr.yaml` for memory safety
212
+ - [ ] Persistent DB connections enabled with Octane flush
@@ -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).