Skip to content

feat!: add opt-in strict transactional audit mode - #7

Merged
Zhortein merged 27 commits into
developfrom
feature/transactional-audit-mode
Aug 4, 2026
Merged

Zhortein merged 27 commits into
developfrom
feature/transactional-audit-mode

Conversation

@Zhortein

@Zhortein Zhortein commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Add an opt-in strict transactional audit mode while preserving the existing
legacy audit behavior by default.

The application owns its transactional audit entity, entry factory, storage,
PSR-20 clock and transaction boundary. The bundle supplies deterministic
Doctrine identifier extraction, Symfony Security actor resolution, strict audit
orchestration and conditional Symfony container wiring.

Why

The legacy listener remains useful for automatic fail-open audit histories, but
its fail-open behavior and persister-owned flush cannot guarantee that business
and audit writes share the same commit or rollback.

The new recorder supports application flows where an audit failure must prevent
the business commit without imposing an audit schema or persistence model.

Breaking changes and platform baseline

  • PHP 8.3 or later is required.
  • Symfony 7.4 or 8.x is supported.
  • Doctrine ORM 3.x is required; Doctrine ORM 2 is no longer supported.
  • DoctrineBundle 2.19 or 3.x is required.
  • The development branch alias now targets the 2.x series.

Dropping Doctrine ORM 2 and raising the DoctrineBundle minimum are the reasons
this work targets the future 2.x series.

Legacy compatibility

The default configuration remains:

  • enabled: true
  • legacy_mapping.enabled: true
  • transactional.enabled: false

The historical AuditEntry mapping, audit_entry schema, integer identifier,
columns, indexes, automatic listener, service aliases, sync/async writers and
fail-open behavior remain unchanged by default.

No SQL migration is mandatory merely to retain the default legacy mode.

Transactional architecture

The strict recorder is enabled explicitly with:

zhortein_auditable:
  transactional:
    enabled: true

Applications must provide:

  • AuditEntryFactoryInterface
  • AuditStorageInterface
  • Psr\Clock\ClockInterface

The bundle does not impose a factory, storage, audit entity, table or migration.
It provides default, replaceable aliases for:

  • IdentifierExtractorInterface
  • AuditActorResolverInterface

The recorder:

  • resolves explicit or strategy-provided subjects, actors and timestamps;
  • delegates entry creation and storage;
  • performs no flush, commit or rollback;
  • catches no factory or storage exception;
  • provides no transactional best_effort mode.

Identifier and actor resolution

The default Doctrine extractor supports:

  • integers;
  • strings;
  • backed enums;
  • stringable identifiers such as UUID objects;
  • deterministic primitive composite identifiers.

The stable composite format is doctrine-composite-v1.

Identifier associations are not handled by the default extractor. Applications
can replace the extractor or provide an explicit AuditSubject.

The default Symfony Security resolver uses getUserIdentifier() and immediate
SwitchUserToken impersonation context. System and technical actors should be
provided explicitly.

PostgreSQL guarantees

A dedicated PostgreSQL 16 suite proves that an application-owned business
mutation and audit entry can:

  • remain absent before the application flush;
  • remain invisible outside the transaction after flush;
  • become visible together after commit;
  • be rolled back together;
  • roll back an already-flushed business mutation when strict audit storage
    fails before commit.

These guarantees require the same EntityManager and underlying connection. The
application storage only calls persist() and never flushes.

Transactional-only applications

Applications no longer using the legacy path can explicitly configure:

zhortein_auditable:
  enabled: false

  legacy_mapping:
    enabled: false

  transactional:
    enabled: true

Disabling the mapping:

  • never drops the existing audit_entry table;
  • provides no migration;
  • requires legacy audit production and workers to be stopped first;
  • requires pending PersistAuditEntryMessage messages to be processed or
    drained;
  • leaves data retention, archival or migration to the application.

Security and privacy

Applications remain responsible for:

  • data minimization;
  • authorization;
  • retention;
  • encryption;
  • backup;
  • compliance;
  • access to audit data.

The bundle does not automatically provide cryptographic signatures, hash
chaining, append-only storage, tamper evidence, archival, purge, anonymization
or legal compliance.

getUserIdentifier() may itself be personal data. Applications needing an
opaque actor identifier can replace the actor resolver or provide an explicit
AuditActor.

Supported matrix

