Skip to content

feat: Doctrine persistence infrastructure (Plan 2)#3

Merged
erikfrerejean merged 8 commits into
mainfrom
feature/audit-log-doctrine-infrastructure
May 30, 2026
Merged

feat: Doctrine persistence infrastructure (Plan 2)#3
erikfrerejean merged 8 commits into
mainfrom
feature/audit-log-doctrine-infrastructure

Conversation

@erikfrerejean

@erikfrerejean erikfrerejean commented May 30, 2026

Copy link
Copy Markdown
Member

Implements Plan 2 — Doctrine persistence infrastructure (docs/superpowers/plans/2026-05-30-audit-log-doctrine-infrastructure.md).

Adds the Doctrine ORM adapters under Infrastructure/Doctrine/, implementing the core's RecordStore/RecordReader ports and the AuditRecord read contract. They depend on Core + Doctrine only — deptrac-enforced as independent of any (future) Symfony adapter.

What's included

  • DBAL types ChangesetType / RenderPayloadType — round-trip Changeset and RenderPayload as JSON (redaction preserved).
  • AuditRecordEntity — append-only entity (private ctor, single fromNew() factory, no setters, public private(set) props), Types::GUID id, indexes on code/occurredAt/actor/subject.
  • DoctrineRecordStore — persist-only (no flush; commits atomically with the surrounding unit of work).
  • DoctrineRecordReader — filter (code/actor/subject/channel/date), newest-first pagination, distinct actors.
  • Integration tests on an in-memory SQLite EntityManager exercise the full persist→flush→clear→find round-trip and give the read-side DTOs (AuditEntry/AuditActor/AuditQuery/AuditPage) their behavioral coverage.

Deviations from the plan text (all necessary, none change scope)

  1. composer.lock not committed — it's no longer tracked on main (CI uses composer update).
  2. DBAL convertToPHPValue narrowing(string) $value fails PHPStan max (cast.string). Replaced with an is_string() guard that rejects a corrupt non-string column value (legitimate integrity behavior).
  3. Test-infra deps/config (in the entity/store commit): symfony/cache (dev) for ORMSetup's metadata cache, enableNativeLazyObjects(true) for ORM 3 on PHP 8.5, and a 512M phpstan memory limit (the ORM type graph exceeds the 128M default).
  4. Mutation hardening — dropped a redundant max(0, …) offset guard (AuditQuery already rejects page/perPage < 1), added a page-2 pagination test, and assert the reader returns detached DTOs (not the EM-managed entity). One genuinely equivalent mutant remains in ChangesetType (public-prop objects serialize byte-identically to the mapped arrays); MSI is 98.77% (≥ 95%).

QA (all green locally)

cs-check · phpstan · rector-check · deptrac (0 violations) · test (23 tests) · infection (MSI 98.77%)

Note (out of scope, flagging)

ADR 0006-framework-free-core.md still says framework wiring lives in Bridge/{Symfony,Doctrine}; the architecture is now Infrastructure/{Doctrine,Symfony}. Worth a follow-up doc fix.

Adds the append-only Doctrine entity (implements AuditRecord) and the
persist-only store, exercised by an in-memory SQLite round-trip test.

Test infra: symfony/cache (dev) for ORMSetup metadata cache, native lazy
objects enabled for ORM 3 on PHP 8.5, and a 512M phpstan memory limit
since the ORM type graph exceeds the 128M default.
- Cover page>1 offset and assert the reader returns detached DTOs (not the
  EM-managed entity), killing the pagination and array_map mutants.
- Drop the redundant max(0, ...) offset guard: AuditQuery already rejects
  page/perPage < 1, so the offset is never negative.
- Decode JSON via the named flags: argument, removing the json_decode depth
  literal (an equivalent mutant on 512).
Drives actorId, subjectClass, subjectId, channel and a from+to window
through AuditQuery — verifying each is bound to the right column (notably
subjectId vs subject_class), that the enum channel filter matches the
enumType column, and that from/to are inclusive (>=/<=).
A 64-char cap risks truncating long or composite/external actor identifiers;
there is no storage cost to the wider varchar on Postgres.
Changeset values are frozen as JSON, so a non-scalar old/new would silently
reshape on read (objects) or throw mid-flush inside the audited transaction
(resources/closures). Enforce scalar-or-null at FieldChange::of() so curation
discipline fails fast at construction, not at persist time.
@erikfrerejean erikfrerejean merged commit 40d951a into main May 30, 2026
1 check passed
@erikfrerejean erikfrerejean deleted the feature/audit-log-doctrine-infrastructure branch May 30, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant