| name | database-migrations |
| description | Create, manage, and apply database migrations using Doctrine ORM (MySQL for this service). 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)
- Repository implementation needed
- Database schema validation fails
- Need to set up indexes for performance
Task (Function)
Create entities with XML mapping and repositories following hexagonal architecture and relational database best practices (Doctrine ORM, MySQL).
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.
Doctrine ORM (MySQL)
- 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 migration/ORM commands
Note: Examples inherited from the template show MongoDB/ODM structures. In this project use .orm.xml mappings and Doctrine ORM migrations instead of ODM commands.
Quick Start
Creating a New Entity
Step 1: Create Entity (Domain Layer)
namespace App\Core\{Context}\Domain\Entity;
final class Customer
{
public function __construct(
private string $id,
private string $name,
private string $email,
private \DateTimeImmutable $createdAt
) {}
}
Step 2: Create XML Mapping
<entity name="App\Core\Customer\Domain\Entity\Customer" repository-class="App\Core\Customer\Infrastructure\Repository\CustomerRepository">
<id name="id" type="domain_uuid"/>
<field name="name" type="string" length="255"/>
<field name="email" type="string" length="255" unique="true"/>
<field name="createdAt" column="created_at" type="datetime_immutable"/>
</entity>
Step 3: Configure API Platform
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:schema:validate
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
- Validate Schema:
docker compose exec php bin/console doctrine:schema:validate
See: entity-modification-guide.md
Creating Repositories
Step 1: Define Interface (Domain)
interface CustomerRepositoryInterface
{
public function save(Customer $customer): void;
public function findById(string $id): ?Customer;
}
Step 2: Implement (Infrastructure)
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
See: repository-patterns.md
Database-Specific Features (Doctrine ORM/MySQL)
Custom Types
| Type | Usage | Purpose |
|---|
ulid | Primary/foreign keys | Sortable, time-ordered identifiers |
domain_uuid | Domain identifiers | Standard UUID format (RFC 4122) |
<id name="id" type="domain_uuid"/>
<field name="token" type="ulid"/>
Indexes & Constraints
- Use
unique=\"true\" on fields or <unique-constraint> elements for uniqueness (e.g., email).
- Add
<indexes> with <index name=\"idx_email\" columns=\"email\"/> for frequent lookups.
- Always index columns used in filters/sorting (email, token, foreign keys, timestamps).
Relationships & Value Objects
- Model associations with Doctrine relations (
one-to-one, one-to-many, many-to-many).
- Persist value objects as simple fields; avoid framework validation inside Domain.
Available Commands
make doctrine-migrations-migrate
make doctrine-migrations-generate
make setup-test-db
docker compose exec php bin/console doctrine:schema:validate
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-db before 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-db to 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:schema:validate
Mapping files are correct.
Expected Test DB Setup Output
$ make setup-test-db
Database dropped and recreated successfully
Verification Checklist
After entity/migration changes:
Related Skills
Quick Commands
docker compose exec php bin/console doctrine:schema:validate
make setup-test-db
make cache-clear
make doctrine-migrations-generate
make doctrine-migrations-migrate
Reference Documentation
Detailed guides and examples:
Migration Best Practices
1. Clean Up Empty Migrations
MANDATORY: Delete empty migrations immediately.
public function up(Schema $schema): void { }
public function down(Schema $schema): void { }
2. Test Before Committing
- Apply migration on dev database
- Verify schema:
doctrine:schema:validate
- Run all tests
- Test rollback if applicable
3. Production Safety
make doctrine-migrations-migrate
Testing with Database
Setup Test Database
make setup-test-db
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();
$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 database
docker compose logs database
Schema Sync Issues:
docker compose exec php bin/console doctrine:schema:validate
docker compose exec php bin/console doctrine:migrations:status
Migration Conflicts:
docker compose exec php bin/console doctrine:migrations:status
docker compose exec php bin/console doctrine:migrations:migrate prev
See: reference/troubleshooting.md for comprehensive guide.