autoworkflow 3.1.5 → 3.6.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/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -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 +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# Doctrine ORM Skill
|
|
2
|
+
|
|
3
|
+
## Entity Definition
|
|
4
|
+
\`\`\`php
|
|
5
|
+
<?php
|
|
6
|
+
|
|
7
|
+
namespace App\\Entity;
|
|
8
|
+
|
|
9
|
+
use App\\Repository\\UserRepository;
|
|
10
|
+
use Doctrine\\Common\\Collections\\ArrayCollection;
|
|
11
|
+
use Doctrine\\Common\\Collections\\Collection;
|
|
12
|
+
use Doctrine\\DBAL\\Types\\Types;
|
|
13
|
+
use Doctrine\\ORM\\Mapping as ORM;
|
|
14
|
+
use Symfony\\Component\\Serializer\\Attribute\\Groups;
|
|
15
|
+
|
|
16
|
+
#[ORM\\Entity(repositoryClass: UserRepository::class)]
|
|
17
|
+
#[ORM\\Table(name: 'users')]
|
|
18
|
+
#[ORM\\Index(columns: ['email'], name: 'idx_users_email')]
|
|
19
|
+
#[ORM\\HasLifecycleCallbacks]
|
|
20
|
+
class User
|
|
21
|
+
{
|
|
22
|
+
#[ORM\\Id]
|
|
23
|
+
#[ORM\\GeneratedValue(strategy: 'CUSTOM')]
|
|
24
|
+
#[ORM\\CustomIdGenerator(class: UuidGenerator::class)]
|
|
25
|
+
#[ORM\\Column(type: Types::GUID)]
|
|
26
|
+
#[Groups(['user:list', 'user:read'])]
|
|
27
|
+
private string $id;
|
|
28
|
+
|
|
29
|
+
#[ORM\\Column(type: Types::STRING, length: 255, unique: true)]
|
|
30
|
+
#[Groups(['user:list', 'user:read'])]
|
|
31
|
+
private string $email;
|
|
32
|
+
|
|
33
|
+
#[ORM\\Column(type: Types::STRING, length: 100)]
|
|
34
|
+
#[Groups(['user:list', 'user:read'])]
|
|
35
|
+
private string $name;
|
|
36
|
+
|
|
37
|
+
#[ORM\\Column(type: Types::STRING)]
|
|
38
|
+
private string $password;
|
|
39
|
+
|
|
40
|
+
#[ORM\\Column(type: Types::BOOLEAN)]
|
|
41
|
+
#[Groups(['user:read'])]
|
|
42
|
+
private bool $isActive = true;
|
|
43
|
+
|
|
44
|
+
#[ORM\\Column(type: Types::DATETIME_IMMUTABLE)]
|
|
45
|
+
#[Groups(['user:read'])]
|
|
46
|
+
private \\DateTimeImmutable $createdAt;
|
|
47
|
+
|
|
48
|
+
#[ORM\\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
|
49
|
+
private ?\\DateTimeImmutable $updatedAt = null;
|
|
50
|
+
|
|
51
|
+
// Relationships
|
|
52
|
+
#[ORM\\OneToOne(targetEntity: Profile::class, mappedBy: 'user', cascade: ['persist', 'remove'])]
|
|
53
|
+
#[Groups(['user:read'])]
|
|
54
|
+
private ?Profile $profile = null;
|
|
55
|
+
|
|
56
|
+
#[ORM\\OneToMany(targetEntity: Post::class, mappedBy: 'author', cascade: ['persist'], orphanRemoval: true)]
|
|
57
|
+
#[ORM\\OrderBy(['createdAt' => 'DESC'])]
|
|
58
|
+
private Collection $posts;
|
|
59
|
+
|
|
60
|
+
#[ORM\\ManyToMany(targetEntity: Role::class, inversedBy: 'users')]
|
|
61
|
+
#[ORM\\JoinTable(name: 'user_roles')]
|
|
62
|
+
private Collection $roles;
|
|
63
|
+
|
|
64
|
+
public function __construct()
|
|
65
|
+
{
|
|
66
|
+
$this->posts = new ArrayCollection();
|
|
67
|
+
$this->roles = new ArrayCollection();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[ORM\\PrePersist]
|
|
71
|
+
public function setCreatedAtValue(): void
|
|
72
|
+
{
|
|
73
|
+
$this->createdAt = new \\DateTimeImmutable();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[ORM\\PreUpdate]
|
|
77
|
+
public function setUpdatedAtValue(): void
|
|
78
|
+
{
|
|
79
|
+
$this->updatedAt = new \\DateTimeImmutable();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Relationship helpers
|
|
83
|
+
public function addPost(Post $post): self
|
|
84
|
+
{
|
|
85
|
+
if (!$this->posts->contains($post)) {
|
|
86
|
+
$this->posts->add($post);
|
|
87
|
+
$post->setAuthor($this);
|
|
88
|
+
}
|
|
89
|
+
return $this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public function removePost(Post $post): self
|
|
93
|
+
{
|
|
94
|
+
if ($this->posts->removeElement($post)) {
|
|
95
|
+
if ($post->getAuthor() === $this) {
|
|
96
|
+
$post->setAuthor(null);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return $this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public function addRole(Role $role): self
|
|
103
|
+
{
|
|
104
|
+
if (!$this->roles->contains($role)) {
|
|
105
|
+
$this->roles->add($role);
|
|
106
|
+
}
|
|
107
|
+
return $this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Getters and setters...
|
|
111
|
+
public function getId(): string { return $this->id; }
|
|
112
|
+
public function getEmail(): string { return $this->email; }
|
|
113
|
+
public function setEmail(string $email): self { $this->email = $email; return $this; }
|
|
114
|
+
// ... etc
|
|
115
|
+
}
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
## Repository with Query Builder
|
|
119
|
+
\`\`\`php
|
|
120
|
+
<?php
|
|
121
|
+
|
|
122
|
+
namespace App\\Repository;
|
|
123
|
+
|
|
124
|
+
use App\\Entity\\User;
|
|
125
|
+
use Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;
|
|
126
|
+
use Doctrine\\ORM\\QueryBuilder;
|
|
127
|
+
use Doctrine\\Persistence\\ManagerRegistry;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @extends ServiceEntityRepository<User>
|
|
131
|
+
*/
|
|
132
|
+
final class UserRepository extends ServiceEntityRepository
|
|
133
|
+
{
|
|
134
|
+
public function __construct(ManagerRegistry $registry)
|
|
135
|
+
{
|
|
136
|
+
parent::__construct($registry, User::class);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public function findByEmail(string $email): ?User
|
|
140
|
+
{
|
|
141
|
+
return $this->findOneBy(['email' => $email]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @return User[]
|
|
146
|
+
*/
|
|
147
|
+
public function findActiveWithPosts(): array
|
|
148
|
+
{
|
|
149
|
+
return $this->createQueryBuilder('u')
|
|
150
|
+
->leftJoin('u.posts', 'p')
|
|
151
|
+
->addSelect('p')
|
|
152
|
+
->where('u.isActive = :active')
|
|
153
|
+
->setParameter('active', true)
|
|
154
|
+
->orderBy('u.createdAt', 'DESC')
|
|
155
|
+
->getQuery()
|
|
156
|
+
->getResult();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @return User[]
|
|
161
|
+
*/
|
|
162
|
+
public function findByRole(string $roleName): array
|
|
163
|
+
{
|
|
164
|
+
return $this->createQueryBuilder('u')
|
|
165
|
+
->innerJoin('u.roles', 'r')
|
|
166
|
+
->where('r.name = :role')
|
|
167
|
+
->setParameter('role', $roleName)
|
|
168
|
+
->getQuery()
|
|
169
|
+
->getResult();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public function countByStatus(bool $isActive): int
|
|
173
|
+
{
|
|
174
|
+
return $this->createQueryBuilder('u')
|
|
175
|
+
->select('COUNT(u.id)')
|
|
176
|
+
->where('u.isActive = :active')
|
|
177
|
+
->setParameter('active', $isActive)
|
|
178
|
+
->getQuery()
|
|
179
|
+
->getSingleScalarResult();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Paginated query
|
|
183
|
+
public function createPaginatedQuery(): QueryBuilder
|
|
184
|
+
{
|
|
185
|
+
return $this->createQueryBuilder('u')
|
|
186
|
+
->where('u.isActive = :active')
|
|
187
|
+
->setParameter('active', true)
|
|
188
|
+
->orderBy('u.createdAt', 'DESC');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Search with multiple conditions
|
|
192
|
+
public function search(UserSearchCriteria $criteria): array
|
|
193
|
+
{
|
|
194
|
+
$qb = $this->createQueryBuilder('u');
|
|
195
|
+
|
|
196
|
+
if ($criteria->email !== null) {
|
|
197
|
+
$qb->andWhere('u.email LIKE :email')
|
|
198
|
+
->setParameter('email', '%' . $criteria->email . '%');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if ($criteria->name !== null) {
|
|
202
|
+
$qb->andWhere('u.name LIKE :name')
|
|
203
|
+
->setParameter('name', '%' . $criteria->name . '%');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ($criteria->isActive !== null) {
|
|
207
|
+
$qb->andWhere('u.isActive = :active')
|
|
208
|
+
->setParameter('active', $criteria->isActive);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ($criteria->createdAfter !== null) {
|
|
212
|
+
$qb->andWhere('u.createdAt > :after')
|
|
213
|
+
->setParameter('after', $criteria->createdAfter);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return $qb->setMaxResults($criteria->limit)
|
|
217
|
+
->setFirstResult($criteria->offset)
|
|
218
|
+
->getQuery()
|
|
219
|
+
->getResult();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
## DQL (Doctrine Query Language)
|
|
225
|
+
\`\`\`php
|
|
226
|
+
<?php
|
|
227
|
+
|
|
228
|
+
// Named queries in entity
|
|
229
|
+
#[ORM\\Entity]
|
|
230
|
+
#[ORM\\NamedQuery(
|
|
231
|
+
name: 'User.findActiveByRole',
|
|
232
|
+
query: 'SELECT u FROM App\\Entity\\User u JOIN u.roles r WHERE u.isActive = true AND r.name = :role'
|
|
233
|
+
)]
|
|
234
|
+
class User { }
|
|
235
|
+
|
|
236
|
+
// Usage
|
|
237
|
+
$users = $this->em
|
|
238
|
+
->createNamedQuery('User.findActiveByRole')
|
|
239
|
+
->setParameter('role', 'ADMIN')
|
|
240
|
+
->getResult();
|
|
241
|
+
|
|
242
|
+
// Direct DQL
|
|
243
|
+
$users = $this->em->createQuery(
|
|
244
|
+
'SELECT u, p FROM App\\Entity\\User u
|
|
245
|
+
LEFT JOIN u.posts p
|
|
246
|
+
WHERE u.isActive = :active
|
|
247
|
+
ORDER BY u.createdAt DESC'
|
|
248
|
+
)
|
|
249
|
+
->setParameter('active', true)
|
|
250
|
+
->getResult();
|
|
251
|
+
|
|
252
|
+
// Aggregate queries
|
|
253
|
+
$stats = $this->em->createQuery(
|
|
254
|
+
'SELECT r.name, COUNT(u.id) as userCount
|
|
255
|
+
FROM App\\Entity\\User u
|
|
256
|
+
JOIN u.roles r
|
|
257
|
+
WHERE u.isActive = :active
|
|
258
|
+
GROUP BY r.name'
|
|
259
|
+
)
|
|
260
|
+
->setParameter('active', true)
|
|
261
|
+
->getResult();
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
## Transactions
|
|
265
|
+
\`\`\`php
|
|
266
|
+
<?php
|
|
267
|
+
|
|
268
|
+
namespace App\\Service;
|
|
269
|
+
|
|
270
|
+
use Doctrine\\ORM\\EntityManagerInterface;
|
|
271
|
+
|
|
272
|
+
final readonly class UserService
|
|
273
|
+
{
|
|
274
|
+
public function __construct(
|
|
275
|
+
private EntityManagerInterface $em,
|
|
276
|
+
) {}
|
|
277
|
+
|
|
278
|
+
public function createWithProfile(CreateUserData $data): User
|
|
279
|
+
{
|
|
280
|
+
return $this->em->wrapInTransaction(function () use ($data) {
|
|
281
|
+
$user = new User();
|
|
282
|
+
$user->setEmail($data->email);
|
|
283
|
+
$user->setName($data->name);
|
|
284
|
+
$this->em->persist($user);
|
|
285
|
+
|
|
286
|
+
$profile = new Profile();
|
|
287
|
+
$profile->setUser($user);
|
|
288
|
+
$profile->setBio($data->bio);
|
|
289
|
+
$this->em->persist($profile);
|
|
290
|
+
|
|
291
|
+
return $user;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Manual transaction control
|
|
296
|
+
public function complexOperation(): void
|
|
297
|
+
{
|
|
298
|
+
$this->em->beginTransaction();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// Multiple operations...
|
|
302
|
+
$this->em->flush();
|
|
303
|
+
$this->em->commit();
|
|
304
|
+
} catch (\\Exception $e) {
|
|
305
|
+
$this->em->rollback();
|
|
306
|
+
throw $e;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
\`\`\`
|
|
311
|
+
|
|
312
|
+
## Migrations
|
|
313
|
+
\`\`\`bash
|
|
314
|
+
# Create migration
|
|
315
|
+
php bin/console make:migration
|
|
316
|
+
|
|
317
|
+
# Run migrations
|
|
318
|
+
php bin/console doctrine:migrations:migrate
|
|
319
|
+
|
|
320
|
+
# Rollback last migration
|
|
321
|
+
php bin/console doctrine:migrations:migrate prev
|
|
322
|
+
|
|
323
|
+
# Generate migration from entity changes
|
|
324
|
+
php bin/console doctrine:migrations:diff
|
|
325
|
+
\`\`\`
|
|
326
|
+
|
|
327
|
+
\`\`\`php
|
|
328
|
+
<?php
|
|
329
|
+
// migrations/Version20240115000000.php
|
|
330
|
+
|
|
331
|
+
declare(strict_types=1);
|
|
332
|
+
|
|
333
|
+
namespace DoctrineMigrations;
|
|
334
|
+
|
|
335
|
+
use Doctrine\\DBAL\\Schema\\Schema;
|
|
336
|
+
use Doctrine\\Migrations\\AbstractMigration;
|
|
337
|
+
|
|
338
|
+
final class Version20240115000000 extends AbstractMigration
|
|
339
|
+
{
|
|
340
|
+
public function up(Schema $schema): void
|
|
341
|
+
{
|
|
342
|
+
$this->addSql('CREATE TABLE users (
|
|
343
|
+
id UUID NOT NULL,
|
|
344
|
+
email VARCHAR(255) NOT NULL,
|
|
345
|
+
name VARCHAR(100) NOT NULL,
|
|
346
|
+
password VARCHAR(255) NOT NULL,
|
|
347
|
+
is_active BOOLEAN DEFAULT true NOT NULL,
|
|
348
|
+
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
|
349
|
+
updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
|
350
|
+
PRIMARY KEY(id)
|
|
351
|
+
)');
|
|
352
|
+
$this->addSql('CREATE UNIQUE INDEX idx_users_email ON users (email)');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public function down(Schema $schema): void
|
|
356
|
+
{
|
|
357
|
+
$this->addSql('DROP TABLE users');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
\`\`\`
|
|
361
|
+
|
|
362
|
+
## Lifecycle Callbacks and Events
|
|
363
|
+
\`\`\`php
|
|
364
|
+
<?php
|
|
365
|
+
|
|
366
|
+
// In entity
|
|
367
|
+
#[ORM\\HasLifecycleCallbacks]
|
|
368
|
+
class User
|
|
369
|
+
{
|
|
370
|
+
#[ORM\\PrePersist]
|
|
371
|
+
public function onPrePersist(): void
|
|
372
|
+
{
|
|
373
|
+
$this->createdAt = new \\DateTimeImmutable();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[ORM\\PreUpdate]
|
|
377
|
+
public function onPreUpdate(): void
|
|
378
|
+
{
|
|
379
|
+
$this->updatedAt = new \\DateTimeImmutable();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[ORM\\PostPersist]
|
|
383
|
+
public function onPostPersist(): void
|
|
384
|
+
{
|
|
385
|
+
// After insert
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Event subscriber for complex logic
|
|
390
|
+
namespace App\\EventSubscriber;
|
|
391
|
+
|
|
392
|
+
use App\\Entity\\User;
|
|
393
|
+
use Doctrine\\Bundle\\DoctrineBundle\\Attribute\\AsDoctrineListener;
|
|
394
|
+
use Doctrine\\ORM\\Event\\PrePersistEventArgs;
|
|
395
|
+
use Doctrine\\ORM\\Events;
|
|
396
|
+
|
|
397
|
+
#[AsDoctrineListener(event: Events::prePersist)]
|
|
398
|
+
final readonly class UserSubscriber
|
|
399
|
+
{
|
|
400
|
+
public function __construct(
|
|
401
|
+
private PasswordHasherInterface $hasher,
|
|
402
|
+
) {}
|
|
403
|
+
|
|
404
|
+
public function prePersist(PrePersistEventArgs $args): void
|
|
405
|
+
{
|
|
406
|
+
$entity = $args->getObject();
|
|
407
|
+
|
|
408
|
+
if (!$entity instanceof User) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Hash password before insert
|
|
413
|
+
if ($entity->getPlainPassword() !== null) {
|
|
414
|
+
$entity->setPassword(
|
|
415
|
+
$this->hasher->hashPassword($entity, $entity->getPlainPassword())
|
|
416
|
+
);
|
|
417
|
+
$entity->eraseCredentials();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
\`\`\`
|
|
422
|
+
|
|
423
|
+
## Performance Optimization
|
|
424
|
+
\`\`\`php
|
|
425
|
+
<?php
|
|
426
|
+
|
|
427
|
+
// Eager loading to avoid N+1
|
|
428
|
+
$users = $this->createQueryBuilder('u')
|
|
429
|
+
->leftJoin('u.posts', 'p')
|
|
430
|
+
->addSelect('p') // Must addSelect for eager loading
|
|
431
|
+
->getQuery()
|
|
432
|
+
->getResult();
|
|
433
|
+
|
|
434
|
+
// Partial loading (only specific fields)
|
|
435
|
+
$users = $this->createQueryBuilder('u')
|
|
436
|
+
->select('partial u.{id, email, name}')
|
|
437
|
+
->getQuery()
|
|
438
|
+
->getResult();
|
|
439
|
+
|
|
440
|
+
// DTO projection (best performance)
|
|
441
|
+
$users = $this->createQueryBuilder('u')
|
|
442
|
+
->select('NEW App\\DTO\\UserDTO(u.id, u.email, u.name)')
|
|
443
|
+
->getQuery()
|
|
444
|
+
->getResult();
|
|
445
|
+
|
|
446
|
+
// Batch processing
|
|
447
|
+
$batchSize = 100;
|
|
448
|
+
$i = 0;
|
|
449
|
+
foreach ($users as $user) {
|
|
450
|
+
$user->setIsActive(false);
|
|
451
|
+
|
|
452
|
+
if (++$i % $batchSize === 0) {
|
|
453
|
+
$this->em->flush();
|
|
454
|
+
$this->em->clear(); // Detach all entities
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
$this->em->flush();
|
|
458
|
+
\`\`\`
|
|
459
|
+
|
|
460
|
+
## ✅ DO
|
|
461
|
+
- Use typed properties and return types
|
|
462
|
+
- Use \`#[ORM\\HasLifecycleCallbacks]\` for entity hooks
|
|
463
|
+
- Use QueryBuilder for complex queries
|
|
464
|
+
- Use \`wrapInTransaction\` for multi-entity operations
|
|
465
|
+
- Use \`addSelect\` with joins to avoid N+1
|
|
466
|
+
- Use DTOs for read-only queries
|
|
467
|
+
|
|
468
|
+
## ❌ DON'T
|
|
469
|
+
- Don't forget \`addSelect\` when joining (causes N+1)
|
|
470
|
+
- Don't use \`$em->find()\` in loops (use batch queries)
|
|
471
|
+
- Don't forget \`$em->flush()\` after changes
|
|
472
|
+
- Don't use \`EAGER\` fetch mode (query explicitly)
|
|
473
|
+
- Don't put heavy logic in lifecycle callbacks
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Documentation Skill
|
|
2
|
+
|
|
3
|
+
## README Structure
|
|
4
|
+
\`\`\`markdown
|
|
5
|
+
# Project Name
|
|
6
|
+
|
|
7
|
+
Brief description of what it does.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
\\\`\\\`\\\`bash
|
|
11
|
+
npm install && npm run dev
|
|
12
|
+
\\\`\\\`\\\`
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
- Feature 1
|
|
16
|
+
- Feature 2
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
| Variable | Description | Default |
|
|
20
|
+
|----------|-------------|---------|
|
|
21
|
+
| PORT | Server port | 3000 |
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
See [Architecture Decision Records](./docs/adr/)
|
|
25
|
+
|
|
26
|
+
## API Reference
|
|
27
|
+
See [API.md](./docs/API.md)
|
|
28
|
+
|
|
29
|
+
## Contributing
|
|
30
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
## Architecture Decision Records (ADR)
|
|
34
|
+
\`\`\`markdown
|
|
35
|
+
# ADR-001: Use PostgreSQL for primary database
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
Accepted
|
|
39
|
+
|
|
40
|
+
## Context
|
|
41
|
+
We need a database that supports complex queries, transactions,
|
|
42
|
+
and can scale to millions of records.
|
|
43
|
+
|
|
44
|
+
## Decision
|
|
45
|
+
Use PostgreSQL with Prisma ORM.
|
|
46
|
+
|
|
47
|
+
## Consequences
|
|
48
|
+
### Positive
|
|
49
|
+
- Strong ACID compliance
|
|
50
|
+
- Excellent query performance
|
|
51
|
+
- Rich ecosystem
|
|
52
|
+
|
|
53
|
+
### Negative
|
|
54
|
+
- More complex than SQLite for development
|
|
55
|
+
- Requires managed service for production
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Changelog (Keep a Changelog format)
|
|
59
|
+
\`\`\`markdown
|
|
60
|
+
# Changelog
|
|
61
|
+
|
|
62
|
+
## [Unreleased]
|
|
63
|
+
### Added
|
|
64
|
+
- User authentication with OAuth
|
|
65
|
+
|
|
66
|
+
## [1.2.0] - 2024-01-15
|
|
67
|
+
### Added
|
|
68
|
+
- Dark mode support
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- Memory leak in dashboard component
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- Upgraded to React 18
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
## Code Documentation
|
|
78
|
+
\`\`\`typescript
|
|
79
|
+
/**
|
|
80
|
+
* Calculates the total price including tax and discounts.
|
|
81
|
+
*
|
|
82
|
+
* @param items - Array of cart items
|
|
83
|
+
* @param taxRate - Tax rate as decimal (e.g., 0.08 for 8%)
|
|
84
|
+
* @returns Total price in cents
|
|
85
|
+
* @throws {InvalidDiscountError} If discount code is invalid
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* const total = calculateTotal(cartItems, 0.08);
|
|
89
|
+
* // Returns: 10800 (for $100 + 8% tax)
|
|
90
|
+
*/
|
|
91
|
+
function calculateTotal(items: CartItem[], taxRate: number): number { }
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
## Inline Comments Best Practices
|
|
95
|
+
\`\`\`typescript
|
|
96
|
+
// ❌ BAD: Explains "what" (obvious from code)
|
|
97
|
+
// Loop through users
|
|
98
|
+
for (const user of users) { }
|
|
99
|
+
|
|
100
|
+
// ✅ GOOD: Explains "why" (not obvious)
|
|
101
|
+
// Skip inactive users to avoid sending emails to churned accounts
|
|
102
|
+
for (const user of users.filter(u => u.isActive)) { }
|
|
103
|
+
|
|
104
|
+
// ✅ GOOD: Explains business logic
|
|
105
|
+
// Discount capped at 50% per legal requirement (see LEGAL-123)
|
|
106
|
+
const finalDiscount = Math.min(discount, 0.5);
|
|
107
|
+
|
|
108
|
+
// ✅ GOOD: Warns about non-obvious behavior
|
|
109
|
+
// IMPORTANT: This must run before auth middleware initializes
|
|
110
|
+
app.use(corsMiddleware);
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
## Diagrams as Code (Mermaid)
|
|
114
|
+
\`\`\`markdown
|
|
115
|
+
\\\`\\\`\\\`mermaid
|
|
116
|
+
flowchart TD
|
|
117
|
+
A[User Request] --> B{Authenticated?}
|
|
118
|
+
B -->|Yes| C[Process Request]
|
|
119
|
+
B -->|No| D[Return 401]
|
|
120
|
+
C --> E[Return Response]
|
|
121
|
+
\\\`\\\`\\\`
|
|
122
|
+
|
|
123
|
+
\\\`\\\`\\\`mermaid
|
|
124
|
+
erDiagram
|
|
125
|
+
USER ||--o{ POST : creates
|
|
126
|
+
USER ||--o{ COMMENT : writes
|
|
127
|
+
POST ||--o{ COMMENT : has
|
|
128
|
+
\\\`\\\`\\\`
|
|
129
|
+
|
|
130
|
+
\\\`\\\`\\\`mermaid
|
|
131
|
+
sequenceDiagram
|
|
132
|
+
Client->>+API: POST /login
|
|
133
|
+
API->>+DB: Verify credentials
|
|
134
|
+
DB-->>-API: User data
|
|
135
|
+
API-->>-Client: JWT token
|
|
136
|
+
\\\`\\\`\\\`
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
## API Documentation (OpenAPI)
|
|
140
|
+
\`\`\`yaml
|
|
141
|
+
paths:
|
|
142
|
+
/users/{id}:
|
|
143
|
+
get:
|
|
144
|
+
summary: Get user by ID
|
|
145
|
+
description: Returns a single user. Requires authentication.
|
|
146
|
+
parameters:
|
|
147
|
+
- name: id
|
|
148
|
+
in: path
|
|
149
|
+
required: true
|
|
150
|
+
schema:
|
|
151
|
+
type: string
|
|
152
|
+
format: uuid
|
|
153
|
+
responses:
|
|
154
|
+
'200':
|
|
155
|
+
description: User found
|
|
156
|
+
content:
|
|
157
|
+
application/json:
|
|
158
|
+
schema:
|
|
159
|
+
$ref: '#/components/schemas/User'
|
|
160
|
+
example:
|
|
161
|
+
id: "123e4567-e89b-12d3-a456-426614174000"
|
|
162
|
+
email: "user@example.com"
|
|
163
|
+
name: "John Doe"
|
|
164
|
+
'404':
|
|
165
|
+
description: User not found
|
|
166
|
+
\`\`\`
|
|
167
|
+
|
|
168
|
+
## ❌ DON'T
|
|
169
|
+
- Document obvious code
|
|
170
|
+
- Let docs become stale
|
|
171
|
+
- Write docs with no examples
|
|
172
|
+
- Use outdated screenshots
|
|
173
|
+
- Write walls of text
|
|
174
|
+
|
|
175
|
+
## ✅ DO
|
|
176
|
+
- Document the "why", not the "what"
|
|
177
|
+
- Include runnable examples
|
|
178
|
+
- Keep docs near the code
|
|
179
|
+
- Update docs with code changes
|
|
180
|
+
- Use diagrams for complex flows
|
|
181
|
+
- Write ADRs for architectural decisions
|
|
182
|
+
- Keep a changelog for releases
|