| name | database-migrations |
| description | Create, manage, and apply database migrations using Doctrine ODM for MongoDB. Use when modifying entities, adding fields, managing database schema changes, creating repositories, or troubleshooting database issues. |
Database Migrations Skill
Context (Input)
- New entity needs database persistence
- Existing entity requires schema changes (add/modify/remove fields)
- MongoDB repository needs implementation
- Database schema validation fails
- Need to set up indexes for performance
Task (Function)
Create entities with XML mapping and repositories following hexagonal architecture and MongoDB best practices.
Success Criteria: make setup-test-db runs without errors, schema validates, all tests pass.
Core Principles
Domain-Driven Design
- Entities: Domain layer (
{Context}/Domain/Entity/) - Repository Interfaces: Domain layer (
{Context}/Domain/Repository/) - Repository Implementations: Infrastructure layer (
{Context}/Infrastructure/Repository/) - XML Mappings: Infrastructure concern (
config/doctrine/)
See: implementing-ddd-architecture for DDD patterns.
MongoDB with Doctrine ODM
- Use XML mappings for all entity metadata (not annotations/attributes)
- Define indexes in XML for performance
- Use custom types (ULID, DomainUuid) for identifiers
- Schema updates applied via Doctrine commands
Quick Start
Creating a New Entity
Step 1: Create Entity (Domain Layer)
// src/Core/{Context}/Domain/Entity/{Entity}.php
namespace App\Core\{Context}\Domain\Entity;
final class Customer
{
public function __construct(
private string $id,
private string $name,
private string $email,
private \DateTimeImmutable $createdAt
) {}
// Getters only - no setters (immutability)
}
Step 2: Create XML Mapping
<!-- config/doctrine/Customer.mongodb.xml -->
<document name="App\Core\Customer\Domain\Entity\Customer" collection="customers">
<field name="id" fieldName="id" id="true" type="ulid"/>
<field name="name" type="string"/>
<field name="email" type="string"/>
<field name="createdAt" fieldName="created_at" type="date_immutable"/>
<indexes>
<index><key name="email" order="asc"/><option name="unique" value="true"/></index>
</indexes>
</document>
Step 3: Configure API Platform
# config/api_platform/resources/customer.yaml
App\Core\Customer\Domain\Entity\Customer:
shortName: Customer
operations:
get_collection: ~
get: ~
post: ~
Step 4: Update Schema
make cache-clear
docker compose exec php bin/console doctrine:mongodb:schema:update
See: entity-creation-guide.md for complete workflow.
Modifying Existing Entities
- Update Entity Class (add/modify fields)
- Update XML Mapping (add field definitions)
- Clear Cache:
make cache-clear - Update Schema:
docker compose exec php bin/console doctrine:mongodb:schema:update
See: entity-modification-guide.md
Creating Repositories
Step 1: Define Interface (Domain)
// Domain/Repository/CustomerRepositoryInterface.php
interface CustomerRepositoryInterface
{
public function save(Customer $customer): void;
public function findById(string $id): ?Customer;
}
Step 2: Implement (Infrastructure)
// Infrastructure/Repository/CustomerRepository.php
final class CustomerRepository implements CustomerRepositoryInterface
{
public function __construct(
private readonly DocumentManager $documentManager
) {}
public function save(Customer $customer): void
{
$this->documentManager->persist($customer);
$this->documentManager->flush();
}
}
Step 3: Register in services.yaml
App\Core\Customer\Domain\Repository\CustomerRepositoryInterface:
alias: App\Core\Customer\Infrastructure\Repository\CustomerRepository
MongoDB-Specific Features
Custom Types
| Type | Usage | Purpose |
|---|---|---|
ulid |
MongoDB _id field |
Sortable, time-ordered identifiers |
domain_uuid |
Domain identifiers | Standard UUID format (RFC 4122) |
<field name="ulid" id="true" type="ulid"/>
<field name="customerId" type="domain_uuid"/>
Indexes
<indexes>
<!-- Unique index -->
<index><key name="email" order="asc"/><option name="unique" value="true"/></index>
<!-- Performance index -->
<index><key name="createdAt" order="desc"/></index>
<!-- Compound index -->
<index><key name="status"/><key name="type"/></index>
</indexes>
Embedded Documents
For Value Objects:
<embed-one field="address" target-document="App\Core\Customer\Domain\ValueObject\Address"/>
<embed-many field="tags" target-document="App\Core\Customer\Domain\ValueObject\Tag"/>
See: mongodb-specifics.md
Available Commands
# Schema Management
make doctrine-migrations-migrate # Apply pending migrations
make doctrine-migrations-generate # Create empty migration file
make setup-test-db # Drop and recreate test database
# Schema Operations
docker compose exec php bin/console doctrine:mongodb:schema:update # Update schema
docker compose exec php bin/console doctrine:mongodb:schema:validate # Validate schema
Constraints (Parameters)
NEVER
- Use Doctrine annotations/attributes in Domain entities
- Modify existing migrations after they're applied
- Skip XML mapping validation
- Leave empty migration files in codebase
- Commit without testing schema changes
- Skip
make setup-test-dbbefore integration tests
ALWAYS
- Create XML mappings for all entity metadata
- Keep Domain entities framework-agnostic
- Define indexes for frequently queried fields
- Test migrations on dev database before committing
- Run
make setup-test-dbto verify schema - Use Faker for unique test data (emails, names, etc.)
- Register resource directories in
api_platform.yaml
Format (Output)
Expected Schema Validation Output
$ docker compose exec php bin/console doctrine:mongodb:schema:validate
The MongoDB schema is valid.
Expected Test DB Setup Output
$ make setup-test-db
Database dropped and recreated successfully
Verification Checklist
After entity/migration changes:
- Entity defined in Domain layer (no framework imports)
- XML mapping created in
config/doctrine/ - API Platform resource configured (if needed)
- Repository interface in Domain layer
- Repository implementation in Infrastructure layer
- Repository registered in
services.yaml - Schema validates:
doctrine:mongodb:schema:validate - Test database setup works:
make setup-test-db - All integration tests pass
-
make deptracpasses (no violations) -
make cipasses
Related Skills
- implementing-ddd-architecture - DDD patterns and repository interfaces
- api-platform-crud - Configuring API Platform resources
- deptrac-fixer - Fixing architectural violations
Quick Commands
# Validate schema
docker compose exec php bin/console doctrine:mongodb:schema:validate
# Update schema
docker compose exec php bin/console doctrine:mongodb:schema:update
# Setup test database
make setup-test-db
# Clear cache after config changes
make cache-clear
Reference Documentation
Detailed guides and examples:
- entity-creation-guide.md - Complete entity creation workflow
- entity-modification-guide.md - Modifying existing entities
- repository-patterns.md - Repository implementation patterns
- mongodb-specifics.md - MongoDB features and patterns
- reference/troubleshooting.md - Common issues and solutions
- examples/ - Complete working examples
Migration Best Practices
1. Clean Up Empty Migrations
MANDATORY: Delete empty migrations immediately.
// ❌ DELETE: No actual changes
public function up(Schema $schema): void { }
public function down(Schema $schema): void { }
2. Test Before Committing
- Apply migration on dev database
- Verify schema:
doctrine:mongodb:schema:validate - Run all tests
- Test rollback if applicable
3. Production Safety
# Always backup before migration
# Apply migration
make doctrine-migrations-migrate
# Verify application works
# Keep backup for potential rollback
Testing with Database
Setup Test Database
make setup-test-db # Before integration/E2E tests
Integration Test Pattern
final class CustomerRepositoryTest extends IntegrationTestCase
{
private CustomerRepositoryInterface $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = $this->getContainer()->get(CustomerRepositoryInterface::class);
}
public function testSaveAndRetrieveCustomer(): void
{
$customer = new Customer(/* unique test data with Faker */);
$this->repository->save($customer);
$retrieved = $this->repository->findById($customer->getId());
$this->assertNotNull($retrieved);
}
}
Important: Always use Faker for unique test data.
Troubleshooting
Common Issues
Database Connection Errors:
docker compose ps mongodb
docker compose logs mongodb
Schema Sync Issues:
docker compose exec php bin/console doctrine:mongodb:schema:validate
docker compose exec php bin/console doctrine:mongodb:schema:update --force
Migration Conflicts:
docker compose exec php bin/console doctrine:migrations:status
docker compose exec php bin/console doctrine:migrations:migrate prev # Rollback
See: reference/troubleshooting.md for comprehensive guide.