Download the PHP package th3mouk/audit-trail-bundle without Composer

On this page you can find all versions of the php package th3mouk/audit-trail-bundle. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package audit-trail-bundle

Audit Trail Bundle

Knowing who changed what, and when — and a trail that is still readable months later.

CI Latest version PHP version

A Doctrine audit trail for Symfony: field-level before/after diffs written in the same transaction as the change they describe, anchored to the aggregate they belong to.

Why

Most applications settle for one of two things.

A last_modified_by column, which answers "who touched this most recently" and nothing else — not what changed, not what it was before, not the four edits that came before this one.

Or a generic trail full of orphaned identifiers: membership 4217 deleted, role_id 7, user_id 512. Six months later the membership is gone, the role has been renamed, the user has been anonymised, and the entry is unreadable. It is evidence that something happened, not a record of what happened.

Two things here are different from that:

The rest follows from the same idea: capture happens in onFlush, while the entity is still hydrated and its associations still resolvable, so a deletion can carry the whole state as it stood. And the audit row joins the caller's transaction, so a rolled-back change takes its audit entry with it.

Quickstart

Enable the bundle:

Mark one entity:

Create the table. The bundle maps its own entity into your default entity manager, so your usual tooling sees it:

That is the whole setup. Zero configuration is required. bin/console config:dump-reference audit_trail documents every option, and docs/configuration.md explains the two that matter in practice: the enabled kill-switch and listener_priority.

Before and after

Revoking a role. Here is what a naive audit trail stores:

Every name in that row is a number pointing at something that may no longer exist. Here is the audit_logs row this bundle writes for the same flush:

Drop the database and this row still reads: role Manager was removed from Jean Dupont in Acme, by Alice Martin.

"operator" is the host application's word, not the bundle's — see Extending it. Updates use {"field": {"before": …, "after": …}}; creations and deletions record flat full state, which is what keeps a deletion legible.

Nowhere in that row is there a class name doing any work. Entries are filed under a short entity_type — declared with #[Auditable(type: 'membership')], or derived from the class name in kebab-case — so renaming or moving a class never orphans its history, and never breaks a saved query or a bookmarked URL. The class is recorded beside the type as information, and is null for the things that have no class at all.

Attributes

Attribute Target What it does
#[Auditable] class Opts the entity in, and names it: #[Auditable(type: 'membership')]. Opting in is inherited, so marking a base class audits its children; a child opts back out with #[Auditable(enabled: false)]. The name is not inherited, and a type claimed twice fails the build.
#[AuditLabel] property or method Designates the entity's human title. Snapshotted onto every entry that names it. Falls back to __toString(), then to nothing.
#[AuditScope] class Anchors entries to their aggregate root: #[AuditScope(root: Post::class, via: 'comment.post')] walks getters, identifiers only, never a query.
#[NotAuditable] property Excludes the property entirely. Not stored, and not row-triggering.
#[AuditMasked] property Records that the property changed, never its value. #[AuditMasked(mask: '[redacted]')] overrides the global sentinel.

Masked and ignored are not two flavours of the same thing, and it is the one distinction worth reading twice. Ignored properties are stripped before the decision to write a row: a flush that only bumps updatedAt or a hit counter records nothing at all. Masked properties still trigger a row: a flush whose only change is password produces one entry, reading {"password": {"before": "********", "after": "********"}}. That is how you keep a high-churn technical column out of the trail without also hiding the fact that a credential was rotated.

Auditing is opt-in on purpose — the audited surface stays a greppable, reviewable fact. docs/attributes.md is the full reference for the attributes and the row-trigger rule; docs/architecture.md walks the capture pipeline, value mapping and the no-query guarantee.

Reading the trail

Because the root is denormalised onto every entry, a per-aggregate history panel is one indexed read — no joins, no fan-out:

forEntity() and forActor() are the two siblings. All three return a QueryBuilder ordered by (occurredAt DESC, id DESC) — the feed's one canonical order, and the key every index ends with. The cursor carries both halves: a UUID v7 is time-ordered, so the identifier alone looks sufficient until something is backfilled, and an imported entry gets a historical timestamp under an identifier minted today. There is deliberately no offset paging and no total count: neither can be served cheaply on a table meant to grow forever. docs/reading-the-trail.md covers the repository in full — cursors, custom criteria, exports — and docs/aggregate-history.md covers #[AuditScope], which is what makes the query above one indexed read.

Changes the ORM never sees

Bulk DQL, raw SQL, a data migration: none of them produce a change set, so nothing can capture them automatically. Say so explicitly instead, and the entry is indistinguishable from a captured one — actor, timestamp and request context are filled in for you:

created(), updated(), deleted() and the lower-level record() all queue an entry; the logger never flushes on its own, so when the transaction closes stays your decision.

Extending it

The bundle ships defaults, not policy. Every opinionated piece is an interface with a replaceable implementation — and, deliberately, no actor taxonomy and no user class. Actor is three free-form strings (id, type, label); there is no ActorType enum, no built-in user/system distinction, and nothing in src/ names a permission or an entity of yours.

The same rule holds inside the bundle: the two seams the Gedmo bridge needs are declared in the core with no mention of Gedmo, which is why the capture pipeline has never heard of it.

