Create, manage, and apply database schema changes with Doctrine โ versioned migrations when the profile's persistence.mapper is doctrine-orm, mapping-driven schema/index sync when it is doctrine-odm. Use when modifying entities or documents, adding fields, managing database schema changes, creating repositories, or troubleshooting schema/mapping issues.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Create, manage, and apply database schema changes with Doctrine โ versioned migrations when the profile's persistence.mapper is doctrine-orm, mapping-driven schema/index sync when it is doctrine-odm. Use when modifying entities or documents, adding fields, managing database schema changes, creating repositories, or troubleshooting schema/mapping issues.
Key difference: Path A keeps an append-only history of migration files;
Path B has no migration files by default โ collections and indexes
are derived from the XML mappings and synced with a console command, and
only data backfills/renames need an explicit script or console command.
When framework.name is symfony and the service runs in containers,
boot with the target mapped by make.start and run every bin/console
command inside the PHP container
(docker compose exec <php-service> bin/console ...).
Task (Function)
Create entities/documents with XML mappings and repositories following
hexagonal architecture, then apply and verify the schema change via the
mapper-appropriate mechanism.
Success criteria: mappings validate, the test database recreates
without errors, all tests pass, the targets mapped by make.deptrac and
make.ci stay green.
Core Principles
Domain-Driven Design
With <src> = architecture.source_root and <Context> one of
architecture.bounded_contexts:
Use XML mappings for all metadata โ never annotations/attributes
in Domain classes (keeps the Domain framework-agnostic)
Define indexes in the XML mapping so code and schema stay in sync
Use custom identifier types (see below) instead of auto-increment ids
Custom identifier types
Registered in the shared context's Infrastructure layer
(<src>/<architecture.shared_context>/Infrastructure/DoctrineType/, or
the owning bounded context's own Infrastructure/DoctrineType/ when
architecture.shared_context is null):
Type
Usage
Purpose
ulid
Primary keys, tokens
Sortable, time-ordered, URL-safe (26 chars)
domain_uuid
Domain identifiers
RFC 4122 UUID v4
Prefer ulid for primary keys (time-ordered โ efficient indexing) and
domain_uuid where standard UUID compatibility matters. Generate in a
named constructor (Ulid::generate() / Uuid::v4()), never in the DB.
Quick Start: Creating a New Entity/Document
Step 1: Create the class (Domain layer โ identical for both paths)
// <src>/<Context>/Domain/Entity/Customer.phpnamespaceApp\Customer\Domain\Entity; // namespace mirrors <src>/<Context>finalclassCustomer{
publicfunction__construct(privatestring$id,
privatestring$name,
privatestring$email,
private \DateTimeImmutable $createdAt) {}
// Getters only โ no setters (immutability); no framework imports
}
Step 2: Create the XML mapping
Path A (doctrine-orm) โ config/doctrine/Customer.orm.xml:
Uniqueness: unique="true" on a field or <unique-constraint>
<indexes> for frequent lookups; always index columns used in
filters/sorting (email, token, foreign keys, timestamps)
Associations: Doctrine relations (one-to-one, one-to-many,
many-to-many); persist value objects as embeddables or simple fields
Never put framework validation inside Domain classes
Path B: ODM (MongoDB)
Compound indexes are used left-to-right: {status, type, createdAt}
serves filters on status and status+type, NOT on type alone
Options: unique, sparse (index only documents having the field),
expireAfterSeconds (TTL auto-deletion), type=text (text search)
Keep indexes to roughly 3โ5 per collection โ each one slows writes
Value objects โ embedded documents (<embedded-document> mapping +
embed-one/embed-many in the parent), loaded with the parent โ
keep them small
Entity references โ IRI strings (e.g. "/api/customer_types/<id>")
stored as plain string fields, NOT reference-one DBRefs: API Platform
expects IRIs, and DBRefs add lazy-loading complexity and overhead
Integration test pattern (run via the target mapped by make.tests):
finalclassCustomerRepositoryTestextendsIntegrationTestCase{
private CustomerRepositoryInterface $repository;
protectedfunctionsetUp(): void{
parent::setUp();
$this->repository = $this->getContainer()->get(CustomerRepositoryInterface::class);
}
publicfunctiontestSaveAndRetrieveCustomer(): void{
$customer = newCustomer(/* unique test data with Faker */);
$this->repository->save($customer);
$this->assertNotNull($this->repository->findById($customer->getId()));
}
}
New repository tests must keep the mutation score at or above
quality.infection_msi (canonical default 100 โ raise-only: a profile
may tighten the floor, never lower it).
Migration best practices (Path A) / schema-sync practices (Path B)
Review every generated migration โ doctrine:migrations:diff
output can include unrelated drift; delete empty ones
Test before committing: apply on dev, validate, run all tests,
test rollback (doctrine:migrations:migrate prev) where applicable
Production safety: back up first, apply, verify the application
works, keep the backup for rollback
Path B: index changes are online by default (MongoDB 4.2+), but
destructive mapping changes (field renames, drops) silently orphan
data โ pair them with an explicit data-migration command
Verification Checklist
Class in Domain layer (no framework imports)
XML mapping in config/doctrine/ (.orm.xml / .mongodb.xml per persistence.mapper)
API resource configured (when framework.api_platform is enabled)
Repository interface in Domain, implementation in Infrastructure, alias in services.yaml
Schema sync issues โ re-validate and check state:
bin/console doctrine:schema:validate # Path A
bin/console doctrine:migrations:status # Path A
bin/console doctrine:mongodb:mapping:info # Path B
Migration conflicts (Path A) โ check doctrine:migrations:status;
roll back with doctrine:migrations:migrate prev, fix, regenerate.
Index conflict (Path B) โ an existing index with the same name but
different options blocks schema:update; drop the old index in mongosh
(db.<collection>.dropIndex("<name>")), then re-run the sync.