dotkernel-doctrine-entities
Use when adding or changing anything persisted in a Dotkernel project — a Doctrine entity, column, relation, index, repository, backed enum with its DBAL type, fixture, or migration. Also use for ques
SKILL.md.claude/skills/dotkernel-doctrine-entities/SKILL.md
GitHubEntities, repositories and migrations
Applies to
Projects: - Dotkernel API
- Dotkernel Admin
- Dotkernel Frontend
Packages: - none
In the stock API and Admin, everything here lives in the Core layer —
src/Core/src/<Module>/src/ — and migrations in src/Core/src/App/src/Migration/ (see
dotkernel-core-submodule). The stock Frontend also uses Doctrine but keeps entities in its own
modules and migrations in data/doctrine/migrations/; there, match its layout rather than this
skill's paths.
Entity anatomy
#[ORM\Entity(repositoryClass: BookRepository::class)]
#[ORM\Table(name: 'book')]
#[ORM\HasLifecycleCallbacks]
class Book extends AbstractEntity
{
use TimestampsTrait;
use UuidIdentifierTrait;
#[ORM\Column(name: 'title', type: 'string', length: 191)]
protected string $title;
#[ORM\Column(
type: 'book_status_enum',
enumType: BookStatusEnum::class,
options: ['default' => BookStatusEnum::Draft]
)]
protected BookStatusEnum $status = BookStatusEnum::Draft;
/** @var Collection<int, Review> */
#[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
protected Collection $reviews;
}
Rules:
- Extend the
Coreabstract entity — it implementsArraySerializableInterface, which the hydrators and serialisers rely on. - UUID identifier trait for anything exposed in a URL; the numeric trait only for internal join and lookup tables. Public identifiers are UUIDs, never auto-increment ids.
- Timestamps trait plus
#[ORM\HasLifecycleCallbacks]for created/updated. - Password trait for anything with a password — it hashes for you. Never hash by hand.
- Initialise every
Collectionproperty in the constructor withnew ArrayCollection(), and callparent::__construct(). length: 191is the house default for indexed strings (utf8mb4-safe). Keep the matchingStringLengthvalidator in sync.- Reserved words need backticks in the table name.
- Annotate collections (
@var Collection<int, X>) or phpstan will fail. - Fluent setters returning
self, matching the rest of the codebase.
Backed enums need three pieces
A column backed by an enum requires:
Core\<Module>\Enum\BookStatusEnum— a string-backed enum.Core\<Module>\DBAL\Types\BookStatusEnumType, extending theCoreabstract enum type and implementinggetEnumClass()andgetName(). The name is the string used in the column'stype:.- Registration under
doctrine.typesin the moduleConfigProvider.
The abstract enum type emits the right SQL per platform — a native enum on MySQL/MariaDB and
PostgreSQL, TEXT on SQLite. That last one is why in-memory functional tests work; a
hand-rolled type that ignores SQLite will pass locally and fail in CI.
Repositories
Extend the Core abstract repository. Query builders live here; business rules do not. Return
QueryBuilder from list methods so the caller can paginate.
Pagination parameters are normalised by the shared paginator helper, and the sort column must be whitelisted in the service before it reaches the query builder.
Migrations
php ./vendor/bin/doctrine-migrations diff # generate — ASK FIRST, it reads the live DB
php ./vendor/bin/doctrine-migrations migrate # apply — the user runs it; see below
php ./vendor/bin/doctrine-migrations status
diff and status prompt for approval. migrate and execute are refused outright by the
guard-bash.sh hook, approval or not — never try to run them. Give the user the exact command to
run and say what it will change.
- Generated into the configured migrations path — never hand-edit. A wrong diff means the entity is wrong; fix that and regenerate.
- Review the generated SQL before applying.
diffwill happily propose dropping a column. - One logical schema change per migration.
- Where a table prefix is supported, it is applied by an event listener — never hardcode prefixes into table names.
- The same migration must be applied by every project sharing that database.
- A schema-validation CI job will fail when entities and migrations drift. If it is red, a migration is missing — do not edit the entity to match an old migration.
Fixtures
Seed data via the data-fixtures package. Order matters: roles before the records referencing them.
php ./bin/doctrine fixtures:list
php ./bin/doctrine fixtures:execute # the user runs it
php ./bin/doctrine fixtures:execute --class=BookLoader # the user runs it
fixtures:list prompts for approval. fixtures:execute writes to the database and is refused
outright by the guard-bash.sh hook — give the user the command to run instead.
Functional tests typically build a fresh schema and load every fixture per test, so a new fixture immediately affects all of them. Keep fixtures small and deterministic.
Checklist
- Entity + Repository in
Core. - Enum + DBAL type +
doctrine.typesregistration for every enum column. - Collections initialised and annotated.
- Column lengths mirrored in the validators.
- Migration generated (not written), reviewed, applied.
- Fixture added if the table needs seed data.
- Every other project on this database accounted for.
- Static analysis clean — the Doctrine phpstan extension is strict.