PHPackages                             anglemx/entity-trail-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. anglemx/entity-trail-bundle

ActiveSymfony-bundle[Logging &amp; Monitoring](/categories/logging)

anglemx/entity-trail-bundle
===========================

Automatic Doctrine entity audit log for Symfony — who changed what and when.

v1.0.2(1mo ago)00MITPHPPHP &gt;=8.1

Since Jun 30Pushed 1mo agoCompare

[ Source](https://github.com/Angle/EntityTrailBundle)[ Packagist](https://packagist.org/packages/anglemx/entity-trail-bundle)[ RSS](/packages/anglemx-entity-trail-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (8)Versions (4)Used By (0)

anglemx/entity-trail-bundle
===========================

[](#anglemxentity-trail-bundle)

Symfony bundle that automatically records **who changed what data and when**, for any Doctrine-managed entity. Drop-in replacement for per-entity changelog tables (`DimensionChangeLog`, `AgreementFeeChangeLog`, …). Provides opt-out attributes, configurable field exclusions, and an optional admin UI.

Every create / update / delete of a tracked entity writes one row to a single `entity_trail_logs` table with a JSON diff (`{"field": {"old": x, "new": y}}`), the acting user, their IP, and a timestamp.

Requirements
------------

[](#requirements)

PHP`>= 8.1`Symfony`^5.4 | ^6.4 | ^7.0`Doctrine ORM`^2.11 | ^3.0`> **Why not ORM 2.7?** The `EntityTrailLog` entity uses PHP-attribute mapping (`#[ORM\Entity]`), which Doctrine only supports from **ORM 2.9**; and `doctrine-bundle`(required by this bundle) couples the ORM version — `doctrine-bundle ^2.10` resolves to **ORM ≥ 2.11**. So the practical, tested floor is ORM **2.11**. The listener is written against the base `LifecycleEventArgs` (not the per-event argument classes introduced in ORM 2.14), so it runs unchanged across ORM 2.11 → 3.x.

`symfony/security-bundle` and `symfony/twig-bundle` are required because the default user provider resolves the acting user from the security token storage, and the admin UI renders Twig. If you override `user_provider` **and** set `enable_admin: false`, neither is used at runtime — but they remain Composer dependencies.

How it works
------------

[](#how-it-works)

`EntityTrailListener` subscribes to Doctrine lifecycle events. It captures the changeset during `preUpdate` / `postPersist` / `preRemove`, queues an audit row in memory, and writes the queued rows in `postFlush` with a **raw DBAL insert** — no ORM involvement in the write, so there's no recursive flush cycle and no risk of the audit write joining the business transaction. An `onFlush` reset guarantees a rolled-back transaction can't leak audit rows into the next successful flush.

The `entity_trail_logs` table stores `entity_type` / `entity_id` as plain columns — there are no ORM relations to your entities, so the bundle stays decoupled from every project.

Installation
------------

[](#installation)

```
composer require anglemx/entity-trail-bundle
```

### 1. Register the bundle

[](#1-register-the-bundle)

```
// config/bundles.php
return [
    // ...
    Angle\EntityTrailBundle\AngleEntityTrailBundle::class => ['all' => true],
];
```

### 2. Configure it

[](#2-configure-it)

```
# config/packages/angle_entity_trail.yaml
angle_entity_trail:
    track_creates: true
    track_updates: true
    track_deletes: true
    exclude_fields: [updatedAt, createdAt, code]   # excluded from ALL entities
    exclude_entities:
        - App\Entity\JobSchedulerLog
        - App\Entity\NotificationEmailLog
    enable_admin: true
    admin_route_prefix: /admin/trail-log
    user_provider: null   # null = default SecurityEntityTrailUserProvider; or a service id
    created_at_format:
        timezone: null     # null = render the stored value as-is; or a timezone name (e.g. America/Mexico_City)
        format: 'Y-m-d H:i'   # DateTime::format() string
```

All keys are optional; the defaults above (minus the example exclusions) are applied automatically. Note Symfony **replaces** array defaults — if you set `exclude_fields`, your list fully overrides the built-in `[updatedAt, createdAt, deletedAt]`.

### 3. Import the admin routes (only if `enable_admin: true`)

[](#3-import-the-admin-routes-only-if-enable_admin-true)

```
# config/routes/audit.yaml
_angle_entity_trail:
    resource: '@AngleEntityTrailBundle/config/routes.yaml'
    prefix: /admin/trail-log
```

### 4. Create the `entity_trail_logs` table

[](#4-create-the-entity_trail_logs-table)

The bundle ships a migration, but a project's `doctrine_migrations.migrations_paths`does not scan `vendor/` by default. Pick one:

**Option A — point Doctrine at the bundle's migration:**

```
# config/packages/doctrine_migrations.yaml
doctrine_migrations:
    migrations_paths:
        'DoctrineMigrations': '%kernel.project_dir%/migrations'
        'Angle\EntityTrailBundle\Migrations': '%kernel.project_dir%/vendor/anglemx/entity-trail-bundle/migrations'
```

```
bin/console doctrine:migrations:migrate
```

**Option B — generate it yourself.** The `EntityTrailLog` entity is auto-mapped by the bundle, so Doctrine already knows the schema:

```
bin/console make:migration   # generates the CREATE TABLE entity_trail_logs migration
bin/console doctrine:migrations:migrate
```

That's the entire install. No entity changes, no listener to register — everything is automatic from here.

Opting out
----------

[](#opting-out)

### Skip an entire entity

[](#skip-an-entire-entity)

```
use Angle\EntityTrailBundle\Attribute\EntityTrailExclude;

#[EntityTrailExclude]
class SessionLog { /* ... */ }
```

### Skip sensitive / high-churn fields

[](#skip-sensitive--high-churn-fields)

```
use Angle\EntityTrailBundle\Attribute\EntityTrailIgnore;

class User
{
    #[EntityTrailIgnore]
    private string $passwordHash;

    #[EntityTrailIgnore]
    private ?\DateTimeImmutable $lastLoginAt;   // changes on every login

    #[EntityTrailIgnore]
    private ?string $rememberMeToken;
}
```

### Skip vendor entities you can't annotate

[](#skip-vendor-entities-you-cant-annotate)

```
angle_entity_trail:
    exclude_entities:
        - Angle\JobSchedulerBundle\Entity\JobSchedulerLog
```

Querying the trail
------------------

[](#querying-the-trail)

Inject `Angle\EntityTrailBundle\Repository\EntityTrailLogRepository`:

```
$repo->findByEntity(Client::class, $clientId);        // full history, newest first
$repo->findLastUpdate(Client::class, $clientId);      // "last edited by" UI
$repo->countByUser($userId, $from, $to);              // activity in a date range
```

Custom user provider
--------------------

[](#custom-user-provider)

The default `SecurityEntityTrailUserProvider` reads the logged-in user and request IP, and returns `null` for everything in CLI/worker contexts. To customise (e.g. a worker that acts on behalf of a user), implement the contract and point the config at your service:

```
use Angle\EntityTrailBundle\Contract\EntityTrailUserProviderInterface;

final class MyEntityTrailUserProvider implements EntityTrailUserProviderInterface
{
    public function getCurrentUserId(): ?int { /* ... */ }
    public function getCurrentUserLabel(): ?string { /* ... */ }
    public function getCurrentIpAddress(): ?string { /* ... */ }
}
```

```
angle_entity_trail:
    user_provider: App\Audit\MyEntityTrailUserProvider
```

Admin UI
--------

[](#admin-ui)

When `enable_admin: true`, three routes are registered under your prefix:

RoutePathPurpose`angle_entity_trail_list``GET {prefix}/`DataTables list with entity-type and action filters`angle_entity_trail_data``POST {prefix}/data`DataTables server-side data endpoint`angle_entity_trail_view``GET {prefix}/{code}`Full changeset for one entry> ⚠️ **Protect these routes.** The bundle does **not** add access control — the trail contains who-changed-what, including user labels and old/new field values that may be sensitive. Restrict the prefix to an admin role in your firewall, e.g.:
>
> ```
> # config/packages/security.yaml
> access_control:
>     - { path: ^/admin/trail-log, roles: ROLE_ADMIN }
> ```
>
>
>
> Also exclude sensitive columns from auditing with `#[EntityTrailIgnore]` / `exclude_fields`.

Templates extend `@AngleEntityTrail/base.html.twig` (a minimal Bootstrap 5 + DataTables base). Override it in your project at `templates/bundles/AngleEntityTrailBundle/base.html.twig` to drop the list/view pages into your own admin layout.

Testing
-------

[](#testing)

```
composer install
vendor/bin/phpunit
```

The suite ships unit tests (configuration, entity, attributes, user provider, DataTables controller) and a functional test that boots a real Symfony kernel with an in-memory SQLite database and asserts that create/update/delete actually write `entity_trail_logs` rows, that `#[EntityTrailExclude]` / `#[EntityTrailIgnore]` / `exclude_fields` are honoured, and that the acting user is recorded.

Non-goals
---------

[](#non-goals)

- Does **not** replace git history or schema migrations (those track code, not data).
- Does **not** audit raw SQL run outside Doctrine.
- Does **not** provide row-level restore/rollback — it's a read-only audit trail.
- Does **not** send notifications on changes.

License
-------

[](#license)

This bundle is released under the [MIT License](LICENSE).

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 87.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

3

Last Release

46d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/936483?v=4)[Edmundo Fuentes](/maintainers/edmundofuentes)[@edmundofuentes](https://github.com/edmundofuentes)

---

Top Contributors

[![jplarar](https://avatars.githubusercontent.com/u/7461991?v=4)](https://github.com/jplarar "jplarar (7 commits)")[![bernardolv](https://avatars.githubusercontent.com/u/19933581?v=4)](https://github.com/bernardolv "bernardolv (1 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/anglemx-entity-trail-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/anglemx-entity-trail-bundle/health.svg)](https://phpackages.com/packages/anglemx-entity-trail-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M433](/packages/easycorp-easyadmin-bundle)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

12017.1k](/packages/rcsofttech-audit-trail-bundle)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1617.3k16](/packages/2lenet-crudit-bundle)[kimai/kimai

Kimai - Time Tracking

4.8k9.4k1](/packages/kimai-kimai)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M780](/packages/sylius-sylius)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M536](/packages/pimcore-pimcore)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
