本文へ移動
cccskills
無料GitHub で公開

doctrine-patterns

Doctrine ORM best practices for Symfony: entity mapping as source of truth, repositories, DQL/QueryBuilder with parameters, N+1 prevention with fetch joins, relations (cascade/fetch/orphanRemoval), lifecycle events, batch processing, and generated migrations. Apply when: writing or reviewing Doctrine entities, repositories, and queries. Activated automatically by symfony-plugin/stack.md as a convention skill for the development and database phases. Do NOT use this skill for: - PHP language idioms — see php-foundation:php-conventions. - Controllers/services/forms — see symfony-plugin:symfony-conventions.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md7.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Doctrine Patterns (Symfony)

Patterns for working with Doctrine ORM that catch the common pitfalls (N+1, DQL injection, missing indexes, broken migrations). In Doctrine the entity mapping is the source of truth and migrations are generated from it.

1. Entity mapping with attributes

namespace App\Entity;

use App\Repository\SubscriptionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: SubscriptionRepository::class)]
#[ORM\Table(name: 'subscriptions')]
#[ORM\Index(columns: ['user_id', 'status'])]
class Subscription
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\ManyToOne(inversedBy: 'subscriptions')]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private User $user;

    #[ORM\Column(length: 255, unique: true)]
    private string $stripeCustomerId;

    #[ORM\Column(enumType: Status::class)]
    private Status $status;

    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]
    private string $amount;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private \DateTimeImmutable $startsAt;
}
  • Use enumType: to map a backed enum directly.
  • Money/decimals map to Types::DECIMAL (string in PHP — never float).
  • Prefer DATETIME_IMMUTABLE over mutable DateTime.
  • Declare indexes/unique constraints on the entity — they become migration SQL.

2. Repositories — extend ServiceEntityRepository

namespace App\Repository;

use App\Entity\Subscription;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/** @extends ServiceEntityRepository<Subscription> */
final class SubscriptionRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Subscription::class);
    }

    /** @return Subscription[] */
    public function activeForUser(User $user): array
    {
        return $this->createQueryBuilder('s')
            ->where('s.user = :user')
            ->andWhere('s.status = :status')
            ->setParameter('user', $user)
            ->setParameter('status', Status::Active)
            ->getQuery()
            ->getResult();
    }
}

Keep query logic in repositories — never build queries inline in controllers. Repositories are autowired by type.

3. DQL / QueryBuilder — always parameterized

// Right — named parameters
$em->createQuery('SELECT s FROM App\Entity\Subscription s WHERE s.status = :status')
   ->setParameter('status', Status::Active)
   ->getResult();

// Wrong — string concatenation (DQL injection)
$em->createQuery("SELECT s FROM App\Entity\Subscription s WHERE s.user = " . $userId);  // ❌
$qb->where("s.name = '" . $input . "'");                                                 // ❌

Positional (?1) or named (:name) parameters only. The same rule applies to native SQL via Connection::executeQuery($sql, $params).

4. N+1 prevention — fetch joins

The single most common Doctrine performance bug.

// Problem: lazy relation accessed in a loop → 1 query per row
$subscriptions = $repo->findAll();
foreach ($subscriptions as $s) {
    echo $s->getUser()->getEmail();   // N+1
}

// Solution: JOIN FETCH
$repo->createQueryBuilder('s')
    ->addSelect('u')
    ->join('s.user', 'u')
    ->getQuery()
    ->getResult();

For read-heavy paths, also consider:

  • setFetchMode / #[ORM\... fetch: 'EAGER'] only for relations always needed (use sparingly).
  • Pagination with Doctrine\ORM\Tools\Pagination\Paginator when fetch-joining to-many relations with LIMIT.

5. Relations — cascade, fetch, orphanRemoval

#[ORM\OneToMany(mappedBy: 'subscription', targetEntity: Invoice::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $invoices;

#[ORM\ManyToOne(fetch: 'LAZY')]
#[ORM\JoinColumn(onDelete: 'CASCADE')]
private User $user;
  • Default fetch: 'LAZY' — avoid EAGER (it loads on every query, even when unused).
  • cascade: ['persist'] to save children with the parent; avoid cascade: ['remove'] for large graphs — prefer DB onDelete.
  • orphanRemoval: true deletes children removed from the collection.
  • onDelete on JoinColumn enforces referential action at the DB level (faster than ORM cascade).

6. Identity map and flush discipline

  • The EntityManager tracks loaded entities (identity map) — fetching the same row twice returns the same object.
  • Call flush() once per unit of work, not per entity. Doctrine batches the SQL inside a transaction.
foreach ($rows as $row) {
    $em->persist($this->toEntity($row));
}
$em->flush();   // single transaction

7. Batch processing — clear to avoid memory blowup

$batchSize = 100;
$q = $em->createQuery('SELECT s FROM App\Entity\Subscription s');
foreach ($q->toIterable() as $i => $subscription) {
    $subscription->expire();
    if (($i % $batchSize) === 0) {
        $em->flush();
        $em->clear();   // detach processed entities, free memory
    }
}
$em->flush();
$em->clear();

8. Lifecycle events / listeners

For cross-cutting concerns (timestamps, audit), prefer entity listeners or event subscribers over fat services.

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
class Subscription
{
    #[ORM\PrePersist]
    public function onCreate(): void
    {
        $this->createdAt = new \DateTimeImmutable();
    }
}

For reusable behaviour across entities (e.g. timestampable), use a dedicated #[AsEntityListener] or the StofDoctrineExtensionsBundle rather than copying callbacks.

9. Migrations — generated, then reviewed

  • The mapping is the source of truth. Generate: php bin/console doctrine:migrations:diff.
  • Review the generated SQL — diff is not always perfect (enum CHECK constraints, defaults, index names).
  • Apply: php bin/console doctrine:migrations:migrate. Roll back one: ... migrate prev.
  • Verify in sync: php bin/console doctrine:schema:validate.
  • Never doctrine:schema:update --force outside throwaway prototypes — it bypasses migration history.

10. Anti-patterns to avoid

Don'tDo
Concatenate input into DQLsetParameter() with :named / ?1
fetch: 'EAGER' everywhereLazy by default; JOIN FETCH where needed
Access lazy relations in a loopFetch join upstream
float for moneyTypes::DECIMAL (string in PHP)
flush() inside a loopflush() once per unit of work
schema:update --forceGenerated migrations + review
Query building in controllersRepository methods

11. Checklist before merging Doctrine changes

  • No N+1: loops over relations have a fetch join upstream
  • All DQL/QueryBuilder uses bound parameters (no concatenation)
  • Indexes declared on FKs and frequently-filtered columns
  • Money/decimals use Types::DECIMAL; datetimes are immutable
  • Relations have correct fetch (lazy) and onDelete
  • flush() called once per unit of work; batch loops clear()
  • Migration generated via diff, SQL reviewed, down() reverses cleanly
  • doctrine:schema:validate reports in sync

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する