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.
- package/README.md +2 -2
- package/dist/detector.js +4 -6
- package/dist/index.js +63 -2
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +480 -0
- package/dist/setup.js +29 -0
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +342 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +267 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +155 -53
- package/stacks/php/skills/laravel-patterns/SKILL.md +244 -39
- package/stacks/php/skills/php-patterns/SKILL.md +113 -53
- package/stacks/php/skills/security-scan-php/SKILL.md +161 -43
- package/stacks/php/stack.json +19 -6
- package/templates/CLAUDE-php.md +108 -29
|
@@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
|
10
10
|
class User extends Model
|
|
11
11
|
{
|
|
12
12
|
use HasUuids;
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
//
|
|
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
|
|
192
|
+
├── UserService.php # Core service
|
|
56
193
|
├── PaymentService.php
|
|
57
194
|
└── AdPlatforms\
|
|
58
|
-
├── AdPlatformService.php
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
131
|
-
|
|
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
|
-
|
|
283
|
+
## Job Design
|
|
135
284
|
|
|
136
285
|
```php
|
|
137
|
-
// ✅ Idempotent jobs — safe to retry
|
|
138
286
|
class SendConversionJob implements ShouldQueue
|
|
139
287
|
{
|
|
140
|
-
|
|
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
|
-
//
|
|
295
|
+
// Idempotent: check status BEFORE processing
|
|
143
296
|
if ($this->lead->conversion_sent) {
|
|
144
|
-
return;
|
|
297
|
+
return;
|
|
145
298
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
$api->sendConversion(
|
|
299
|
+
|
|
300
|
+
$gateway->send(
|
|
149
301
|
unique_key: $this->lead->order_id,
|
|
150
|
-
data: $this->lead->
|
|
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
|
|
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
|
-
#
|
|
367
|
+
# ALWAYS incremental
|
|
202
368
|
php artisan make:migration add_status_to_leads_table
|
|
203
369
|
|
|
204
|
-
#
|
|
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.
|
|
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
|
|
14
|
+
class CreateUserDTO {
|
|
15
15
|
public function __construct(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
27
|
-
case
|
|
28
|
-
case
|
|
29
|
-
case
|
|
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
|
|
48
|
+
### Readonly Classes for DTOs
|
|
34
49
|
|
|
35
50
|
```php
|
|
36
|
-
readonly class
|
|
51
|
+
readonly class PaymentResult {
|
|
37
52
|
public function __construct(
|
|
38
|
-
public string $
|
|
39
|
-
public
|
|
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
|
|
48
|
-
public const
|
|
49
|
-
public const int
|
|
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($
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
|
|
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 =
|
|
67
|
-
|
|
84
|
+
$response = Response::json(
|
|
85
|
+
data: $collection,
|
|
68
86
|
status: 200,
|
|
69
|
-
headers: ['
|
|
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
|
-
###
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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-
|
|
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)
|