autoworkflow 3.1.4 → 3.5.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.
Files changed (123) hide show
  1. package/.claude/commands/analyze.md +19 -0
  2. package/.claude/commands/audit.md +174 -11
  3. package/.claude/commands/build.md +39 -0
  4. package/.claude/commands/commit.md +25 -0
  5. package/.claude/commands/fix.md +23 -0
  6. package/.claude/commands/plan.md +18 -0
  7. package/.claude/commands/suggest.md +23 -0
  8. package/.claude/commands/verify.md +18 -0
  9. package/.claude/hooks/post-bash-router.sh +20 -0
  10. package/.claude/hooks/post-commit.sh +140 -0
  11. package/.claude/hooks/pre-edit.sh +129 -0
  12. package/.claude/hooks/session-check.sh +79 -0
  13. package/.claude/settings.json +40 -6
  14. package/.claude/settings.local.json +3 -1
  15. package/.claude/skills/actix.md +337 -0
  16. package/.claude/skills/alembic.md +504 -0
  17. package/.claude/skills/angular.md +237 -0
  18. package/.claude/skills/api-design.md +187 -0
  19. package/.claude/skills/aspnet-core.md +377 -0
  20. package/.claude/skills/astro.md +245 -0
  21. package/.claude/skills/auth-clerk.md +327 -0
  22. package/.claude/skills/auth-firebase.md +367 -0
  23. package/.claude/skills/auth-nextauth.md +359 -0
  24. package/.claude/skills/auth-supabase.md +368 -0
  25. package/.claude/skills/axum.md +386 -0
  26. package/.claude/skills/blazor.md +456 -0
  27. package/.claude/skills/chi.md +348 -0
  28. package/.claude/skills/code-review.md +133 -0
  29. package/.claude/skills/csharp.md +296 -0
  30. package/.claude/skills/css-modules.md +325 -0
  31. package/.claude/skills/cypress.md +343 -0
  32. package/.claude/skills/debugging.md +133 -0
  33. package/.claude/skills/diesel.md +392 -0
  34. package/.claude/skills/django.md +301 -0
  35. package/.claude/skills/docker.md +319 -0
  36. package/.claude/skills/doctrine.md +473 -0
  37. package/.claude/skills/documentation.md +182 -0
  38. package/.claude/skills/dotnet.md +409 -0
  39. package/.claude/skills/drizzle.md +293 -0
  40. package/.claude/skills/echo.md +321 -0
  41. package/.claude/skills/eloquent.md +256 -0
  42. package/.claude/skills/emotion.md +426 -0
  43. package/.claude/skills/entity-framework.md +370 -0
  44. package/.claude/skills/express.md +316 -0
  45. package/.claude/skills/fastapi.md +329 -0
  46. package/.claude/skills/fastify.md +299 -0
  47. package/.claude/skills/fiber.md +315 -0
  48. package/.claude/skills/flask.md +322 -0
  49. package/.claude/skills/gin.md +342 -0
  50. package/.claude/skills/git.md +116 -0
  51. package/.claude/skills/github-actions.md +353 -0
  52. package/.claude/skills/go.md +377 -0
  53. package/.claude/skills/gorm.md +409 -0
  54. package/.claude/skills/graphql.md +478 -0
  55. package/.claude/skills/hibernate.md +379 -0
  56. package/.claude/skills/hono.md +306 -0
  57. package/.claude/skills/java.md +400 -0
  58. package/.claude/skills/jest.md +313 -0
  59. package/.claude/skills/jpa.md +282 -0
  60. package/.claude/skills/kotlin.md +347 -0
  61. package/.claude/skills/kubernetes.md +363 -0
  62. package/.claude/skills/laravel.md +414 -0
  63. package/.claude/skills/mcp-browser.md +320 -0
  64. package/.claude/skills/mcp-database.md +219 -0
  65. package/.claude/skills/mcp-fetch.md +241 -0
  66. package/.claude/skills/mcp-filesystem.md +204 -0
  67. package/.claude/skills/mcp-github.md +217 -0
  68. package/.claude/skills/mcp-memory.md +240 -0
  69. package/.claude/skills/mcp-search.md +218 -0
  70. package/.claude/skills/mcp-slack.md +262 -0
  71. package/.claude/skills/micronaut.md +388 -0
  72. package/.claude/skills/mongodb.md +319 -0
  73. package/.claude/skills/mongoose.md +355 -0
  74. package/.claude/skills/mysql.md +281 -0
  75. package/.claude/skills/nestjs.md +335 -0
  76. package/.claude/skills/nextjs-app-router.md +260 -0
  77. package/.claude/skills/nextjs-pages.md +172 -0
  78. package/.claude/skills/nuxt.md +202 -0
  79. package/.claude/skills/openapi.md +489 -0
  80. package/.claude/skills/performance.md +199 -0
  81. package/.claude/skills/php.md +398 -0
  82. package/.claude/skills/playwright.md +371 -0
  83. package/.claude/skills/postgresql.md +257 -0
  84. package/.claude/skills/prisma.md +293 -0
  85. package/.claude/skills/pydantic.md +304 -0
  86. package/.claude/skills/pytest.md +313 -0
  87. package/.claude/skills/python.md +272 -0
  88. package/.claude/skills/quarkus.md +377 -0
  89. package/.claude/skills/react.md +230 -0
  90. package/.claude/skills/redis.md +391 -0
  91. package/.claude/skills/refactoring.md +143 -0
  92. package/.claude/skills/remix.md +246 -0
  93. package/.claude/skills/rest-api.md +490 -0
  94. package/.claude/skills/rocket.md +366 -0
  95. package/.claude/skills/rust.md +341 -0
  96. package/.claude/skills/sass.md +380 -0
  97. package/.claude/skills/sea-orm.md +382 -0
  98. package/.claude/skills/security.md +167 -0
  99. package/.claude/skills/sequelize.md +395 -0
  100. package/.claude/skills/spring-boot.md +416 -0
  101. package/.claude/skills/sqlalchemy.md +269 -0
  102. package/.claude/skills/sqlx-rust.md +408 -0
  103. package/.claude/skills/state-jotai.md +346 -0
  104. package/.claude/skills/state-mobx.md +353 -0
  105. package/.claude/skills/state-pinia.md +431 -0
  106. package/.claude/skills/state-redux.md +337 -0
  107. package/.claude/skills/state-tanstack-query.md +434 -0
  108. package/.claude/skills/state-zustand.md +340 -0
  109. package/.claude/skills/styled-components.md +403 -0
  110. package/.claude/skills/svelte.md +238 -0
  111. package/.claude/skills/sveltekit.md +207 -0
  112. package/.claude/skills/symfony.md +437 -0
  113. package/.claude/skills/tailwind.md +279 -0
  114. package/.claude/skills/terraform.md +394 -0
  115. package/.claude/skills/testing-library.md +371 -0
  116. package/.claude/skills/trpc.md +426 -0
  117. package/.claude/skills/typeorm.md +368 -0
  118. package/.claude/skills/vitest.md +330 -0
  119. package/.claude/skills/vue.md +202 -0
  120. package/.claude/skills/warp.md +365 -0
  121. package/README.md +135 -52
  122. package/package.json +1 -1
  123. package/system/triggers.md +152 -11
