benmacha/audit-bundle
Symfony bundle to audit Doctrine entity changes with rollback support. Includes a web UI and REST API to browse audit logs, flexible configuration, security integration, and optional async processing. Supports PHP 7.4–8.4 and Symfony 5.4–7.x.
A comprehensive Symfony bundle for auditing entity changes with rollback functionality, web interface, and REST API. Now with Symfony Flex auto-configuration support!
composer require benmacha/audit-bundle
Symfony Flex will automatically:
config/bundles.phpconfig/packages/audit.yamlconfig/routes/audit.yamlIf you're not using Symfony Flex:
composer require benmacha/audit-bundle
Then manually register the bundle in config/bundles.php:
return [
// ...
BenMacha\AuditBundle\AuditBundle::class => ['all' => true],
];
composer require benmacha/audit-bundle
Add the bundle to your config/bundles.php:
<?php
return [
// ...
BenMacha\AuditBundle\AuditBundle::class => ['all' => true],
];
With Symfony Flex, the configuration file config/packages/audit.yaml is automatically created with sensible defaults:
audit:
# Enable/disable the audit system
enabled: '%env(bool:AUDIT_ENABLED)%'
# Storage configuration
storage:
driver: '%env(AUDIT_STORAGE_DRIVER)%'
# Logging configuration
logging:
level: '%env(AUDIT_LOG_LEVEL)%'
# Default audit configuration
entities:
# Auto-discover entities with Auditable attribute
auto_discover: true
# Default settings for audited entities
defaults:
track_created_at: true
track_updated_at: true
track_deleted_at: true
store_old_values: true
store_new_values: true
# Web interface configuration
web_interface:
enabled: true
route_prefix: '/audit'
# API configuration
api:
enabled: true
route_prefix: '/api/audit'
# Security configuration
security:
# Roles required to access audit features
view_role: 'ROLE_ADMIN'
manage_role: 'ROLE_SUPER_ADMIN'
rollback_role: 'ROLE_SUPER_ADMIN'
# Performance settings
performance:
batch_size: 100
cache_enabled: true
cache_ttl: 3600
Add these environment variables to your .env file:
# Audit Bundle Configuration
AUDIT_ENABLED=true
AUDIT_STORAGE_DRIVER=doctrine
AUDIT_LOG_LEVEL=info
Create config/packages/audit.yaml:
audit:
enabled: true
retention_days: 365
async_processing: false
# Database configuration
database:
connection: default
# Entity-specific configuration
entities:
App\Entity\User:
enabled: true
operations: ['create', 'update', 'delete']
ignored_fields: ['password', 'lastLogin']
App\Entity\Product:
enabled: true
operations: ['create', 'update']
# API configuration
api:
enabled: true
rate_limit: 100
prefix: '/api/audit'
# Security roles
security:
admin_role: 'ROLE_ADMIN'
auditor_role: 'ROLE_AUDITOR'
developer_role: 'ROLE_DEVELOPER'
# UI configuration
ui:
route_prefix: '/admin/audit'
items_per_page: 25
show_ip_address: true
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
Add to config/routes.yaml:
audit_bundle:
resource: '@AuditBundle/Resources/config/routes.yaml'
prefix: /admin
audit_api:
resource: '@AuditBundle/Resources/config/api_routes.yaml'
prefix: /api
Simply add the #[Auditable] attribute to your entity:
<?php
namespace App\Entity;
use BenMacha\AuditBundle\Attribute\Auditable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[Auditable]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string')]
private string $username;
#[ORM\Column(type: 'string')]
private string $email;
// ... getters and setters
}
Access the web interface at /admin/audit or use the API:
# Get all audit logs
curl -X GET /api/audit/logs
# Get logs for specific entity
curl -X GET /api/audit/logs/entity/User/123
# Rollback entity to previous state
curl -X POST /api/audit/rollback/456
#[Auditable]Marks an entity as auditable.
use BenMacha\AuditBundle\Attribute\Auditable;
#[Auditable(
operations: ['create', 'update', 'delete'], // Operations to track
ignoredFields: ['password', 'updatedAt'], // Fields to ignore
async: true // Process asynchronously
)]
class User
{
// ...
}
Parameters:
operations: Array of operations to track (create, update, delete)ignoredFields: Array of field names to exclude from auditingasync: Whether to process audit logs asynchronously#[IgnoreAudit]Excludes specific fields or operations from auditing.
use BenMacha\AuditBundle\Attribute\IgnoreAudit;
class User
{
#[IgnoreAudit] // Never audit this field
private string $password;
#[IgnoreAudit(operations: ['update'])] // Don't audit on updates
private \DateTime $createdAt;
}
Parameters:
operations: Array of operations to ignore for this field#[AuditSensitive]Marks fields as sensitive for special handling.
use BenMacha\AuditBundle\Attribute\AuditSensitive;
class User
{
#[AuditSensitive(
encrypt: true, // Encrypt the value
mask: true, // Mask in UI (show as ***)
hashAlgorithm: 'sha256' // Hash algorithm for encryption
)]
private string $socialSecurityNumber;
}
Parameters:
encrypt: Whether to encrypt the field valuemask: Whether to mask the value in the UIhashAlgorithm: Algorithm for hashing sensitive data#[AuditMetadata]Adds custom metadata to audit logs.
use BenMacha\AuditBundle\Attribute\AuditMetadata;
class User
{
#[AuditMetadata(
tags: ['pii', 'gdpr'], // Custom tags
indexed: true, // Index for searching
ttl: 2592000, // TTL in seconds (30 days)
customData: ['department' => 'HR'] // Additional metadata
)]
private string $email;
}
Parameters:
tags: Array of custom tags for categorizationindexed: Whether to index this field for searchingttl: Time-to-live in secondscustomData: Additional custom metadata#[AuditContext]Provides additional context for audit operations.
use BenMacha\AuditBundle\Attribute\AuditContext;
#[AuditContext(
reason: 'User profile update',
category: 'user_management',
priority: 'high',
metadata: ['source' => 'admin_panel']
)]
class User
{
// ...
}
Parameters:
reason: Human-readable reason for the changecategory: Category for grouping related changespriority: Priority level (low, medium, high, critical)metadata: Additional context metadataListen to audit events for custom processing:
<?php
namespace App\EventSubscriber;
use BenMacha\AuditBundle\Event\AuditEvent;
use BenMacha\AuditBundle\Event\AuditEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class AuditSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
AuditEvents::PRE_AUDIT => 'onPreAudit',
AuditEvents::POST_AUDIT => 'onPostAudit',
AuditEvents::ROLLBACK => 'onRollback',
];
}
public function onPreAudit(AuditEvent $event): void
{
// Modify audit data before saving
$auditLog = $event->getAuditLog();
$auditLog->addMetadata('processed_by', 'custom_subscriber');
}
public function onPostAudit(AuditEvent $event): void
{
// React to audit log creation
$this->notificationService->sendAuditNotification($event->getAuditLog());
}
public function onRollback(AuditEvent $event): void
{
// Handle rollback operations
$this->logger->info('Entity rolled back', [
'entity' => $event->getEntityClass(),
'id' => $event->getEntityId()
]);
}
}
Log changes manually when needed:
<?php
namespace App\Service;
use BenMacha\AuditBundle\Service\AuditService;
class UserService
{
public function __construct(
private AuditService $auditService
) {}
public function updateUserProfile(User $user, array $data): void
{
$oldData = $this->extractUserData($user);
// Update user
$user->setEmail($data['email']);
$user->setName($data['name']);
// Manual audit logging
$this->auditService->logEntityChange(
$user,
'update',
$oldData,
$this->extractUserData($user),
'Profile updated via API'
);
}
}
Implement custom rollback behavior:
<?php
namespace App\Service;
use BenMacha\AuditBundle\Service\AuditService;
use BenMacha\AuditBundle\Entity\AuditLog;
class CustomRollbackService
{
public function __construct(
private AuditService $auditService
) {}
public function rollbackWithValidation(AuditLog $auditLog): bool
{
// Custom validation logic
if (!$this->canRollback($auditLog)) {
throw new \Exception('Rollback not allowed');
}
// Perform rollback
return $this->auditService->rollbackEntity($auditLog);
}
private function canRollback(AuditLog $auditLog): bool
{
// Implement your validation logic
return $auditLog->getCreatedAt() > new \DateTime('-24 hours');
}
}
Manage audit data lifecycle:
<?php
namespace App\Command;
use BenMacha\AuditBundle\Service\AuditCleanupService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AuditCleanupCommand extends Command
{
protected static $defaultName = 'audit:cleanup';
public function __construct(
private AuditCleanupService $cleanupService
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// Clean up old audit logs
$deleted = $this->cleanupService->cleanupExpiredLogs();
$output->writeln("Deleted {$deleted} expired audit logs");
// Optimize audit tables
$this->cleanupService->optimizeTables();
$output->writeln('Optimized audit tables');
return Command::SUCCESS;
}
}
Override default templates:
templates/bundles/AuditBundle/ directoryvendor/benmacha/audit-bundle/src/Resources/views/{# templates/bundles/AuditBundle/audit/index.html.twig #}
{% extends 'base.html.twig' %}
{% block title %}Custom Audit Logs{% endblock %}
{% block body %}
<div class="custom-audit-container">
<h1>My Custom Audit Interface</h1>
{# Include original content with modifications #}
{% include '@Audit/audit/_table.html.twig' %}
</div>
{% endblock %}
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/audit/logs |
Get all audit logs |
| GET | /api/audit/logs/{id} |
Get specific audit log |
| GET | /api/audit/logs/entity/{class}/{id} |
Get logs for entity |
| POST | /api/audit/rollback/{id} |
Rollback to audit log |
| DELETE | /api/audit/logs/{id} |
Delete audit log |
| GET | /api/audit/stats |
Get audit statistics |
limit: Number of results (default: 25)offset: Offset for pagination (default: 0)entity: Filter by entity classoperation: Filter by operation typeuser: Filter by user IDfrom: Start date (ISO 8601)to: End date (ISO 8601){
"data": [
{
"id": 123,
"entityClass": "App\\Entity\\User",
"entityId": "456",
"operation": "update",
"oldValues": {"email": "old@example.com"},
"newValues": {"email": "new@example.com"},
"userId": 789,
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"createdAt": "2024-01-15T10:30:00Z",
"metadata": {"reason": "Profile update"}
}
],
"total": 1,
"limit": 25,
"offset": 0
}
Enable async processing for high-traffic applications:
# config/packages/audit.yaml
audit:
async_processing: true
# config/packages/messenger.yaml
framework:
messenger:
transports:
audit: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'BenMacha\AuditBundle\Message\AuditMessage': audit
// Batch processing for large operations
$batchSize = 100;
for ($i = 0; $i < $totalRecords; $i += $batchSize) {
$entities = $repository->findBy([], null, $batchSize, $i);
foreach ($entities as $entity) {
// Process entity
}
$entityManager->flush();
$entityManager->clear(); // Clear memory
}
# config/packages/security.yaml
security:
access_control:
- { path: ^/admin/audit, roles: ROLE_AUDITOR }
- { path: ^/api/audit, roles: ROLE_API_USER }
#[AuditSensitive] for sensitive fieldsMissing Audit Logs
#[Auditable] attributePerformance Issues
Memory Issues
Enable debug logging:
# config/packages/dev/monolog.yaml
monolog:
handlers:
audit:
type: stream
path: '%kernel.logs_dir%/audit.log'
level: debug
channels: ['audit']
git clone https://github.com/benmacha/audit-bundle.git
cd audit-bundle
composer install
php bin/phpunit
composer run quality # Run all quality checks
composer run cs-fix # Fix coding standards
composer run phpstan # Static analysis
composer run psalm # Additional static analysis
This bundle is released under the MIT License. See LICENSE for details.
See CHANGELOG.md for version history.
How can I help you explore Laravel packages today?