Skip to content
Development
Skill

/symfony-doctrine-persistence

Doctrine persistence for Symfony hexagonal architecture (ORM 2.x/3.x, DBAL 3/4, DoctrineBundle 2/3): repository adapters, XML mapping in Infrastructure, embeddables, enums, custom DBAL types, native lazy objects, migrations, transactions, read models with DBAL QueryBuilder,

From plugin
symfony-hexagonal
3510 skills2 agents1 hook

Claude Code

$ npx -y skills add aligundogdu/symfony-hexagonal-skill --skill symfony-doctrine-persistence --agent claude-code

SKILL.md

symfony-doctrine-persistence.SKILL.md
name: symfony-doctrine-persistence
description: "Doctrine persistence for Symfony hexagonal architecture (ORM 2.x/3.x, DBAL 3/4, DoctrineBundle 2/3): repository adapters, XML mapping in Infrastructure, embeddables, enums, custom DBAL types, native lazy objects, migrations, transactions, read models with DBAL QueryBuilder, strict no-native-SQL rule. Use when: doctrine, repository, persistence, database, mapping, migration, ORM, entity manager, DBAL, transaction, query builder, pagination"

Symfony Doctrine Persistence

You are an expert in Doctrine ORM within Symfony hexagonal architecture.

When to Activate

  • User needs a repository implementation
  • User asks about entity mapping
  • User needs migration management
  • User discusses transactions or database access

Version Notes

| Topic | Doctrine ORM 2.x (Symfony 6.4) | Doctrine ORM 3.x (Symfony 7.4 / 8.x) | |---|---|---| | Mapping drivers | XML, attributes, annotations, YAML | XML and attributes only (annotations + YAML removed) | | XML mapping | XSD validation optional | XSD validation always on: invalid XML fails at boot | | DBAL | 3.x | 4.x (`Type::getName()` removed, register types by name) | | Lazy loading | proxy classes | PHP 8.4 + ORM 3.4+: native lazy objects (`enable_native_lazy_objects: true`, DoctrineBundle 2.16+; always on in DoctrineBundle 3.1) | | Pagination | `Tools\Pagination\Paginator` | ORM 3.7+: `OffsetPaginator` + `Window` (`Paginator` deprecated) | | Readonly properties / `private(set)` / hooks | readonly since 2.13 | readonly; asymmetric visibility + property hooks since 3.4 |

XML entity mapping is unaffected by Symfony 8 removing XML *service/route* configuration.

Core Rules

1. **Mapping NEVER in Domain**: No `#[ORM\Entity]` or annotations on domain entities 2. **Mapping in Infrastructure**: Use XML or separate mapping files in `Infrastructure/{Module}/Persistence/Mapping/` 3. **Repository = Adapter**: Implements domain port interface 4. **Event dispatch after persist**: Repository dispatches domain events after flush, with `DispatchAfterCurrentBusStamp` 5. **NEVER use native/raw SQL**: No `$connection->executeQuery()`, `$connection->executeStatement()`, `NativeQuery`, `$connection->prepare()`, or raw SQL strings anywhere in application code. Always use Doctrine QueryBuilder (ORM or DBAL), DQL, finder methods, or Criteria API. The only exception is Doctrine Migrations (`$this->addSql()`) which requires raw SQL by design.

Repository Adapter Pattern

namespace App\Infrastructure\{Module}\Persistence;

use App\Domain\{Module}\Entity\{Entity};
use App\Domain\{Module}\Port\{Entity}RepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp;

final readonly class Doctrine{Entity}Repository implements {Entity}RepositoryInterface
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private MessageBusInterface $eventBus,
    ) {
    }

    public function save({Entity} $entity): void
    {
        $this->entityManager->persist($entity);
        $this->entityManager->flush();

        foreach ($entity->pullDomainEvents() as $event) {
            // released after the current command (and its transaction) succeeds
            $this->eventBus->dispatch($event, [new DispatchAfterCurrentBusStamp()]);
        }
    }

    public function findById({Entity}Id $id): ?{Entity}
    {
        return $this->entityManager->find({Entity}::class, $id->value);
    }

    public function remove({Entity} $entity): void
    {
        $this->entityManager->remove($entity);
        $this->entityManager->flush();
    }
}

Mapping Strategies

XML Mapping (required for Domain entities)

File: `src/Infrastructure/User/Persistence/Mapping/Entity.User.orm.xml` (name = class relative to the mapping `prefix`, see `references/mapping-patterns.md`)

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                  https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    <entity name="App\Domain\User\Entity\User" table="users">
        <id name="id" type="user_id" column="id" />
        <embedded name="email" class="App\Domain\User\ValueObject\Email" use-column-prefix="false" />
        <field name="name" type="string" />
        <field name="createdAt" type="datetime_immutable" column="created_at" />
    </entity>
</doctrine-mapping>

Why not attributes?

`#[ORM\Entity]` attributes must sit on the entity class itself, which lives in Domain. XML is the supported driver that keeps the mapping in Infrastructure. Legacy modules may keep attributes until they are refactored.

Migration Workflow

**Always ask the user** which strategy they prefer: 1. **Auto-diff**: `php bin/console doctrine:migrations:diff` (generates from mapping) 2. **Manual**: Write migrations by hand for full control

Why No Native SQL?

Native/raw SQL bypasses Doctrine's abstraction layers and creates several problems:

  • **Database portability lost** - Raw SQL ties code to a specific database engine (MySQL, PostgreSQL, etc.)
  • **No type safety** - QueryBuilder provides parameter binding with type inference, raw SQL doesn't
  • **Mapping bypass** - Entities, value objects, and custom types are ignored when using raw SQL
  • **Harder to maintain** - SQL strings scattered in code are harder to refactor than QueryBuilder chains
  • **Security risk** - Raw SQL increases the surface for SQL injection if parameters are not properly bound

Detecting Native SQL (for code review)

Flag these patterns as CRITICAL violations:

// FORBIDDEN - native SQL patterns
$connection->executeQuery('SELECT ...');
$connection->exe
Read more
@aligundogdu
Ships withsymfony-hexagonal

A Claude Code plugin that enforces hexagonal architecture (ports & adapters) in Symfony projects. Works with both new projects (full scaffolding) and existing projects (progressive, module-by-module refactoring).

3510 skills2 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
35
Star history of aligundogdu/symfony-hexagonal-skill on GitHub, a chart by star-history.com

Other skills on symfony-hexagonal.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills2w309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react5d252k
@facebook@facebookInstall Skill

Questions about symfony-doctrine-persistence.

Which coding agents can use symfony-doctrine-persistence?
symfony-doctrine-persistence installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.