start-vibing-stacks 2.2.0 → 2.4.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 +23 -11
- package/dist/index.js +78 -5
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +501 -0
- package/dist/setup.js +46 -1
- package/dist/types.d.ts +24 -0
- package/dist/ui.js +6 -5
- package/package.json +1 -1
- package/stacks/_shared/config/security-rules.json +27 -5
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +31 -35
- package/stacks/frontend/react/skills/react-standards/SKILL.md +20 -20
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +78 -42
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +1 -1
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +84 -18
- 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/nodejs/skills/nextjs-app-router/SKILL.md +101 -0
- package/stacks/nodejs/stack.json +43 -121
- 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-nodejs.md +323 -0
- package/templates/CLAUDE-php.md +233 -33
|
@@ -114,6 +114,104 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Environment Variables & API Security (MANDATORY)
|
|
118
|
+
|
|
119
|
+
> **NEXT_PUBLIC_ vars are embedded in the browser JS bundle.** Anyone can see them in DevTools.
|
|
120
|
+
|
|
121
|
+
### Server vs Client Environment
|
|
122
|
+
|
|
123
|
+
| Prefix | Accessible from | Safe for |
|
|
124
|
+
|--------|----------------|----------|
|
|
125
|
+
| No prefix | Server Components, Route Handlers, Server Actions | API keys, secrets, tokens, DB URLs |
|
|
126
|
+
| `NEXT_PUBLIC_*` | Server + Browser (embedded in JS) | Public URLs, analytics IDs, publishable keys |
|
|
127
|
+
|
|
128
|
+
### FORBIDDEN Environment Patterns
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# NEVER DO THIS — secret exposed in browser bundle
|
|
132
|
+
NEXT_PUBLIC_OPENAI_KEY=sk-abc123
|
|
133
|
+
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc
|
|
134
|
+
NEXT_PUBLIC_DATABASE_URL=postgresql://user:pass@host/db
|
|
135
|
+
|
|
136
|
+
# CORRECT — server-only (no NEXT_PUBLIC_ prefix)
|
|
137
|
+
OPENAI_KEY=sk-abc123
|
|
138
|
+
STRIPE_SECRET_KEY=sk_live_abc
|
|
139
|
+
DATABASE_URL=postgresql://user:pass@host/db
|
|
140
|
+
|
|
141
|
+
# OK as NEXT_PUBLIC_ — no secret value
|
|
142
|
+
NEXT_PUBLIC_APP_URL=https://myapp.com
|
|
143
|
+
NEXT_PUBLIC_STRIPE_KEY=pk_live_abc
|
|
144
|
+
NEXT_PUBLIC_GA_ID=G-XXXXX
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### API Proxy Pattern (MANDATORY)
|
|
148
|
+
|
|
149
|
+
External API calls with secrets MUST go through server-side Route Handlers:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
// app/api/ai/route.ts — Secret stays on server
|
|
153
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
154
|
+
|
|
155
|
+
export async function POST(req: NextRequest) {
|
|
156
|
+
const { prompt } = await req.json();
|
|
157
|
+
|
|
158
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
Authorization: `Bearer ${process.env['OPENAI_KEY']}`,
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
model: 'gpt-4',
|
|
166
|
+
messages: [{ role: 'user', content: prompt }],
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
return NextResponse.json({ error: 'AI request failed' }, { status: 502 });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return NextResponse.json(await response.json());
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// components/chat.tsx — Client calls YOUR route, not external API
|
|
180
|
+
'use client';
|
|
181
|
+
|
|
182
|
+
async function sendMessage(prompt: string) {
|
|
183
|
+
const res = await fetch('/api/ai', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({ prompt }),
|
|
187
|
+
});
|
|
188
|
+
return res.json();
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Server Actions for Mutations
|
|
193
|
+
|
|
194
|
+
Server Actions also keep secrets server-side:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// app/actions/payment.ts
|
|
198
|
+
'use server';
|
|
199
|
+
|
|
200
|
+
import Stripe from 'stripe';
|
|
201
|
+
|
|
202
|
+
const stripe = new Stripe(process.env['STRIPE_SECRET_KEY']!);
|
|
203
|
+
|
|
204
|
+
export async function createCheckout(priceId: string) {
|
|
205
|
+
const session = await stripe.checkout.sessions.create({
|
|
206
|
+
mode: 'payment',
|
|
207
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
208
|
+
success_url: `${process.env['APP_URL']}/success`,
|
|
209
|
+
cancel_url: `${process.env['APP_URL']}/cancel`,
|
|
210
|
+
});
|
|
211
|
+
return { url: session.url };
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
117
215
|
## FORBIDDEN
|
|
118
216
|
|
|
119
217
|
1. **`'use client'` on server-capable components** — default to server
|
|
@@ -121,3 +219,6 @@ export async function generateMetadata({ params }: { params: { id: string } }) {
|
|
|
121
219
|
3. **`getServerSideProps` / `getStaticProps`** — App Router uses async components
|
|
122
220
|
4. **API routes for server-only data** — use server components directly
|
|
123
221
|
5. **Prop drilling through layouts** — use parallel routes or context
|
|
222
|
+
6. **`NEXT_PUBLIC_` with API keys, secrets, or tokens** — secrets leak to browser bundle
|
|
223
|
+
7. **Calling external APIs from client components** — use Route Handlers as proxy
|
|
224
|
+
8. **`process.env['SECRET']` in `'use client'` files** — only `NEXT_PUBLIC_*` vars work client-side
|
package/stacks/nodejs/stack.json
CHANGED
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "nodejs",
|
|
3
3
|
"name": "Node.js / TypeScript",
|
|
4
|
-
"icon": "
|
|
4
|
+
"icon": "📦",
|
|
5
5
|
"runtime": "Bun / Node.js 20+",
|
|
6
6
|
"minVersion": "20.0.0",
|
|
7
7
|
"packageManager": "bun|npm|pnpm",
|
|
8
|
-
"extensions": [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
".js",
|
|
12
|
-
".jsx",
|
|
13
|
-
".mjs",
|
|
14
|
-
".cjs"
|
|
15
|
-
],
|
|
16
|
-
"testExtensions": [
|
|
17
|
-
"*.test.ts",
|
|
18
|
-
"*.spec.ts",
|
|
19
|
-
"*.test.tsx"
|
|
20
|
-
],
|
|
21
|
-
"detectFiles": [
|
|
22
|
-
"package.json",
|
|
23
|
-
"tsconfig.json",
|
|
24
|
-
"bun.lockb",
|
|
25
|
-
"next.config.js"
|
|
26
|
-
],
|
|
8
|
+
"extensions": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
9
|
+
"testExtensions": ["*.test.ts", "*.spec.ts", "*.test.tsx"],
|
|
10
|
+
"detectFiles": ["package.json", "tsconfig.json", "bun.lockb", "bun.lock", "next.config.js"],
|
|
27
11
|
"commands": {
|
|
28
12
|
"test": "bun run test",
|
|
29
13
|
"lint": "bun run lint",
|
|
@@ -33,146 +17,95 @@
|
|
|
33
17
|
"typecheck": "bun run typecheck"
|
|
34
18
|
},
|
|
35
19
|
"qualityGates": [
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"order": 1
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"name": "Lint",
|
|
44
|
-
"command": "bun run lint",
|
|
45
|
-
"required": true,
|
|
46
|
-
"order": 2
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"name": "Tests",
|
|
50
|
-
"command": "bun run test",
|
|
51
|
-
"required": true,
|
|
52
|
-
"order": 3
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "Build",
|
|
56
|
-
"command": "bun run build",
|
|
57
|
-
"required": true,
|
|
58
|
-
"order": 4
|
|
59
|
-
}
|
|
20
|
+
{ "name": "TypeCheck", "command": "bun run typecheck", "required": true, "order": 1 },
|
|
21
|
+
{ "name": "Lint", "command": "bun run lint", "required": true, "order": 2 },
|
|
22
|
+
{ "name": "Tests", "command": "bun run test", "required": true, "order": 3 },
|
|
23
|
+
{ "name": "Build", "command": "bun run build", "required": true, "order": 4 }
|
|
60
24
|
],
|
|
61
25
|
"frameworks": [
|
|
62
26
|
{
|
|
63
27
|
"id": "nextjs",
|
|
64
28
|
"name": "Next.js (App Router)",
|
|
65
|
-
"icon": "
|
|
66
|
-
"detectFiles": [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"next.config.mjs"
|
|
70
|
-
]
|
|
29
|
+
"icon": "▲",
|
|
30
|
+
"detectFiles": ["next.config.js", "next.config.ts", "next.config.mjs"],
|
|
31
|
+
"default": true,
|
|
32
|
+
"skills": ["nextjs-app-router"]
|
|
71
33
|
},
|
|
72
34
|
{
|
|
73
35
|
"id": "nuxt",
|
|
74
36
|
"name": "Nuxt",
|
|
75
|
-
"icon": "
|
|
76
|
-
"detectFiles": [
|
|
77
|
-
|
|
78
|
-
]
|
|
37
|
+
"icon": "💚",
|
|
38
|
+
"detectFiles": ["nuxt.config.ts"],
|
|
39
|
+
"skills": []
|
|
79
40
|
},
|
|
80
41
|
{
|
|
81
42
|
"id": "astro",
|
|
82
43
|
"name": "Astro",
|
|
83
|
-
"icon": "
|
|
84
|
-
"detectFiles": [
|
|
85
|
-
|
|
86
|
-
]
|
|
44
|
+
"icon": "🚀",
|
|
45
|
+
"detectFiles": ["astro.config.mjs"],
|
|
46
|
+
"skills": []
|
|
87
47
|
},
|
|
88
48
|
{
|
|
89
49
|
"id": "express",
|
|
90
50
|
"name": "Express",
|
|
91
|
-
"icon": "
|
|
51
|
+
"icon": "⚡",
|
|
52
|
+
"skills": ["trpc-api"]
|
|
92
53
|
},
|
|
93
54
|
{
|
|
94
55
|
"id": "fastify",
|
|
95
56
|
"name": "Fastify",
|
|
96
|
-
"icon": "
|
|
57
|
+
"icon": "🏎️",
|
|
58
|
+
"skills": ["trpc-api"]
|
|
97
59
|
},
|
|
98
60
|
{
|
|
99
61
|
"id": "vanilla",
|
|
100
62
|
"name": "Vanilla Node.js",
|
|
101
|
-
"icon": "
|
|
63
|
+
"icon": "📄",
|
|
64
|
+
"skills": []
|
|
102
65
|
}
|
|
103
66
|
],
|
|
104
67
|
"databases": [
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
"id": "postgresql",
|
|
112
|
-
"name": "PostgreSQL",
|
|
113
|
-
"icon": "\ud83d\udc18"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
"id": "mysql",
|
|
117
|
-
"name": "MySQL / MariaDB",
|
|
118
|
-
"icon": "\ud83d\udc2c"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"id": "sqlite",
|
|
122
|
-
"name": "SQLite (Turso / libSQL)",
|
|
123
|
-
"icon": "\ud83d\udcc1"
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"id": "redis",
|
|
127
|
-
"name": "Redis (Upstash)",
|
|
128
|
-
"icon": "\ud83d\udd34"
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
"id": "none",
|
|
132
|
-
"name": "None",
|
|
133
|
-
"icon": "\u274c"
|
|
134
|
-
}
|
|
68
|
+
{ "id": "mongodb", "name": "MongoDB", "icon": "🍃" },
|
|
69
|
+
{ "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
|
|
70
|
+
{ "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬" },
|
|
71
|
+
{ "id": "sqlite", "name": "SQLite (Turso / libSQL)", "icon": "📁" },
|
|
72
|
+
{ "id": "redis", "name": "Redis (Upstash)", "icon": "🔴" },
|
|
73
|
+
{ "id": "none", "name": "None", "icon": "❌" }
|
|
135
74
|
],
|
|
136
75
|
"frontendOptions": [
|
|
137
76
|
{
|
|
138
77
|
"id": "react-tailwind",
|
|
139
78
|
"name": "React 19+ / TailwindCSS 4+",
|
|
140
|
-
"icon": "
|
|
79
|
+
"icon": "⚛️",
|
|
80
|
+
"skillsDir": "react",
|
|
81
|
+
"default": true,
|
|
82
|
+
"frameworks": ["nextjs", "express", "fastify", "vanilla"]
|
|
141
83
|
},
|
|
142
84
|
{
|
|
143
85
|
"id": "vue",
|
|
144
86
|
"name": "Vue.js / Nuxt",
|
|
145
|
-
"icon": "
|
|
87
|
+
"icon": "💚",
|
|
88
|
+
"frameworks": ["nuxt"]
|
|
146
89
|
},
|
|
147
90
|
{
|
|
148
91
|
"id": "svelte",
|
|
149
92
|
"name": "Svelte / SvelteKit",
|
|
150
|
-
"icon": "
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
"id": "shadcn",
|
|
154
|
-
"name": "shadcn/ui + Tailwind",
|
|
155
|
-
"icon": "\ud83c\udfa8"
|
|
93
|
+
"icon": "🔥",
|
|
94
|
+
"frameworks": ["astro", "vanilla"]
|
|
156
95
|
},
|
|
157
96
|
{
|
|
158
97
|
"id": "none",
|
|
159
|
-
"name": "API only
|
|
160
|
-
"icon": "
|
|
98
|
+
"name": "API only — no frontend",
|
|
99
|
+
"icon": "❌"
|
|
161
100
|
}
|
|
162
101
|
],
|
|
163
102
|
"deployTargets": [
|
|
164
|
-
{
|
|
165
|
-
"id": "github",
|
|
166
|
-
"name": "GitHub (git push)",
|
|
167
|
-
"icon": "\ud83d\udc19"
|
|
168
|
-
}
|
|
103
|
+
{ "id": "github", "name": "GitHub (git push)", "icon": "🐙" }
|
|
169
104
|
],
|
|
170
105
|
"skills": [
|
|
171
106
|
"typescript-strict",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"zod-validation",
|
|
175
|
-
"vitest-testing"
|
|
107
|
+
"bun-runtime",
|
|
108
|
+
"zod-validation"
|
|
176
109
|
],
|
|
177
110
|
"requirements": [
|
|
178
111
|
{
|
|
@@ -185,17 +118,6 @@
|
|
|
185
118
|
"linux": "curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
|
|
186
119
|
},
|
|
187
120
|
"versionRegex": "v?(\\d+\\.\\d+\\.\\d+)"
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
"name": "Bun",
|
|
191
|
-
"command": "bun",
|
|
192
|
-
"versionFlag": "--version",
|
|
193
|
-
"minVersion": "1.0.0",
|
|
194
|
-
"installCommand": {
|
|
195
|
-
"macos": "brew install oven-sh/bun/bun",
|
|
196
|
-
"linux": "curl -fsSL https://bun.sh/install | bash"
|
|
197
|
-
},
|
|
198
|
-
"versionRegex": "(\\d+\\.\\d+\\.\\d+)"
|
|
199
121
|
}
|
|
200
122
|
]
|
|
201
123
|
}
|
|
@@ -1,92 +1,170 @@
|
|
|
1
1
|
# Laravel Octane (RoadRunner) Patterns
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## How Octane Works
|
|
4
4
|
|
|
5
|
-
Octane runs your app in a **long-lived process
|
|
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
|
-
|
|
7
|
+
**Consequence:** Any state stored in static properties, globals, or singletons persists across requests and can leak between users.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
// Dependency Injection over resolve()
|
|
11
|
-
public function __construct(
|
|
12
|
-
private readonly UserService $userService,
|
|
13
|
-
) {}
|
|
9
|
+
## Critical Rules
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
### DO: Dependency Injection
|
|
12
|
+
|
|
13
|
+
```php
|
|
14
|
+
// CORRECT: Constructor injection (resolved fresh per request scope)
|
|
15
|
+
class OrderController extends Controller
|
|
17
16
|
{
|
|
18
|
-
|
|
17
|
+
public function __construct(
|
|
18
|
+
private readonly OrderService $orderService,
|
|
19
|
+
private readonly CacheManager $cache,
|
|
20
|
+
) {}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
###
|
|
31
|
+
### DO: Use the Request Object
|
|
28
32
|
|
|
29
33
|
```php
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
###
|
|
43
|
+
### DO: Persistent DB Connections with Flush
|
|
51
44
|
|
|
52
45
|
```php
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
84
|
-
|
|
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
|
|
175
|
+
max_jobs: 500
|
|
98
176
|
supervisor:
|
|
99
|
-
max_worker_memory: 128
|
|
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
|
|
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
|