The CI executes:

  • PHP 8.3, 8.4 and 8.5;
  • Symfony 7.4, 8.0 and 8.1;
  • DoctrineBundle 2.19, 2.x and 3.x;
  • Doctrine ORM 3;
  • DBAL 3 and 4;
  • native and polyfilled mbstring boundaries;
  • PostgreSQL 16 transactional proofs.

PHPStan 2 runs at maximum level across PHP 8.3 through 8.5 without a baseline or
global ignored errors.

Test results

General runtime matrix, six lines:

  • 222 tests
  • 2,220 assertions
  • 0 failures, errors, skips or risky tests

PostgreSQL matrix, two lines:

  • 4 tests
  • 78 assertions
  • 0 failures, errors, skips or risky tests

Additional gates:

  • PHPStan 2 at maximum level;
  • PHP-CS-Fixer;
  • Composer Audit;
  • Actionlint;
  • self-contained Composer package archive;
  • no-dev package installation and runtime autoload verification.

Migration and operating documentation

The branch includes:

  • UPGRADE-2.0.md
  • docs/legacy-mode.md
  • docs/transactional-doctrine.md
  • docs/security-privacy.md
  • docs/compatibility.md

Both the 1.0.0 and future 2.0.0 public APIs are protected by deterministic
contract snapshots.

Release workflow

This draft pull request targets develop, the integration branch.

It must not be merged into main directly.

After review and green CI on develop, the release process will use a separate
release/2.0.0 branch created from develop. That release branch will:

  • finalize the 2.0.0 changelog version and release date;
  • receive a final release-candidate validation;
  • open a separate pull request toward main.

Tagging 2.0.0, creating a GitHub Release and publishing to Packagist remain
separate operations requiring explicit maintainer authorization.

After the release, main must be synchronized back into develop.

Out of scope

  • no default transactional audit entity;
  • no default entry factory or storage;
  • no hidden flush or transaction control;
  • no transactional best_effort mode;
  • no automatic legacy table removal;
  • no production migration;
  • no automatic Doctrine ORM downgrade path;
  • no tag, GitHub release or Packagist publication in this PR.

Release status

This is a draft integration branch for the future 2.x series.

It has not been tagged, released or published.

Zhortein added 27 commits August 3, 2026 14:34
Lock the historical public API, Symfony configuration, Doctrine mapping, persistence pipeline, listener behavior, and known legacy semantics before introducing opt-in transactional auditing.
Canonicalize equivalent reflection types across PHP versions and adapt Doctrine test fixtures for DoctrineBundle 2/3 and PHP 8.4 native lazy objects without changing the 1.0.0 API snapshot.
Exercise PHP 8.3 through 8.5, Symfony 7.4 and 8.x, DoctrineBundle 2/3 and Doctrine ORM 3 across lowest and latest dependencies, with separate static analysis, coding style, platform checks, and security audit gates.
Raise the DoctrineBundle minimum to 2.19, drop Doctrine ORM 2 support, declare symfony/polyfill-mbstring as a direct runtime dependency, and prepare the main branch for the 2.x series.

BREAKING CHANGE: Doctrine ORM 2 is no longer supported. DoctrineBundle 2.19 or 3.x and Doctrine ORM 3.x are now required.
Exercise UTF-8 truncation with and without the native mbstring extension, assert the direct polyfill dependency and platform state, and keep the supported compatibility matrix green across PHP 8.3 through 8.5.
Document PHP 8.3+, Symfony 7.4 and 8.x, Doctrine ORM 3, DoctrineBundle 2.19/3.x, and the native-or-polyfilled mbstring runtime policy.
Define strict fail-closed extension contracts and readonly subject, actor, event, and record models while keeping the legacy runtime, Doctrine mapping, service aliases, and database schema unchanged.
Resolve mapped entity types and scalar, BackedEnum, Stringable, and primitive composite identifiers through Doctrine metadata, with a private default extractor alias and explicit failures for unavailable or unsupported identifiers.

The extractor performs no persistence, flush, transaction management, repository lookup, or explicit database query.
Resolve authenticated Symfony users and immediate SwitchUserToken impersonation through TokenStorageInterface, with a private default resolver alias and no implicit role, token metadata, HTTP context, or Doctrine access.

Invalid users and identifiers fail explicitly with ActorResolutionException while preserving the original exception and excluding raw identifiers from error messages.
Resolve explicit or strategy-provided subjects, actors, and timestamps into an AuditRecord before delegating entry creation and storage, with deterministic ordering and exact exception propagation.

