autoworkflow 3.1.5 → 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.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- 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)
|