diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index bc43e4d..e5da762 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -1,12 +1,19 @@ doctrine: dbal: - url: '%env(resolve:DATABASE_URL)%' - - # IMPORTANT: You MUST configure your server version, - # either here or in the DATABASE_URL env var (see .env file) - #server_version: '16' - - profiling_collect_backtrace: '%kernel.debug%' + default_connection: default + connections: + # ORM uses `default`; AuditLogWriter uses `audit` (same DSN, separate + # service) to write outside the ORM transaction so audit rows survive + # an application-side rollback and avoid transactional entanglement. + default: + url: '%env(resolve:DATABASE_URL)%' + profiling_collect_backtrace: '%kernel.debug%' + # audit_log has no ORM entity (written via raw DBAL). Exclude it + # from schema comparison so migrations:diff / schema:validate stay + # clean. Creation/teardown stay driven by migrations. + schema_filter: '~^(?!audit_log$).+~' + audit: + url: '%env(resolve:DATABASE_URL)%' orm: validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware @@ -33,8 +40,13 @@ doctrine: when@test: doctrine: dbal: - # "TEST_TOKEN" is typically set by ParaTest - dbname_suffix: '_test%env(default::TEST_TOKEN)%' + # Propagate the _test suffix to BOTH connections: the audit + # connection must write to the test DB, not the dev DB. + connections: + default: + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + audit: + dbname_suffix: '_test%env(default::TEST_TOKEN)%' when@prod: doctrine: diff --git a/migrations/Version20260619185448.php b/migrations/Version20260619185448.php new file mode 100644 index 0000000..a9c2335 --- /dev/null +++ b/migrations/Version20260619185448.php @@ -0,0 +1,56 @@ +addSql(<<<'SQL' + CREATE TABLE audit_log ( + id uuid NOT NULL, + entity_type VARCHAR(100) NOT NULL, + entity_id VARCHAR(64) NOT NULL, + action VARCHAR(10) NOT NULL, + changes JSONB NOT NULL DEFAULT '{}'::jsonb, + performed_by VARCHAR(100) NOT NULL, + performed_at TIMESTAMP(6) WITH TIME ZONE NOT NULL, + ip_address VARCHAR(45) DEFAULT NULL, + request_id VARCHAR(36) DEFAULT NULL, + PRIMARY KEY(id) + ) + SQL); + $this->addSql('CREATE INDEX idx_audit_entity_time ON audit_log (entity_type, entity_id, performed_at)'); + $this->addSql('CREATE INDEX idx_audit_performer ON audit_log (performed_by, performed_at)'); + $this->addSql('CREATE INDEX idx_audit_time ON audit_log (performed_at)'); + $this->addSql("COMMENT ON COLUMN audit_log.entity_type IS 'Audited entity type, format module.Entity (e.g. core.User)'"); + $this->addSql("COMMENT ON COLUMN audit_log.entity_id IS 'Audited entity identifier (int or composite key serialized)'"); + $this->addSql("COMMENT ON COLUMN audit_log.action IS 'create|update|delete'"); + $this->addSql("COMMENT ON COLUMN audit_log.changes IS 'JSON diff: {field:{old,new}} for update, full snapshot for create/delete'"); + $this->addSql("COMMENT ON COLUMN audit_log.performed_by IS 'User identifier or system'"); + $this->addSql("COMMENT ON COLUMN audit_log.request_id IS 'UUID shared by all audit rows of a single HTTP request (null in CLI)'"); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE audit_log'); + } +} diff --git a/src/Shared/Domain/Attribute/AuditIgnore.php b/src/Shared/Domain/Attribute/AuditIgnore.php new file mode 100644 index 0000000..8f95350 --- /dev/null +++ b/src/Shared/Domain/Attribute/AuditIgnore.php @@ -0,0 +1,17 @@ +