EntityAuditBundle

repository·1.x·Indexed 20 days ago

https://github.com/sonata-project/entityauditbundle

A Doctrine 2 extension inspired by Hibernate Envers that provides full versioning for entities and their associations. It creates mirroring audit tables (suffixed with _audit) and a central revisions table to track changes, including timestamps and usernames. The bundle supports Symfony integration via a dedicated bundle class and routing, but can also be used in standalone applications by manually configuring AuditConfiguration and AuditManager. It includes an AuditReader for retrieving historical entity states and revision history.

Tokens
6.4K
Snippets
22
Records
28
Agent score
69%

What's inside EntityAuditBundle

  1. How EntityAuditBundle works

    1.x

    The bundle implements entity versioning by creating a mirroring table for every audited entity, suffixed with _audit.

    Each audit table contains all columns from the original entity plus two metadata fields:

    • rev: A global revision number linked to a central revisions table.
    • revtype: The type of operation that caused the entry: 'INS' (Insert), 'UPD' (Update), or 'DEL' (Delete).

    The central revisions table tracks the id, timestamp, username, and a change comment for every global revision.

    The bundle hooks into the Doctrine SchemaTool process, so audit tables are automatically generated during schema updates.

  2. Install EntityAuditBundle in a standalone application

    1.x

    If you are not using Symfony, you must manually configure the AuditConfiguration, AuditManager, and register the event listeners with the Doctrine EventManager.

    use Doctrine\ORM\Configuration;
    use Doctrine\ORM\EntityManager;
    use Doctrine\Common\EventManager;
    use SimpleThings\EntityAudit\AuditConfiguration;
    use SimpleThings\EntityAudit\AuditManager;
    
    $auditConfig = new AuditConfiguration();
    $auditConfig->setAuditedEntityClasses([ArticleAudit::class, UserAudit::class]);
    $auditConfig->setGlobalIgnoreColumns(['created_at', 'updated_at']);
    
    $eventManager = new EventManager();
    $auditManager = new AuditManager($auditConfig);
    $auditManager->registerEvents($eventManager);
    
    $config = new Configuration();
    // ... configure $config and $connection ...
    $entityManager = EntityManager::create($connection, $config, $eventManager);
    use Doctrine\ORM\Configuration;
    use Doctrine\ORM\EntityManager;
    use Doctrine\Common\EventManager;
    use SimpleThings\EntityAudit\AuditConfiguration;
    use SimpleThings\EntityAudit\AuditManager;
    use SimpleThings\EntityAudit\Tests\ArticleAudit;
    use SimpleThings\EntityAudit\Tests\UserAudit;
    
    $auditConfig = new AuditConfiguration();
    $auditConfig->setAuditedEntityClasses([ArticleAudit::class, UserAudit::class]);
    $auditConfig->setGlobalIgnoreColumns(['created_at', 'updated_at']);
    
    $eventManager = new EventManager();
    $auditManager = new AuditManager($auditConfig);
    $auditManager->registerEvents($eventManager);
    
    $config = new Configuration();
    // $config ...
    $connection = [];
    $entityManager = EntityManager::create($connection, $config, $eventManager);
  3. Migration: Changes to CreateSchemaListener in 1.x

    1.x

    When upgrading within the 1.x version branch, note the following changes to how database schemas are managed by SimpleThings\EntityAudit\EventListener\CreateSchemaListener:

    1. Event Change: The bundle no longer listens to the postGenerateSchema event. Instead, the table responsible for storing the revisions index is now created during the postGenerateSchemaTable event.
    2. Foreign Key Constraints: A new foreign key constraint has been added between the revisions index and the audit tables. This constraint prevents the deletion of records in the index if their referenced values still exist in the audit tables.
  4. View auditing data via Symfony routes

    1.x

    The bundle provides a default Symfony controller for viewing audit data. To use it, import the provided routing configuration into your config/routes.yaml and set a prefix (ensure you secure this prefix).

    # config/routes.yaml
    
    simple_things_entity_audit:
        resource: "@SimpleThingsEntityAuditBundle/Resources/config/routing/audit.xml"
        prefix: /audit

    Available Routes

    • simple_things_entity_audit_home: Paginated list of revisions (timestamps and users).
    • simple_things_entity_audit_viewrevision: List of classes modified in a specific revision.
    • simple_things_entity_audit_viewentity: List of revisions where a specific entity was modified.
    • simple_things_entity_audit_viewentity_detail: Data for a specific entity at a specific revision.
    • simple_things_entity_audit_compare: Comparison of an entity between two revisions.
  5. Customizing the username resolution

    1.x

    The bundle automatically saves the username associated with a revision. In Symfony, it uses the security context. To use custom logic, you can provide a username_callable.

    In Symfony

    Configure the service ID of your callable in entity_audit.yaml:

    simple_things_entity_audit:
        service:
            username_callable: acme.username_callable

    Your service must be a callable that returns a string or null.

    In Standalone Applications

    Use the setUsernameCallable method on the AuditConfiguration object:

    $auditConfig = new \SimpleThings\EntityAudit\AuditConfiguration();
    $auditConfig->setUsernameCallable(function () {
        return 'custom_user';
    });
    $auditConfig = new \SimpleThings\EntityAudit\AuditConfiguration();
    $auditConfig->setUsernameCallable(function () {
        // your custom logic
        return $username;
    });
  6. Install and enable EntityAuditBundle

    1.x

    To use the bundle in a Symfony project, install it via Composer and then register it in your kernel configuration.

    1. Install via Composer

    composer require sonata-project/entity-audit-bundle

    2. Enable the bundle

    Add the bundle class to your config/bundles.php file:

    // config/bundles.php
    
    return [
        //...
        SimpleThings\EntityAudit\SimpleThingsEntityAuditBundle::class => ['all' => true],
        //...
    ];
    $ composer require sonata-project/entity-audit-bundle
  7. Configure EntityAuditBundle

    1.x

    Configure the bundle by loading the simple_things_entity_audit extension. You must specify which entities should be audited.

    Basic Configuration

    Specify the list of audited entity classes under audited_entities:

    # config/packages/entity_audit.yaml
    
    simple_things_entity_audit:
        audited_entities:
            - MyBundle\Entity\MyEntity
            - MyBundle\Entity\MyEntity2

    Advanced Configuration Options

    • global_ignore_columns: List of entity properties that should not trigger a revision when changed (e.g., timestamps).
    • connection: Specify a custom Doctrine connection name if not using default.
    • entity_manager: Specify a custom Doctrine entity manager name if not using default.
    • disable_foreign_keys: Set to true to discard inferred foreign keys from audited entities.
    • service.username_callable: Define a service ID for a custom callable that resolves the current username.
    # config/packages/entity_audit.yaml
    
    simple_things_entity_audit:
        audited_entities:
            - MyBundle\Entity\MyEntity
        global_ignore_columns:
            - created_at
            - updated_at
        connection: custom
        entity_manager: custom
        disable_foreign_keys: true
  8. AuditedCollection is a read-only collection for historical entity associations

    1.x

    AuditedCollection is a specialized implementation of the Doctrine Collection interface used to represent a collection of entities as they existed at a specific point in time (a specific revision).

    Key Characteristics

    • Read-Only: You cannot add, remove, or modify elements in an AuditedCollection. Attempting to use add(), remove(), removeElement(), set(), or offsetSet() will throw an AuditedCollectionException.
    • Lazy Loading: The collection initially contains only metadata (identifiers and revision information). Actual entity objects are loaded from the audit tables only when you attempt to access them (e.g., via get(), current(), toArray(), or iterating over the collection).
    • Historical Accuracy: It uses the AuditReader to resolve identifiers into the actual state of the entity at the requested revision, ensuring that the collection reflects the database state at that specific time.

    Common Operations

    Since it implements Collection, you can use standard methods to inspect the historical data:

    • count(): Returns the number of entities in the collection.
    • toArray(): Returns an array of the loaded entity objects.
    • filter(\Closure $p): Returns a new collection containing elements that satisfy the predicate.
    • first() / last(): Retrieves the first or last entity in the collection.
    • get($key): Retrieves a specific entity by its key (if indexBy was configured).
  9. Query auditing information with AuditReader

    1.x

    Use the SimpleThings\EntityAudit\AuditReader to retrieve historical data. In Symfony, this is available via dependency injection. In standalone apps, create it from the AuditManager.

    Find entity state at a specific revision

    Returns the state of an entity at a specific revision number. Note: Instances returned by find() are not managed by the EntityManager's UnitOfWork; you must merge() them if you intend to continue working with them in the current persistence context.

    $articleAudit = $auditReader->find(
        SimpleThings\EntityAudit\Tests\ArticleAudit::class,
        $id = 1,
        $rev = 10
    );

    Find revision history of an entity

    Returns a list of all revisions associated with a specific entity ID.

    $revisions = $auditReader->findRevisions(
        SimpleThings\EntityAudit\Tests\ArticleAudit::class,
        $id = 1
    );

    Each Revision object provides:

    • getRev(): The revision ID.
    • getTimestamp(): The time of change.
    • getUsername(): The user who made the change.

    Find entities changed at a specific revision

    Returns a list of all ChangedEntity objects modified during a specific global revision.

    $changedEntities = $auditReader->findEntitiesChangedAtRevision(10);

    Each ChangedEntity provides:

    • getClassName()
    • getId()
    • getRevisionType()
    • getEntity()

    Find current revision of an entity

    $revision = $auditReader->getCurrentRevision(
        'SimpleThings\EntityAudit\Tests\ArticleAudit',
        $id = 3
    );
    use SimpleThings
    EntityAudit
    AuditReader;
    
    // In a Symfony Controller
    public function indexAction(AuditReader $auditReader)
    {
        // Find entity state at revision 10
        $articleAudit = $auditReader->find(
            SimpleThings\EntityAudit\Tests\ArticleAudit::class,
            1,
            10
        );
    
        // Find revisions for an entity
        $revisions = $auditReader->findRevisions(
            SimpleThings\EntityAudit\Tests\ArticleAudit::class,
            1
        );
    }
  10. Set the current username provider

    1.x

    The bundle needs to know who is performing an action to record the username in the audit log. Instead of a static string, you should provide a callable that returns the current username (e.g., by accessing the Security component or the current Token in Symfony).

    Note: setCurrentUsername() is deprecated. Use setUsernameCallable() instead.

    // Example using a closure to fetch the username from a security context
    $configuration->setUsernameCallable(function () use ($security) {
        $user = $security->getUser();
        return $user ? $user->getUserIdentifier() : 'system';
    });
  11. Retrieve entity history with AuditReader::getEntityHistory()

    1.x

    Use AuditReader::getEntityHistory() to get a list of all historical versions of a specific entity, ordered from newest to oldest.

    This method returns an array of entity objects representing the state of the entity at each recorded revision.

    /** @var User[] $history */
    $history = $auditReader->getEntityHistory(User::class, $userId);