@relipa/ai-flow-kit 0.2.0 → 0.2.1

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.
Files changed (38) hide show
  1. package/custom/rules/java/spring-boot-rules.md +209 -0
  2. package/custom/rules/javascript/nestjs-examples.md +41 -0
  3. package/custom/rules/javascript/nestjs-rules.md +42 -0
  4. package/custom/rules/javascript/nodejs-express-examples.md +35 -0
  5. package/custom/rules/javascript/nodejs-express-rules.md +49 -0
  6. package/custom/rules/javascript/reactjs-examples.md +380 -0
  7. package/custom/rules/javascript/reactjs-rules.md +173 -0
  8. package/custom/rules/php/php-examples.md +161 -0
  9. package/custom/rules/php/php-rules.md +127 -0
  10. package/custom/rules/python/python-django-examples.md +34 -0
  11. package/custom/rules/python/python-django-rules.md +48 -0
  12. package/custom/rules/python/python-examples.md +32 -0
  13. package/custom/rules/python/python-fastapi-examples.md +30 -0
  14. package/custom/rules/python/python-fastapi-rules.md +35 -0
  15. package/custom/rules/python/python-ml-examples.md +187 -0
  16. package/custom/rules/python/python-ml-rules.md +121 -0
  17. package/custom/rules/python/python-rules.md +58 -0
  18. package/custom/skills/ba-skills/skill-ba-qna-template-v1.md +4 -4
  19. package/custom/skills/ba-skills/skill-ba-qna-v1.md +6 -0
  20. package/custom/skills/create-system-requirement/SKILL.md +52 -16
  21. package/custom/skills/create-system-requirement/system-requirement-template-v1.md +128 -0
  22. package/custom/skills/impact-analysis/SKILL.md +106 -106
  23. package/custom/skills/report-customer/SKILL.md +99 -99
  24. package/custom/templates/nestjs.md +5 -72
  25. package/custom/templates/nodejs-express.md +5 -73
  26. package/custom/templates/php-plain.md +5 -261
  27. package/custom/templates/php.md +5 -261
  28. package/custom/templates/python-django.md +5 -71
  29. package/custom/templates/python-fastapi.md +5 -54
  30. package/custom/templates/python-ml.md +1 -269
  31. package/custom/templates/python.md +5 -79
  32. package/custom/templates/reactjs.md +5 -492
  33. package/custom/templates/shared/gate-workflow.md +1 -0
  34. package/custom/templates/shared/ml-gate-workflow.md +1 -0
  35. package/custom/templates/spring-boot.md +5 -224
  36. package/docs/common/CHANGELOG.md +20 -10
  37. package/package.json +1 -1
  38. package/scripts/init.js +143 -40