Seam Interface How to plug in Default
Who did it Actor\ActorResolverInterface tag audit_trail.actor_resolver (priority) SecurityTokenActorResolver, registered only when a security extension is present, at priority -100
Human title Capture\LabelResolverInterface decorate the service #[AuditLabel], then __toString(), then null
Aggregate root Capture\ScopeResolverInterface decorate the service #[AuditScope], or Scope\AuditScopeProviderInterface on the entity itself
Value rendering Capture\ValueSerializerInterface tag audit_trail.value_serializer (priority) scalar → date → enum → entity reference → Stringable chain
Capture veto Capture\CaptureGateInterface tag audit_trail.capture_gate (priority) kill-switch and cascade suppression; all gates must agree before an entry is written
What an update means Capture\ActionResolverInterface tag audit_trail.action_resolver none in core; the Gedmo bridge reclassifies a logical delete as a delete
Fields to leave out Capture\FieldExclusionInterface tag audit_trail.field_exclusion none in core; the Gedmo bridge drops fields whose change is diverted to a translation
Where rows go Storage\AuditStorageInterface decorate or replace DoctrineAuditStorage, which enlists the entry in the ongoing flush

Each interface has a public alias, so autowiring and #[AsDecorator] both work out of the box.

Actor resolution is the seam most applications touch first. Resolvers are asked in priority order and return null to defer; when nobody answers, the change is still recorded, attributed to nobody:

Resolvers must be stateless — the actor is re-read on every call, so a long-running worker can never attribute one message's changes to another message's principal. Full walkthrough in docs/extending.md.

Bridges

Both are auto-detected from the installed packages and can be forced on or off. The bundle boots with neither installed.

Gedmo (gedmo/doctrine-extensions) — audits translated content, which Translatable moves out of the entity change set before a plain listener can see it, and maps a SoftDeleteable logical delete onto a real delete action instead of an update of a date column. Two honest gaps: a translation inserted for an entity that has no identifier yet goes straight through the DBAL and cannot be captured from onFlush (assign identifiers in the constructor and the case disappears), and a bare locale switch is not recorded. Details in src/Bridge/Gedmo/README.md.

API Platform (api-platform/core) — exposes the trail as a read-only, keyset-paginated feed at /audit-logs, with filters on actor, entity, root, action and date, and an id[lt] UUID cursor. Writes are not declared: the routes are GET-only, so a POST gets a 405. It is off by default, because a package landing in vendor/ must never be what publishes an audit log, and switching it on requires naming who may read it:

Any Symfony voter attribute works, so "write a voter and name it here" is the whole extension story. Declaring no access mode is a build error, not an open feed. Details in docs/bridges/api-platform.md.

Requirements

Optional: api-platform/core ^4.1, gedmo/doctrine-extensions ^3.16, symfony/security-bundle (for the default actor resolver).

Tested against PHP 8.4 and 8.5, Symfony 7.4 and 8.1, lowest and highest dependency resolutions, SQLite and PostgreSQL — and, in a job of its own, with every optional package genuinely uninstalled, because "works without them" is worth proving rather than asserting.

Testing

The suites default to an in-memory SQLite database, so there is nothing to provision. Export DATABASE_URL to run them against PostgreSQL instead. PHPStan runs at level 8 with no baseline.

Documentation

Start at docs/index.md, or jump straight to what you need:

Page Contents
docs/installation.md Install, register, create the table, rename it, jsonb on PostgreSQL
docs/attributes.md The five attributes, the field-policy table, the row-trigger rule
docs/configuration.md Every audit_trail option, and when listener priority matters
docs/aggregate-history.md #[AuditScope], the dotted walk, building a history panel
docs/reading-the-trail.md Reading from PHP: the Doctrine repository, cursors, custom criteria, exports
docs/actor.md Actor resolution, why there is no taxonomy, impersonation, unknown actors
docs/manual-logging.md Recording what the ORM cannot see, and when to refactor instead
docs/extending.md Every seam, with a worked example each
docs/bridges/gedmo.md Translation auditing and soft deletes: what is covered, and what is not
docs/bridges/api-platform.md The read feed: endpoints, filters, cursor pagination, security
docs/architecture.md For contributors: the capture pipeline, the invariants, what is out of scope
docs/faq.md Cost per write, table growth, reads, tamper-evidence, JSON columns

Contributing

Bug reports, ideas and pull requests are welcome. CONTRIBUTING.md covers the gates, where a test belongs, and the two invariants that are not negotiable. AGENTS.md is the orientation guide for anyone — human or agent — about to change something: the repository map, the four rules that settle most design questions, a recipe per kind of change, and the traps that have already cost time here. Security issues: SECURITY.md.

License

MIT.


All versions of audit-trail-bundle with dependencies

PHP Build Version
Package Version
Requires php Version >=8.4
composer-runtime-api Version ^2.2
doctrine/dbal Version ^4.0
doctrine/doctrine-bundle Version ^2.13 || ^3.0
doctrine/orm Version ^3.2
doctrine/persistence Version ^3.1 || ^4.0
psr/log Version ^3.0
symfony/doctrine-bridge Version ^7.4 || ^8.0
symfony/config Version ^7.4 || ^8.0
symfony/dependency-injection Version ^7.4 || ^8.0
symfony/http-kernel Version ^7.4 || ^8.0
symfony/uid Version ^7.4 || ^8.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package th3mouk/audit-trail-bundle contains the following files

Loading the files please wait ...