The recorder remains manually composable and performs no flush, transaction control, logging, Doctrine access, Messenger dispatch, or implicit Symfony service wiring.
Register the strict transactional recorder and its private interface alias only when transactional.enabled is explicitly enabled, while requiring the application container to supply the entry factory, storage, and PSR-20 clock.

The default-disabled path preserves the legacy container and schema, and no factory, storage, clock, flush, transaction control, or legacy fallback is provided by the bundle.
Add an application-owned UUID v7 business entity, audit entity, entry factory, no-flush Doctrine storage, and dedicated PostgreSQL tests proving shared commit, shared rollback, and fail-closed rollback after a flushed business mutation.

The executable example remains isolated to test fixtures and does not add a production entity, mapping, migration, storage, or PostgreSQL runtime requirement to the bundle.
Execute the dedicated PostgreSQL 16 atomicity suite on the minimum PHP 8.3 and latest PHP 8.5 supported stacks, covering Doctrine DBAL 3 and 4 without duplicating the general compatibility and QA gates.
Document application-owned audit entities, no-flush Doctrine storage, shared EntityManager and connection requirements, explicit transaction boundaries, and the executable PostgreSQL commit, rollback, and fail-closed proof.
Add a guarded legacy_mapping.enabled option that retains the AuditEntry mapping by default and conditionally omits it only after legacy auditing has been disabled.

The opt-out changes Doctrine metadata registration only: it performs no table deletion, SQL, migration, queue draining, service removal, or automatic storage replacement.
Document coexistence and transactional-only configurations, clarify that disabling the mapping never removes an existing table, and require legacy Messenger queues and workers to be drained before opting out.
Add a deterministic full public API baseline for the 2.0 series while preserving the immutable 1.0 contract, canonicalizing equivalent reflection types across PHP 8.3 through 8.5, and allowing future additive public types.

The contract rejects removals and changes to signatures, interfaces, enums, public properties, constructors, finality, and readonly semantics.
Upgrade static analysis to PHPStan 2 across the supported PHP 8.3 through 8.5 platform and tighten configuration, Doctrine, Reflection, generic and fixture types without changing runtime behavior or public native signatures.

No baseline, global ignoreErrors, bleeding edge ruleset, functional cast, configuration change, mapping change, or exception handling was introduced.
Require PHPStan 2 in the static-analysis gates and validate the Composer archive as a self-contained runtime package with bundled versioned documentation, relative-link integrity, development-file exclusions, no-dev installation, platform checks, and runtime autoload verification.
Document the 1.0-to-2.0 upgrade path, preserved legacy semantics, strict transactional Doctrine integration, security and privacy responsibilities, and the supported compatibility and deprecation policy.

The guides distinguish fail-open legacy behavior from the opt-in fail-closed recorder and do not mark 2.0 as released.
Rewrite the project overview, configuration comments, and unreleased changelog around the future 2.x platform, default-preserved legacy mode, opt-in strict recorder, migration paths, and explicit security and durability non-guarantees.

The documentation does not mark 2.0 as published and preserves the historical 1.0 changelog entry.
Require the upgrade, legacy, transactional, security, privacy and compatibility guides in the Composer archive and validate every distributed Markdown document for safe, resolvable relative links.
Validate all latest-compatible Symfony 8 matrix entries against the supported major instead of pinning a stale minor, while preserving the explicit Symfony 8.0 compatibility boundary.
Run CI once for pull requests targeting develop or main, and on pushes reaching the integration, stable or tag refs, so feature, release and Dependabot branches no longer execute duplicate push and pull-request matrices.
Alias the develop branch to the future 2.0 development line and prepare monthly Composer and GitHub Actions version updates targeting develop, with compatible minor and patch updates grouped.
Document feature integration through develop, release branches toward main, explicit tag and publication authorization, security-fix synchronization, and main-to-develop reconciliation after every release.
Adopt the reviewed checkout v7.0.1 Node 24 release and pin every checkout and setup-php invocation to its verified full commit SHA while retaining version comments for Dependabot updates.
@Zhortein
Zhortein marked this pull request as ready for review August 4, 2026 06:19
@Zhortein
Zhortein merged commit 167f188 into develop Aug 4, 2026
26 checks passed
@Zhortein
Zhortein deleted the feature/transactional-audit-mode branch August 4, 2026 06:19
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