@@ -0,0 +1,380 @@
1
+ # React.js Code Examples
2
+
3
+ Reference examples for each rule area. Read the relevant section when generating code for that area.
4
+
5
+ ---
6
+
7
+ ## Component Rules
8
+
9
+ ### Always use Functional Components
10
+
11
+ ```tsx
12
+ // ✅ Good
13
+ const UserCard = ({ user }: UserCardProps) => {
14
+ return <div>{user.name}</div>;
15
+ };
16
+
17
+ // ❌ Bad — class component
18
+ class UserCard extends React.Component {}
19
+ ```
20
+
21
+ ### Define Props with TypeScript interface
22
+
23
+ ```tsx
24
+ // ✅ Good
25
+ interface UserCardProps {
26
+ user: User;
27
+ onDelete?: (id: number) => void;
28
+ className?: string;
29
+ }
30
+
31
+ const UserCard = ({ user, onDelete, className }: UserCardProps) => {
32
+ // ...
33
+ };
34
+
35
+ // ❌ Bad — any, or no type
36
+ const UserCard = ({ user }: any) => {};
37
+ const UserCard = (props) => {};
38
+ ```
39
+
40
+ ### Keep components focused and small
41
+
42
+ ```tsx
43
+ // ✅ Good — logic extracted to hook
44
+ const UserList = () => {
45
+ const { users, isLoading, error, deleteUser } = useUserList();
46
+
47
+ if (isLoading) return <Spinner />;
48
+ if (error) return <ErrorMessage message={error.message} />;
49
+
50
+ return (
51
+ <ul>
52
+ {users.map(user => (
53
+ <UserCard key={user.id} user={user} onDelete={deleteUser} />
54
+ ))}
55
+ </ul>
56
+ );
57
+ };
58
+
59
+ // ❌ Bad — logic mixed into component
60
+ const UserList = () => {
61
+ const [users, setUsers] = useState([]);
62
+ const [loading, setLoading] = useState(true);
63
+
64
+ useEffect(() => {
65
+ fetch('/api/users')
66
+ .then(r => r.json())
67
+ .then(data => { setUsers(data); setLoading(false); });
68
+ }, []);
69
+
70
+ const handleDelete = async (id) => {
71
+ await fetch(`/api/users/${id}`, { method: 'DELETE' });
72
+ setUsers(prev => prev.filter(u => u.id !== id));
73
+ };
74
+ // ... long JSX
75
+ };
76
+ ```
77
+
78
+ ### Export convention
79
+
80
+ ```tsx
81
+ // Named export for regular components
82
+ export const UserCard = ({ user }: UserCardProps) => { ... };
83
+
84
+ // Default export only for page-level components
85
+ export default function UserPage() { ... }
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Custom Hook Rules
91
+
92
+ ```tsx
93
+ // ✅ Good
94
+ const useUserList = () => {
95
+ const { data: users = [], isLoading, error } = useQuery({
96
+ queryKey: ['users'],
97
+ queryFn: userApi.getAll,
98
+ });
99
+
100
+ const { mutate: deleteUser } = useMutation({
101
+ mutationFn: userApi.delete,
102
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
103
+ });
104
+
105
+ return { users, isLoading, error, deleteUser };
106
+ };
107
+
108
+ // Usage
109
+ const { users, isLoading, deleteUser } = useUserList();
110
+ ```
111
+
112
+ ---
113
+
114
+ ## State Management Rules
115
+
116
+ ### Server state → TanStack Query
117
+
118
+ ```tsx
119
+ // Fetching
120
+ const { data, isLoading, error } = useQuery({
121
+ queryKey: ['users', filters], // include deps in key
122
+ queryFn: () => userApi.getAll(filters),
123
+ staleTime: 5 * 60 * 1000, // 5 minutes
124
+ });
125
+
126
+ // Mutation
127
+ const { mutate, isPending } = useMutation({
128
+ mutationFn: userApi.create,
129
+ onSuccess: () => {
130
+ queryClient.invalidateQueries({ queryKey: ['users'] });
131
+ toast.success('User created');
132
+ },
133
+ onError: (error) => toast.error(error.message),
134
+ });
135
+ ```
136
+
137
+ ### Client/UI state → Zustand (or useState for local)
138
+
139
+ ```tsx
140
+ // Local state — use useState
141
+ const [isOpen, setIsOpen] = useState(false);
142
+
143
+ // Shared UI state — use Zustand
144
+ const useAuthStore = create<AuthStore>((set) => ({
145
+ user: null,
146
+ token: null,
147
+ login: (user, token) => set({ user, token }),
148
+ logout: () => set({ user: null, token: null }),
149
+ }));
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Form Rules
155
+
156
+ Use **React Hook Form + Zod** for all forms:
157
+
158
+ ```tsx
159
+ // 1. Define schema with Zod
160
+ const createUserSchema = z.object({
161
+ email: z.string().email('Invalid email'),
162
+ fullName: z.string().min(2, 'Min 2 characters').max(100),
163
+ password: z.string().min(8, 'Min 8 characters'),
164
+ });
165
+
166
+ type CreateUserForm = z.infer<typeof createUserSchema>;
167
+
168
+ // 2. Use in component
169
+ const CreateUserForm = () => {
170
+ const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<CreateUserForm>({
171
+ resolver: zodResolver(createUserSchema),
172
+ });
173
+
174
+ const onSubmit = async (data: CreateUserForm) => {
175
+ await createUser(data);
176
+ };
177
+
178
+ return (
179
+ <form onSubmit={handleSubmit(onSubmit)}>
180
+ <input {...register('email')} />
181
+ {errors.email && <span>{errors.email.message}</span>}
182
+
183
+ <button type="submit" disabled={isSubmitting}>
184
+ {isSubmitting ? 'Saving...' : 'Create'}
185
+ </button>
186
+ </form>
187
+ );
188
+ };
189
+ ```
190
+
191
+ ---
192
+
193
+ ## API Layer Rules
194
+
195
+ One `axios` instance shared across the app with interceptors:
196
+
197
+ ```tsx
198
+ // src/services/api.ts
199
+ const api = axios.create({
200
+ baseURL: import.meta.env.VITE_API_URL,
201
+ timeout: 10000,
202
+ headers: { 'Content-Type': 'application/json' },
203
+ });
204
+
205
+ // Attach token automatically
206
+ api.interceptors.request.use((config) => {
207
+ const token = useAuthStore.getState().token;
208
+ if (token) config.headers.Authorization = `Bearer ${token}`;
209
+ return config;
210
+ });
211
+
212
+ // Handle 401 globally
213
+ api.interceptors.response.use(
214
+ (response) => response,
215
+ (error) => {
216
+ if (error.response?.status === 401) {
217
+ useAuthStore.getState().logout();
218
+ window.location.href = '/login';
219
+ }
220
+ return Promise.reject(error);
221
+ }
222
+ );
223
+ ```
224
+
225
+ Feature API module:
226
+
227
+ ```tsx
228
+ // src/features/users/api.ts
229
+ export const userApi = {
230
+ getAll: (params?: UserListParams): Promise<User[]> =>
231
+ api.get('/users', { params }).then(r => r.data),
232
+
233
+ getById: (id: number): Promise<User> =>
234
+ api.get(`/users/${id}`).then(r => r.data),
235
+
236
+ create: (data: CreateUserRequest): Promise<User> =>
237
+ api.post('/users', data).then(r => r.data),
238
+
239
+ update: (id: number, data: UpdateUserRequest): Promise<User> =>
240
+ api.put(`/users/${id}`, data).then(r => r.data),
241
+
242
+ delete: (id: number): Promise<void> =>
243
+ api.delete(`/users/${id}`).then(r => r.data),
244
+ };
245
+ ```
246
+
247
+ ---
248
+
249
+ ## TypeScript Rules
250
+
251
+ ```tsx
252
+ // ✅ Good
253
+ interface User {
254
+ id: number;
255
+ email: string;
256
+ fullName: string;
257
+ createdAt: string;
258
+ }
259
+
260
+ type UserStatus = 'active' | 'inactive' | 'banned';
261
+
262
+ const getUser = async (id: number): Promise<User> => {
263
+ return api.get(`/users/${id}`).then(r => r.data);
264
+ };
265
+
266
+ // ❌ Bad
267
+ const getUser = async (id: any) => {
268
+ return api.get(`/users/${id}`).then((r: any) => r.data);
269
+ };
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Styling Rules (Tailwind CSS)
275
+
276
+ ```tsx
277
+ // ✅ Good — cn() for conditional classes
278
+ import { cn } from '\@/utils/cn';
279
+
280
+ const Button = ({ variant = 'primary', disabled, className, children }: ButtonProps) => (
281
+ <button
282
+ className={cn(
283
+ 'px-4 py-2 rounded font-medium transition-colors',
284
+ variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
285
+ variant === 'outline' && 'border border-gray-300 hover:bg-gray-50',
286
+ disabled && 'opacity-50 cursor-not-allowed',
287
+ className
288
+ )}
289
+ disabled={disabled}
290
+ >
291
+ {children}
292
+ </button>
293
+ );
294
+
295
+ // ❌ Bad — inline style
296
+ <button style={{ backgroundColor: 'blue', padding: '8px 16px' }}>
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Performance Rules
302
+
303
+ ```tsx
304
+ // Lazy routes
305
+ const UserPage = React.lazy(() => import('./pages/UserPage'));
306
+
307
+ <Suspense fallback={<PageSkeleton />}>
308
+ <UserPage />
309
+ </Suspense>
310
+
311
+ // useMemo for expensive computation
312
+ const sortedUsers = useMemo(
313
+ () => [...users].sort((a, b) => a.fullName.localeCompare(b.fullName)),
314
+ [users]
315
+ );
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Error Handling Rules
321
+
322
+ ```tsx
323
+ // Error boundary at route level
324
+ <ErrorBoundary fallback={<ErrorPage />}>
325
+ <Routes>
326
+ <Route path="/users" element={<UserPage />} />
327
+ </Routes>
328
+ </ErrorBoundary>
329
+
330
+ // Async error handling
331
+ const { mutate } = useMutation({
332
+ mutationFn: userApi.create,
333
+ onSuccess: () => toast.success('User created successfully'),
334
+ onError: (error: AxiosError<ApiError>) =>
335
+ toast.error(error.response?.data?.message ?? 'Something went wrong'),
336
+ });
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Testing Rules
342
+
343
+ ### Unit/Component tests with React Testing Library
344
+
345
+ ```tsx
346
+ // Test behavior, not implementation
347
+ describe('UserCard', () => {
348
+ it('should display user name and email', () => {
349
+ const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };
350
+ render(<UserCard user={user} />);
351
+
352
+ expect(screen.getByText('Test User')).toBeInTheDocument();
353
+ expect(screen.getByText('test\@example.com')).toBeInTheDocument();
354
+ });
355
+
356
+ it('should call onDelete with user id when delete button clicked', async () => {
357
+ const onDelete = vi.fn();
358
+ const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };
359
+
360
+ render(<UserCard user={user} onDelete={onDelete} />);
361
+ await userEvent.click(screen.getByRole('button', { name: /delete/i }));
362
+
363
+ expect(onDelete).toHaveBeenCalledWith(1);
364
+ });
365
+ });
366
+ ```
367
+
368
+ ### Hook tests
369
+
370
+ ```tsx
371
+ import { renderHook, waitFor } from '\@testing-library/react';
372
+
373
+ it('should fetch users', async () => {
374
+ const { result } = renderHook(() => useUserList(), { wrapper: QueryWrapper });
375
+
376
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
377
+
378
+ expect(result.current.users).toHaveLength(2);
379
+ });
380
+ ```
@@ -0,0 +1,173 @@
1
+ # React.js Rules
2
+
3
+ Full coding rules for this stack. Read this in full before writing or modifying any React code in this project — not just once, keep applying it to every edit in the session, not only the first.
4
+
5
+ ---
6
+
7
+ ## Project Structure
8
+
9
+ ```
10
+ src/
11
+ ├── components/
12
+ │ ├── ui/ # Generic, reusable UI (Button, Input, Modal...)
13
+ │ └── [feature]/ # Feature-specific components
14
+ ├── pages/ # Route-level components (thin, delegate to features)
15
+ ├── features/ # Self-contained feature modules
16
+ │ └── [feature]/
17
+ │ ├── components/ # UI specific to this feature
18
+ │ ├── hooks/ # Custom hooks for this feature
19
+ │ ├── api.ts # API calls for this feature
20
+ │ ├── store.ts # Zustand slice (if needed)
21
+ │ ├── types.ts # TypeScript types/interfaces
22
+ │ └── index.ts # Public exports
23
+ ├── hooks/ # Shared custom hooks
24
+ ├── services/
25
+ │ └── api.ts # Axios instance + interceptors
26
+ ├── store/ # Global Zustand store
27
+ ├── types/ # Shared TypeScript types
28
+ ├── utils/ # Pure utility functions
29
+ ├── constants/ # App-wide constants
30
+ └── App.tsx
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Component Rules
36
+
37
+ ### Always use Functional Components
38
+
39
+ - Never use class components.
40
+
41
+ ### Define Props with TypeScript interface
42
+
43
+ - No `any`, no untyped `props` param.
44
+
45
+ ### Keep components focused and small
46
+
47
+ - One component = one responsibility
48
+ - If a component exceeds ~150 lines, split it
49
+ - Extract logic to custom hooks, keep JSX clean
50
+
51
+ ### Export convention
52
+
53
+ - Named export for regular components.
54
+ - Default export only for page-level components.
55
+
56
+ ---
57
+
58
+ ## Custom Hook Rules
59
+
60
+ - Prefix with `use`: `useUserList`, `useAuth`, `useModal`
61
+ - Extract all side effects, API calls, and complex state from components
62
+ - Return objects (not arrays) for multiple values — easier to destructure
63
+
64
+ ---
65
+
66
+ ## State Management Rules
67
+
68
+ ### Server state → TanStack Query
69
+
70
+ Use for all API data (fetching, caching, mutations).
71
+
72
+ ### Client/UI state → Zustand (or useState for local)
73
+
74
+ ### Rules:
75
+ - **Never** use global state for server/API data — that's TanStack Query's job
76
+ - **Never** fetch in `useEffect` manually — use TanStack Query
77
+ - **Never** put loading/error state in Zustand — TanStack Query handles that
78
+
79
+ ---
80
+
81
+ ## Form Rules
82
+
83
+ Use **React Hook Form + Zod** for all forms — define the schema with Zod, infer the form type from it, wire it into `useForm` via `zodResolver`.
84
+
85
+ ---
86
+
87
+ ## API Layer Rules
88
+
89
+ One `axios` instance shared across the app with interceptors (attach auth token on request, handle 401 globally on response). Each feature gets its own thin API module built on that instance.
90
+
91
+ ---
92
+
93
+ ## TypeScript Rules
94
+
95
+ - **No `any`** — use `unknown`, proper types, or generics
96
+ - Define shared types in `types.ts` files
97
+ - Use `interface` for object shapes, `type` for unions/intersections
98
+ - Always type function return values for public hooks and API functions
99
+
100
+ ---
101
+
102
+ ## Styling Rules (Tailwind CSS)
103
+
104
+ - Use utility classes directly — avoid custom CSS unless necessary
105
+ - Extract repeated class combinations into components or a `cn()` helper
106
+ - Use `className` prop for external style overrides
107
+ - Never use inline `style={{ }}` for anything Tailwind can express
108
+
109
+ ---
110
+
111
+ ## Performance Rules
112
+
113
+ - Use `React.memo` only when profiling shows a real problem — do not optimize prematurely
114
+ - Use `useMemo` / `useCallback` for expensive computations or stable references passed to children
115
+ - Lazy-load routes with `React.lazy` + `Suspense`
116
+ - Avoid anonymous functions in JSX props when passing to memoized children
117
+
118
+ ---
119
+
120
+ ## Error Handling Rules
121
+
122
+ - Use React Error Boundary at the route level
123
+ - Handle API errors in TanStack Query `onError` callbacks
124
+ - Show user-friendly messages — never raw error objects
125
+ - Use toast notifications for async operation feedback
126
+
127
+ ---
128
+
129
+ ## Testing Rules
130
+
131
+ ### Unit/Component tests with React Testing Library
132
+
133
+ - Test behavior, not implementation.
134
+
135
+ ### Hook tests
136
+
137
+ - Use `renderHook` + `waitFor` from `@testing-library/react`.
138
+
139
+ ---
140
+
141
+ ## Naming Conventions
142
+
143
+ | Element | Convention | Example |
144
+ |---------|-----------|---------|
145
+ | Component file | PascalCase | `UserCard.tsx` |
146
+ | Hook file | camelCase | `useUserList.ts` |
147
+ | Utility file | camelCase | `formatDate.ts` |
148
+ | Type/Interface | PascalCase | `User`, `CreateUserRequest` |
149
+ | Component | PascalCase | `UserCard`, `OrderList` |
150
+ | Hook | `use` + PascalCase | `useUserList`, `useAuth` |
151
+ | Event handler | `handle` + Action | `handleDelete`, `handleSubmit` |
152
+ | Boolean variable | `is/has/can` prefix | `isLoading`, `hasError`, `canEdit` |
153
+ | Constant | UPPER_SNAKE_CASE | `MAX_RETRY`, `API_TIMEOUT` |
154
+ | CSS class (custom) | kebab-case | `user-card`, `nav-item` |
155
+
156
+ ---
157
+
158
+ ## Common Anti-Patterns to Avoid
159
+
160
+ - ❌ Fetching in `useEffect` → use TanStack Query
161
+ - ❌ Using `any` type → define proper TypeScript types
162
+ - ❌ Storing server data in Zustand → that's TanStack Query's responsibility
163
+ - ❌ Class components → use functional components
164
+ - ❌ `index.js` everywhere → use named files for traceability
165
+ - ❌ Direct DOM manipulation → use React state/refs
166
+ - ❌ Mutating state directly → always use setter functions
167
+ - ❌ Large components doing everything → split into smaller focused components
168
+ - ❌ Prop drilling more than 2 levels → use context or Zustand
169
+ - ❌ Hardcoding API URLs → use `import.meta.env.VITE_API_URL`
170
+
171
+ ---
172
+
173
+ When explaining changes, refer to the [React Official Documentation](https://react.dev), [TanStack Query](https://tanstack.com/query), and [React Hook Form](https://react-hook-form.com) conventions.
@@ -0,0 +1,161 @@
1
+ # PHP Code Examples
2
+
3
+ Reference examples for each rule area. Read the relevant section when generating code for that area.
4
+
5
+ ---
6
+
7
+ ## General
8
+
9
+ ```php
10
+ // ✅ Good
11
+ declare(strict_types=1);
12
+
13
+ class UserService
14
+ {
15
+ public function __construct(
16
+ private readonly UserRepository $userRepository,
17
+ ) {}
18
+
19
+ public function findById(int $id): UserDto
20
+ {
21
+ $user = $this->userRepository->findById($id);
22
+ if ($user === null) {
23
+ throw new NotFoundException("User $id not found");
24
+ }
25
+ return UserDto::fromArray($user);
26
+ }
27
+ }
28
+
29
+ // ❌ Bad — no strict types, logic in global scope
30
+ $pdo = new PDO(...);
31
+ $user = $pdo->query("SELECT * FROM users WHERE id = $_GET[id]")->fetch();
32
+ echo $user['name'];
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Security
38
+
39
+ ```php
40
+ // ✅ Good — prepared statement
41
+ $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
42
+ $stmt->execute([':email' => $email]);
43
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
44
+
45
+ // ✅ Good — safe HTML output
46
+ echo htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');
47
+
48
+ // ❌ Bad — SQL injection
49
+ $result = $pdo->query("SELECT * FROM users WHERE email = '$email'");
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Database / Repository
55
+
56
+ ```php
57
+ // ✅ Good
58
+ class UserRepository
59
+ {
60
+ public function __construct(private readonly \PDO $pdo) {}
61
+
62
+ public function findByEmail(string $email): ?array
63
+ {
64
+ $stmt = $this->pdo->prepare(
65
+ 'SELECT id, email, full_name FROM users WHERE email = :email AND deleted = 0'
66
+ );
67
+ $stmt->execute([':email' => $email]);
68
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
69
+ return $row ?: null;
70
+ }
71
+
72
+ public function create(string $email, string $fullName, string $passwordHash): int
73
+ {
74
+ $stmt = $this->pdo->prepare(
75
+ 'INSERT INTO users (email, full_name, password_hash) VALUES (:email, :full_name, :password_hash)'
76
+ );
77
+ $stmt->execute([
78
+ ':email' => $email,
79
+ ':full_name' => $fullName,
80
+ ':password_hash' => $passwordHash,
81
+ ]);
82
+ return (int) $this->pdo->lastInsertId();
83
+ }
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Controller
90
+
91
+ ```php
92
+ // ✅ Good
93
+ declare(strict_types=1);
94
+
95
+ class UserController
96
+ {
97
+ public function __construct(private readonly UserService $userService) {}
98
+
99
+ public function create(): void
100
+ {
101
+ $body = json_decode(file_get_contents('php://input'), true) ?? [];
102
+ $email = trim($body['email'] ?? '');
103
+ $fullName = trim($body['full_name'] ?? '');
104
+ $password = $body['password'] ?? '';
105
+
106
+ if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
107
+ http_response_code(400);
108
+ echo json_encode(['error' => 'Invalid email']);
109
+ return;
110
+ }
111
+
112
+ $user = $this->userService->create($email, $fullName, $password);
113
+ http_response_code(201);
114
+ echo json_encode($user);
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Error Handling
122
+
123
+ ```php
124
+ // ✅ Good — centralised handler
125
+ set_exception_handler(function (\Throwable $e): void {
126
+ $status = match (true) {
127
+ $e instanceof NotFoundException => 404,
128
+ $e instanceof ValidationException => 422,
129
+ $e instanceof UnauthorizedException => 401,
130
+ default => 500,
131
+ };
132
+ http_response_code($status);
133
+ header('Content-Type: application/json');
134
+ if ($status === 500) {
135
+ error_log($e->getMessage() . ' ' . $e->getTraceAsString());
136
+ echo json_encode(['error' => 'Internal server error']);
137
+ } else {
138
+ echo json_encode(['error' => $e->getMessage()]);
139
+ }
140
+ });
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Testing
146
+
147
+ ```php
148
+ class UserServiceTest extends TestCase
149
+ {
150
+ public function testCreateThrowsOnDuplicateEmail(): void
151
+ {
152
+ $repo = $this->createMock(UserRepository::class);
153
+ $repo->method('findByEmail')->willReturn(['id' => 1]);
154
+
155
+ $service = new UserService($repo);
156
+
157
+ $this->expectException(ValidationException::class);
158
+ $service->create('dup@example.com', 'Test', 'password');
159
+ }
160
+ }
161
+ ```