@@ -0,0 +1,207 @@
1
+ # SvelteKit Skill
2
+
3
+ ## File Conventions
4
+ \`\`\`
5
+ src/
6
+ ├── routes/
7
+ │ ├── +page.svelte # Home page
8
+ │ ├── +page.server.ts # Server load/actions
9
+ │ ├── +layout.svelte # Root layout
10
+ │ ├── +layout.server.ts # Layout load function
11
+ │ ├── +error.svelte # Error page
12
+ │ ├── users/
13
+ │ │ ├── +page.svelte # /users
14
+ │ │ └── [id]/
15
+ │ │ ├── +page.svelte # /users/:id
16
+ │ │ └── +page.server.ts
17
+ │ └── api/
18
+ │ └── users/+server.ts # API endpoint
19
+ ├── lib/ # $lib alias
20
+ ├── hooks.server.ts # Server hooks
21
+ └── app.html # HTML template
22
+ \`\`\`
23
+
24
+ ## Data Loading
25
+ \`\`\`typescript
26
+ // +page.server.ts
27
+ import type { PageServerLoad } from './$types';
28
+
29
+ export const load: PageServerLoad = async ({ params, locals, fetch }) => {
30
+ // Access authenticated user from hooks
31
+ if (!locals.user) {
32
+ throw redirect(303, '/login');
33
+ }
34
+
35
+ const user = await db.user.findUnique({ where: { id: params.id } });
36
+
37
+ if (!user) {
38
+ throw error(404, 'User not found');
39
+ }
40
+
41
+ return { user };
42
+ };
43
+
44
+ // +page.svelte
45
+ <script lang="ts">
46
+ import type { PageData } from './$types';
47
+ export let data: PageData;
48
+ </script>
49
+
50
+ <h1>{data.user.name}</h1>
51
+ \`\`\`
52
+
53
+ ## Form Actions
54
+ \`\`\`typescript
55
+ // +page.server.ts
56
+ import type { Actions } from './$types';
57
+ import { fail, redirect } from '@sveltejs/kit';
58
+
59
+ export const actions: Actions = {
60
+ // Default action (POST to current page)
61
+ default: async ({ request, locals }) => {
62
+ const formData = await request.formData();
63
+ const email = formData.get('email') as string;
64
+ const password = formData.get('password') as string;
65
+
66
+ // Validation
67
+ if (!email || !password) {
68
+ return fail(400, { email, missing: true });
69
+ }
70
+
71
+ try {
72
+ const user = await login(email, password);
73
+ locals.user = user;
74
+ } catch (e) {
75
+ return fail(401, { email, incorrect: true });
76
+ }
77
+
78
+ throw redirect(303, '/dashboard');
79
+ },
80
+
81
+ // Named action
82
+ logout: async ({ cookies }) => {
83
+ cookies.delete('session', { path: '/' });
84
+ throw redirect(303, '/');
85
+ }
86
+ };
87
+
88
+ // +page.svelte
89
+ <script>
90
+ import { enhance } from '$app/forms';
91
+ export let form; // Action return data
92
+ </script>
93
+
94
+ <form method="POST" use:enhance>
95
+ <input name="email" value={form?.email ?? ''} />
96
+ {#if form?.missing}
97
+ <p class="error">Email and password required</p>
98
+ {/if}
99
+ {#if form?.incorrect}
100
+ <p class="error">Invalid credentials</p>
101
+ {/if}
102
+ <input type="password" name="password" />
103
+ <button>Login</button>
104
+ </form>
105
+
106
+ <form method="POST" action="?/logout" use:enhance>
107
+ <button>Logout</button>
108
+ </form>
109
+ \`\`\`
110
+
111
+ ## API Endpoints
112
+ \`\`\`typescript
113
+ // routes/api/users/+server.ts
114
+ import { json, error } from '@sveltejs/kit';
115
+ import type { RequestHandler } from './$types';
116
+
117
+ export const GET: RequestHandler = async ({ url }) => {
118
+ const limit = Number(url.searchParams.get('limit') ?? 10);
119
+ const users = await db.user.findMany({ take: limit });
120
+ return json(users);
121
+ };
122
+
123
+ export const POST: RequestHandler = async ({ request }) => {
124
+ const data = await request.json();
125
+
126
+ if (!data.email) {
127
+ throw error(400, 'Email required');
128
+ }
129
+
130
+ const user = await db.user.create({ data });
131
+ return json(user, { status: 201 });
132
+ };
133
+ \`\`\`
134
+
135
+ ## Hooks
136
+ \`\`\`typescript
137
+ // src/hooks.server.ts
138
+ import type { Handle, HandleServerError } from '@sveltejs/kit';
139
+
140
+ export const handle: Handle = async ({ event, resolve }) => {
141
+ // Run on every request
142
+ const session = event.cookies.get('session');
143
+
144
+ if (session) {
145
+ event.locals.user = await getUserFromSession(session);
146
+ }
147
+
148
+ // Add custom headers
149
+ const response = await resolve(event);
150
+ response.headers.set('X-Custom-Header', 'value');
151
+
152
+ return response;
153
+ };
154
+
155
+ export const handleError: HandleServerError = async ({ error, event }) => {
156
+ // Log errors
157
+ console.error(error);
158
+
159
+ return {
160
+ message: 'An unexpected error occurred',
161
+ code: 'INTERNAL_ERROR'
162
+ };
163
+ };
164
+ \`\`\`
165
+
166
+ ## Page Options
167
+ \`\`\`typescript
168
+ // +page.ts or +page.server.ts
169
+ export const prerender = true; // Static generation
170
+ export const ssr = false; // Client-only
171
+ export const csr = true; // Enable client-side JS
172
+
173
+ // +layout.server.ts
174
+ export const trailingSlash = 'always'; // URL formatting
175
+ \`\`\`
176
+
177
+ ## Authentication Pattern
178
+ \`\`\`typescript
179
+ // src/hooks.server.ts
180
+ export const handle: Handle = async ({ event, resolve }) => {
181
+ const session = event.cookies.get('session');
182
+ event.locals.user = session ? await validateSession(session) : null;
183
+ return resolve(event);
184
+ };
185
+
186
+ // src/routes/(protected)/+layout.server.ts
187
+ export const load = async ({ locals }) => {
188
+ if (!locals.user) {
189
+ throw redirect(303, '/login');
190
+ }
191
+ return { user: locals.user };
192
+ };
193
+ \`\`\`
194
+
195
+ ## ❌ DON'T
196
+ - Use fetch in load functions for internal APIs (use db directly)
197
+ - Forget to handle form validation errors
198
+ - Skip type safety with $types
199
+ - Put secrets in +page.ts (client-accessible)
200
+
201
+ ## ✅ DO
202
+ - Use load functions for data fetching
203
+ - Use form actions for mutations
204
+ - Use hooks for authentication
205
+ - Use +page.server.ts for sensitive operations
206
+ - Enable prerendering for static pages
207
+ - Use use:enhance for progressive enhancement
@@ -0,0 +1,437 @@
1
+ # Symfony Skill
2
+
3
+ ## Controller
4
+ \`\`\`php
5
+ <?php
6
+
7
+ namespace App\\Controller\\Api\\V1;
8
+
9
+ use App\\DTO\\CreateUserRequest;
10
+ use App\\DTO\\UpdateUserRequest;
11
+ use App\\Entity\\User;
12
+ use App\\Repository\\UserRepository;
13
+ use App\\Service\\UserService;
14
+ use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
15
+ use Symfony\\Component\\HttpFoundation\\JsonResponse;
16
+ use Symfony\\Component\\HttpFoundation\\Response;
17
+ use Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload;
18
+ use Symfony\\Component\\Routing\\Attribute\\Route;
19
+
20
+ #[Route('/api/v1/users')]
21
+ final class UserController extends AbstractController
22
+ {
23
+ public function __construct(
24
+ private readonly UserService $userService,
25
+ private readonly UserRepository $repository,
26
+ ) {}
27
+
28
+ #[Route('', methods: ['GET'])]
29
+ public function index(): JsonResponse
30
+ {
31
+ $users = $this->repository->findAllActive();
32
+ return $this->json($users, context: ['groups' => ['user:list']]);
33
+ }
34
+
35
+ #[Route('/{id}', methods: ['GET'])]
36
+ public function show(User $user): JsonResponse
37
+ {
38
+ return $this->json($user, context: ['groups' => ['user:read']]);
39
+ }
40
+
41
+ #[Route('', methods: ['POST'])]
42
+ public function create(
43
+ #[MapRequestPayload] CreateUserRequest $request
44
+ ): JsonResponse {
45
+ $user = $this->userService->create($request);
46
+
47
+ return $this->json(
48
+ $user,
49
+ Response::HTTP_CREATED,
50
+ context: ['groups' => ['user:read']]
51
+ );
52
+ }
53
+
54
+ #[Route('/{id}', methods: ['PUT'])]
55
+ public function update(
56
+ User $user,
57
+ #[MapRequestPayload] UpdateUserRequest $request
58
+ ): JsonResponse {
59
+ $user = $this->userService->update($user, $request);
60
+
61
+ return $this->json($user, context: ['groups' => ['user:read']]);
62
+ }
63
+
64
+ #[Route('/{id}', methods: ['DELETE'])]
65
+ public function delete(User $user): Response
66
+ {
67
+ $this->userService->delete($user);
68
+
69
+ return new Response(null, Response::HTTP_NO_CONTENT);
70
+ }
71
+ }
72
+ \`\`\`
73
+
74
+ ## DTO with Validation
75
+ \`\`\`php
76
+ <?php
77
+
78
+ namespace App\\DTO;
79
+
80
+ use Symfony\\Component\\Validator\\Constraints as Assert;
81
+
82
+ final readonly class CreateUserRequest
83
+ {
84
+ public function __construct(
85
+ #[Assert\\NotBlank]
86
+ #[Assert\\Email]
87
+ #[Assert\\Length(max: 255)]
88
+ public string $email,
89
+
90
+ #[Assert\\NotBlank]
91
+ #[Assert\\Length(min: 2, max: 100)]
92
+ public string $name,
93
+
94
+ #[Assert\\NotBlank]
95
+ #[Assert\\Length(min: 8)]
96
+ #[Assert\\PasswordStrength]
97
+ public string $password,
98
+ ) {}
99
+ }
100
+
101
+ final readonly class UpdateUserRequest
102
+ {
103
+ public function __construct(
104
+ #[Assert\\Length(min: 2, max: 100)]
105
+ public ?string $name = null,
106
+
107
+ #[Assert\\Email]
108
+ public ?string $email = null,
109
+ ) {}
110
+ }
111
+ \`\`\`
112
+
113
+ ## Service
114
+ \`\`\`php
115
+ <?php
116
+
117
+ namespace App\\Service;
118
+
119
+ use App\\DTO\\CreateUserRequest;
120
+ use App\\DTO\\UpdateUserRequest;
121
+ use App\\Entity\\User;
122
+ use App\\Repository\\UserRepository;
123
+ use Doctrine\\ORM\\EntityManagerInterface;
124
+ use Symfony\\Component\\PasswordHasher\\Hasher\\UserPasswordHasherInterface;
125
+
126
+ final readonly class UserService
127
+ {
128
+ public function __construct(
129
+ private EntityManagerInterface $em,
130
+ private UserRepository $repository,
131
+ private UserPasswordHasherInterface $hasher,
132
+ ) {}
133
+
134
+ public function create(CreateUserRequest $request): User
135
+ {
136
+ $user = new User();
137
+ $user->setEmail($request->email);
138
+ $user->setName($request->name);
139
+ $user->setPassword($this->hasher->hashPassword($user, $request->password));
140
+
141
+ $this->em->persist($user);
142
+ $this->em->flush();
143
+
144
+ return $user;
145
+ }
146
+
147
+ public function update(User $user, UpdateUserRequest $request): User
148
+ {
149
+ if ($request->name !== null) {
150
+ $user->setName($request->name);
151
+ }
152
+ if ($request->email !== null) {
153
+ $user->setEmail($request->email);
154
+ }
155
+
156
+ $this->em->flush();
157
+
158
+ return $user;
159
+ }
160
+
161
+ public function delete(User $user): void
162
+ {
163
+ $this->em->remove($user);
164
+ $this->em->flush();
165
+ }
166
+ }
167
+ \`\`\`
168
+
169
+ ## Repository
170
+ \`\`\`php
171
+ <?php
172
+
173
+ namespace App\\Repository;
174
+
175
+ use App\\Entity\\User;
176
+ use Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;
177
+ use Doctrine\\Persistence\\ManagerRegistry;
178
+
179
+ /**
180
+ * @extends ServiceEntityRepository<User>
181
+ */
182
+ final class UserRepository extends ServiceEntityRepository
183
+ {
184
+ public function __construct(ManagerRegistry $registry)
185
+ {
186
+ parent::__construct($registry, User::class);
187
+ }
188
+
189
+ public function findByEmail(string $email): ?User
190
+ {
191
+ return $this->findOneBy(['email' => $email]);
192
+ }
193
+
194
+ /**
195
+ * @return User[]
196
+ */
197
+ public function findAllActive(): array
198
+ {
199
+ return $this->createQueryBuilder('u')
200
+ ->where('u.isActive = :active')
201
+ ->setParameter('active', true)
202
+ ->orderBy('u.createdAt', 'DESC')
203
+ ->getQuery()
204
+ ->getResult();
205
+ }
206
+
207
+ /**
208
+ * @return User[]
209
+ */
210
+ public function findPaginated(int $page, int $limit): array
211
+ {
212
+ return $this->createQueryBuilder('u')
213
+ ->setFirstResult(($page - 1) * $limit)
214
+ ->setMaxResults($limit)
215
+ ->orderBy('u.createdAt', 'DESC')
216
+ ->getQuery()
217
+ ->getResult();
218
+ }
219
+
220
+ public function countActive(): int
221
+ {
222
+ return $this->createQueryBuilder('u')
223
+ ->select('COUNT(u.id)')
224
+ ->where('u.isActive = :active')
225
+ ->setParameter('active', true)
226
+ ->getQuery()
227
+ ->getSingleScalarResult();
228
+ }
229
+ }
230
+ \`\`\`
231
+
232
+ ## Exception Handler
233
+ \`\`\`php
234
+ <?php
235
+
236
+ namespace App\\EventListener;
237
+
238
+ use App\\Exception\\NotFoundException;
239
+ use App\\Exception\\ValidationException;
240
+ use Symfony\\Component\\HttpFoundation\\JsonResponse;
241
+ use Symfony\\Component\\HttpFoundation\\Response;
242
+ use Symfony\\Component\\HttpKernel\\Event\\ExceptionEvent;
243
+ use Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;
244
+
245
+ final class ExceptionListener
246
+ {
247
+ public function onKernelException(ExceptionEvent $event): void
248
+ {
249
+ $exception = $event->getThrowable();
250
+
251
+ $response = match (true) {
252
+ $exception instanceof NotFoundException => new JsonResponse(
253
+ ['error' => $exception->getMessage()],
254
+ Response::HTTP_NOT_FOUND
255
+ ),
256
+ $exception instanceof ValidationException => new JsonResponse(
257
+ ['error' => 'Validation failed', 'errors' => $exception->getErrors()],
258
+ Response::HTTP_BAD_REQUEST
259
+ ),
260
+ $exception instanceof HttpExceptionInterface => new JsonResponse(
261
+ ['error' => $exception->getMessage()],
262
+ $exception->getStatusCode()
263
+ ),
264
+ default => new JsonResponse(
265
+ ['error' => 'Internal server error'],
266
+ Response::HTTP_INTERNAL_SERVER_ERROR
267
+ ),
268
+ };
269
+
270
+ $event->setResponse($response);
271
+ }
272
+ }
273
+ \`\`\`
274
+
275
+ ## Events
276
+ \`\`\`php
277
+ <?php
278
+
279
+ namespace App\\Event;
280
+
281
+ final readonly class UserCreatedEvent
282
+ {
283
+ public function __construct(
284
+ public string $userId,
285
+ public string $email,
286
+ ) {}
287
+ }
288
+
289
+ // Listener
290
+ namespace App\\EventListener;
291
+
292
+ use App\\Event\\UserCreatedEvent;
293
+ use Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener;
294
+
295
+ #[AsEventListener]
296
+ final readonly class SendWelcomeEmailListener
297
+ {
298
+ public function __construct(
299
+ private MailerInterface $mailer,
300
+ ) {}
301
+
302
+ public function __invoke(UserCreatedEvent $event): void
303
+ {
304
+ // Send welcome email
305
+ }
306
+ }
307
+
308
+ // Dispatch in service
309
+ $this->eventDispatcher->dispatch(new UserCreatedEvent($user->getId(), $user->getEmail()));
310
+ \`\`\`
311
+
312
+ ## Security
313
+ \`\`\`php
314
+ <?php
315
+
316
+ // Entity implementing UserInterface
317
+ namespace App\\Entity;
318
+
319
+ use Symfony\\Component\\Security\\Core\\User\\PasswordAuthenticatedUserInterface;
320
+ use Symfony\\Component\\Security\\Core\\User\\UserInterface;
321
+
322
+ class User implements UserInterface, PasswordAuthenticatedUserInterface
323
+ {
324
+ public function getRoles(): array
325
+ {
326
+ $roles = $this->roles;
327
+ $roles[] = 'ROLE_USER';
328
+ return array_unique($roles);
329
+ }
330
+
331
+ public function getUserIdentifier(): string
332
+ {
333
+ return $this->email;
334
+ }
335
+
336
+ public function getPassword(): string
337
+ {
338
+ return $this->password;
339
+ }
340
+
341
+ public function eraseCredentials(): void
342
+ {
343
+ // Clear any temporary sensitive data
344
+ }
345
+ }
346
+
347
+ // Controller with authorization
348
+ #[Route('/api/v1/admin/users')]
349
+ #[IsGranted('ROLE_ADMIN')]
350
+ final class AdminUserController extends AbstractController
351
+ {
352
+ #[Route('/{id}', methods: ['DELETE'])]
353
+ #[IsGranted('ROLE_SUPER_ADMIN')]
354
+ public function delete(User $user): Response
355
+ {
356
+ // ...
357
+ }
358
+ }
359
+ \`\`\`
360
+
361
+ ## Testing
362
+ \`\`\`php
363
+ <?php
364
+
365
+ namespace App\\Tests\\Controller;
366
+
367
+ use App\\Entity\\User;
368
+ use Doctrine\\ORM\\EntityManagerInterface;
369
+ use Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;
370
+
371
+ final class UserControllerTest extends WebTestCase
372
+ {
373
+ private EntityManagerInterface $em;
374
+
375
+ protected function setUp(): void
376
+ {
377
+ $this->em = self::getContainer()->get(EntityManagerInterface::class);
378
+ }
379
+
380
+ public function testListUsers(): void
381
+ {
382
+ $client = static::createClient();
383
+
384
+ $client->request('GET', '/api/v1/users');
385
+
386
+ $this->assertResponseIsSuccessful();
387
+ $this->assertResponseHeaderSame('content-type', 'application/json');
388
+ }
389
+
390
+ public function testCreateUser(): void
391
+ {
392
+ $client = static::createClient();
393
+
394
+ $client->request('POST', '/api/v1/users', [], [], [
395
+ 'CONTENT_TYPE' => 'application/json',
396
+ ], json_encode([
397
+ 'email' => 'test@example.com',
398
+ 'name' => 'Test User',
399
+ 'password' => 'Password123!',
400
+ ]));
401
+
402
+ $this->assertResponseStatusCodeSame(201);
403
+
404
+ $user = $this->em->getRepository(User::class)->findByEmail('test@example.com');
405
+ $this->assertNotNull($user);
406
+ }
407
+
408
+ public function testCreateUserValidationError(): void
409
+ {
410
+ $client = static::createClient();
411
+
412
+ $client->request('POST', '/api/v1/users', [], [], [
413
+ 'CONTENT_TYPE' => 'application/json',
414
+ ], json_encode([
415
+ 'email' => 'invalid',
416
+ 'name' => '',
417
+ 'password' => '123',
418
+ ]));
419
+
420
+ $this->assertResponseStatusCodeSame(422);
421
+ }
422
+ }
423
+ \`\`\`
424
+
425
+ ## ✅ DO
426
+ - Use constructor property promotion
427
+ - Use attributes for routing and validation
428
+ - Use \`#[MapRequestPayload]\` for automatic DTO mapping
429
+ - Use services for business logic
430
+ - Use events for side effects
431
+ - Use serialization groups for API responses
432
+
433
+ ## ❌ DON'T
434
+ - Don't inject EntityManager in controllers (use services)
435
+ - Don't put business logic in controllers
436
+ - Don't return entities directly (use serialization groups or DTOs)
437
+ - Don't catch exceptions in controllers (use event listeners)