Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Auditor Bundle
https://github.com/damienharper/auditor-bundle
Admin
Auditor Bundle is a Symfony bundle that integrates audit logging functionality to automatically
...
Tokens:
30,320
Snippets:
281
Trust Score:
9.2
Update:
1 month ago
Context
Skills
Chat
Benchmark
69.9
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Auditor Bundle Auditor Bundle integrates the auditor library into Symfony applications, providing automatic audit logging for all Doctrine ORM database changes. It automatically tracks and records entity insertions, updates, deletions, and relationship changes with full user attribution, IP address tracking, and transaction grouping. The bundle seamlessly integrates with Symfony's security system for user identification and access control. The bundle provides a complete solution for audit trail requirements including a built-in web viewer at `/audit`, console command tracking, role-based access control for viewing audit logs, and support for multi-database configurations. It includes PHP 8 attributes for entity configuration, customizable providers for user/security information, and extensibility through custom audit providers. Translations are available in 9 languages, and the viewer includes features like dark mode, activity graphs, and filtering by action type or user. ## Installation Install the bundle via Composer and register it in your Symfony application. ```bash # Install the bundle (includes auditor library automatically) composer require damienharper/auditor-bundle # Clear cache after installation bin/console cache:clear # Create audit tables using migrations (recommended) bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate # Or using schema tool directly bin/console doctrine:schema:update --force # Install assets for the viewer bin/console assets:install ``` ## Basic Configuration Configure audited entities in `config/packages/dh_auditor.yaml` with YAML configuration. ```yaml # config/packages/dh_auditor.yaml dh_auditor: enabled: true # Enable/disable auditing globally timezone: 'UTC' # Timezone for audit timestamps providers: doctrine: table_prefix: '' # Prefix for audit table names table_suffix: '_audit' # Suffix for audit table names # Properties to ignore globally across all entities ignored_columns: - createdAt - updatedAt - password entities: # Simple: all defaults App\Entity\User: ~ # With options App\Entity\Post: enabled: true ignored_columns: - viewCount roles: view: - ROLE_ADMIN App\Entity\Comment: ~ # Enable the web viewer viewer: enabled: true page_size: 50 activity_graph: enabled: true days: 30 layout: 'bottom' # 'bottom' or 'inline' cache: enabled: true pool: 'cache.app' ttl: 300 ``` ## Entity Attributes Configure auditing behavior directly on entity classes using PHP 8 attributes. ```php <?php namespace App\Entity; use DH\Auditor\Provider\Doctrine\Auditing\Attribute as Audit; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[Audit\Auditable] // Mark entity as auditable #[Audit\Security(view: ['ROLE_ADMIN', 'ROLE_AUDITOR'])] // Restrict who can view audits class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] private ?string $email = null; #[ORM\Column] #[Audit\Ignore] // Exclude from auditing private ?string $password = null; #[ORM\Column(length: 255)] private ?string $firstName = null; #[ORM\Column(length: 255)] private ?string $lastName = null; #[ORM\Column] #[Audit\Ignore] // Exclude from auditing private ?\DateTimeImmutable $lastLoginAt = null; // Getters and setters... } ``` ## Diff Label Resolvers Display human-readable labels instead of raw IDs in audit diffs using the `#[DiffLabel]` attribute. ```php <?php namespace App\Audit\Resolver; use App\Repository\CategoryRepository; use DH\Auditor\Contract\DiffLabelResolverInterface; // Resolver implementation - auto-tagged by Symfony autoconfiguration final class CategoryResolver implements DiffLabelResolverInterface { public function __construct( private readonly CategoryRepository $repository, ) {} public function __invoke(mixed $value): ?string { // Return null if value cannot be resolved (record deleted, etc.) return $this->repository->find($value)?->getName(); } } // Entity using the resolver namespace App\Entity; use App\Audit\Resolver\CategoryResolver; use DH\Auditor\Provider\Doctrine\Auditing\Attribute as Audit; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[Audit\Auditable] class Product { #[ORM\Column(type: Types::INTEGER)] #[Audit\DiffLabel(resolver: CategoryResolver::class)] // Resolves categoryId to category name private int $categoryId; } // Result: {"categoryId": {"old": {"value": 1, "label": "Books"}, "new": {"value": 2, "label": "Electronics"}}} ``` ## Reading Audit Entries Query and read audit entries programmatically using the Reader service. ```php <?php use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader; use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\DateRangeFilter; use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\JsonFilter; class AuditService { public function __construct( private readonly Reader $reader, ) {} public function getRecentUserChanges(): array { // Create query for User entity $query = $this->reader->createQuery(User::class, [ 'page_size' => 50, 'page' => 1, ]); // Execute and get entries $entries = $query->execute(); foreach ($entries as $entry) { echo sprintf( "ID: %s, Type: %s, User: %s, Date: %s\n", $entry->id, $entry->type, // insert, update, remove, associate, dissociate $entry->username, $entry->createdAt->format('Y-m-d H:i:s') ); // Access the diff data $diffs = $entry->getDiffs(); foreach ($diffs as $field => $change) { echo sprintf(" %s: %s -> %s\n", $field, $change['old'] ?? 'null', $change['new'] ?? 'null'); } // Access extra data if present if (null !== $entry->extraData) { echo sprintf(" Route: %s\n", $entry->extraData['route'] ?? 'N/A'); } } return $entries; } public function getEntityHistory(int $userId): array { // Get all changes for a specific entity instance $query = $this->reader->createQuery(User::class, [ 'object_id' => $userId, 'page_size' => null, // No pagination - get all ]); return $query->execute(); } public function getTransactionAudits(string $transactionHash): array { // Get all changes from a single database transaction return $this->reader->getAuditsByTransactionHash($transactionHash); } public function filterByExtraData(): array { // Filter entries by extra_data JSON content $query = $this->reader->createQuery(User::class, ['page_size' => null]); // Filter by exact value $query->addFilter(new JsonFilter('extra_data', 'route', 'app_user_edit')); // Filter with LIKE pattern $query->addFilter(new JsonFilter('extra_data', 'department', 'IT%', 'LIKE')); // Filter by multiple values (IN) $query->addFilter(new JsonFilter('extra_data', 'status', ['active', 'pending'], 'IN')); return $query->execute(); } } ``` ## Custom User Provider Customize how the current user is identified in audit entries for non-standard authentication systems. ```php <?php namespace App\Audit; use DH\Auditor\User\User; use DH\Auditor\User\UserInterface; use DH\Auditor\User\UserProviderInterface; use App\Security\ApiTokenManager; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; // Custom provider combining multiple auth sources class CompositeUserProvider implements UserProviderInterface { public function __construct( private readonly TokenStorageInterface $tokenStorage, private readonly ApiTokenManager $apiTokenManager, ) {} public function __invoke(): ?UserInterface { // Try Symfony security first $token = $this->tokenStorage->getToken(); if (null !== $token && null !== $token->getUser()) { $user = $token->getUser(); return new User( method_exists($user, 'getId') ? (string) $user->getId() : '', $user->getUserIdentifier() ); } // Fall back to API token $apiToken = $this->apiTokenManager->getCurrentToken(); if (null !== $apiToken) { return new User( $apiToken->getClientId(), 'API: ' . $apiToken->getClientName() ); } return null; // Anonymous user } } // config/packages/dh_auditor.yaml // dh_auditor: // user_provider: 'App\Audit\CompositeUserProvider' ``` ## Custom Security Provider Customize IP address and context detection for audit entries, especially when behind proxies. ```php <?php namespace App\Audit; use DH\Auditor\Security\SecurityProviderInterface; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; // Cloudflare-aware security provider class CloudflareSecurityProvider implements SecurityProviderInterface { public function __construct( private readonly RequestStack $requestStack, private readonly FirewallMap $firewallMap, ) {} public function __invoke(): array { $request = $this->requestStack->getCurrentRequest(); if (!$request instanceof Request) { return [null, null]; } // Cloudflare provides original IP in CF-Connecting-IP header $clientIp = $request->headers->get('CF-Connecting-IP') ?? $request->headers->get('X-Forwarded-For') ?? $request->getClientIp(); // Handle X-Forwarded-For with multiple IPs (take first) if (null !== $clientIp && str_contains($clientIp, ',')) { $clientIp = trim(explode(',', $clientIp)[0]); } $firewallConfig = $this->firewallMap->getFirewallConfig($request); return [$clientIp, $firewallConfig?->getName()]; } } // config/packages/dh_auditor.yaml // dh_auditor: // security_provider: 'App\Audit\CloudflareSecurityProvider' ``` ## Custom Role Checker Customize access control for the audit viewer with complex authorization logic. ```php <?php namespace App\Audit; use App\Entity\User; use App\Entity\Order; use App\Entity\Payment; use DH\Auditor\Security\RoleCheckerInterface; use Symfony\Bundle\SecurityBundle\Security; class EntityRoleChecker implements RoleCheckerInterface { private const ENTITY_ROLES = [ User::class => ['ROLE_USER_ADMIN'], Order::class => ['ROLE_ORDER_MANAGER', 'ROLE_ACCOUNTANT'], Payment::class => ['ROLE_ACCOUNTANT'], ]; public function __construct( private readonly Security $security, ) {} public function __invoke(string $entity, string $scope): bool { // Super admin bypass if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { return true; } $requiredRoles = self::ENTITY_ROLES[$entity] ?? []; // No restriction for unlisted entities if (empty($requiredRoles)) { return true; } // Check if user has any of the required roles (OR logic) foreach ($requiredRoles as $role) { if ($this->security->isGranted($role)) { return true; } } return false; } } // config/packages/dh_auditor.yaml // dh_auditor: // role_checker: 'App\Audit\EntityRoleChecker' ``` ## Extra Data Provider Attach additional contextual information to audit entries automatically. ```php <?php namespace App\Audit; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; // Global extra data provider - called for every audit entry final readonly class MyExtraDataProvider { public function __construct( private RequestStack $requestStack, private TokenStorageInterface $tokenStorage, ) {} public function __invoke(): ?array { $request = $this->requestStack->getCurrentRequest(); if (null === $request) { return null; // Console context - no extra data } return [ 'route' => $request->attributes->get('_route'), 'request_id' => $request->headers->get('X-Request-ID'), 'user_agent' => $request->headers->get('User-Agent'), 'tenant_id' => $request->attributes->get('tenant_id'), ]; } } // config/packages/dh_auditor.yaml // dh_auditor: // extra_data_provider: 'App\Audit\MyExtraDataProvider' ``` ## LifecycleEvent Listener for Extra Data Attach entity-specific extra data using Symfony event listeners. ```php <?php namespace App\EventListener; use App\Entity\Order; use DH\Auditor\Event\LifecycleEvent; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\RequestStack; #[AsEventListener(event: LifecycleEvent::class, priority: 10)] final class OrderAuditExtraDataListener { public function __construct( private readonly Security $security, private readonly RequestStack $requestStack, ) {} public function __invoke(LifecycleEvent $event): void { $payload = $event->getPayload(); // Only process Order entity if ($payload['entity'] !== Order::class) { return; } $request = $this->requestStack->getCurrentRequest(); // Decode existing extra_data if provider already set it $existing = null !== $payload['extra_data'] ? json_decode($payload['extra_data'], true, 512, JSON_THROW_ON_ERROR) : []; // Merge entity-specific data $merged = array_merge($existing, [ 'admin_user' => $this->security->getUser()?->getUserIdentifier(), 'order_status' => $event->entity?->getStatus(), 'audit_reason' => $request?->headers->get('X-Audit-Reason'), ]); // IMPORTANT: Must be JSON-encoded string, not array $payload['extra_data'] = json_encode($merged, JSON_THROW_ON_ERROR); $event->setPayload($payload); } } ``` ## Multi-Database Storage Configuration Store audit logs in a separate database from your entities. ```yaml # config/packages/doctrine.yaml doctrine: dbal: default_connection: default connections: default: url: '%env(DATABASE_URL)%' audit: url: '%env(AUDIT_DATABASE_URL)%' orm: default_entity_manager: default entity_managers: default: connection: default mappings: App: type: attribute dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' audit: connection: audit # No mappings needed - audit tables are created dynamically # config/packages/dh_auditor.yaml dh_auditor: providers: doctrine: # Store audits in separate database storage_services: - '@doctrine.orm.audit_entity_manager' # Monitor changes on default connection auditing_services: - '@doctrine.orm.default_entity_manager' # Route audits to storage (required for multi-storage) storage_mapper: 'App\Audit\StorageMapper' entities: App\Entity\User: ~ App\Entity\Order: ~ ``` ```php <?php namespace App\Audit; use DH\Auditor\Provider\Service\StorageServiceInterface; class StorageMapper { public function __invoke(string $entity, array $storageServices): StorageServiceInterface { // All audits go to the audit database return $storageServices['dh_auditor.provider.doctrine.storage_services.doctrine.orm.audit_entity_manager']; } } ``` ## Custom Audit Provider Register a custom provider for alternative storage backends like Elasticsearch. ```php <?php namespace App\Audit; use DH\Auditor\Event\LifecycleEvent; use DH\Auditor\Provider\AbstractProvider; use Symfony\Contracts\Service\ResetInterface; final class ElasticsearchProvider extends AbstractProvider implements ResetInterface { public function __construct(private readonly ElasticClient $client) { $this->configuration = new ElasticsearchConfiguration($client); $this->registerStorageService(new ElasticsearchStorageService('default', $client)); } public function supportsStorage(): bool { return true; } public function supportsAuditing(): bool { // This provider only writes audit entries; DoctrineProvider handles change detection return false; } public function persist(LifecycleEvent $event): void { $payload = $event->getPayload(); $this->client->index([ 'index' => 'audit', 'body' => [ 'type' => $payload['type'], 'entity' => $payload['entity'], 'object_id' => $payload['object_id'], 'diffs' => json_decode($payload['diffs'], true), 'blame_user' => $payload['blame_user'], 'created_at' => $payload['created_at']->format(\DateTimeInterface::ATOM), ], ]); } public function reset(): void { $this->client->reset(); // Clear state for long-running processes } } // config/services.yaml // App\Audit\ElasticsearchProvider: // tags: [dh_auditor.provider] ``` ## Viewer Routes and URL Generation Generate URLs for the audit viewer programmatically. ```php <?php use DH\AuditorBundle\Helper\UrlHelper; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class AuditLinkGenerator { public function __construct( private readonly UrlGeneratorInterface $urlGenerator, ) {} public function generateLinks(): array { // Entity list - /audit $listUrl = $this->urlGenerator->generate('dh_auditor_list_audits'); // Entity stream - /audit/App-Entity-User $entityUrl = $this->urlGenerator->generate('dh_auditor_show_entity_stream', [ 'entity' => UrlHelper::namespaceToParam('App\Entity\User'), // 'App-Entity-User' ]); // Specific entity instance - /audit/App-Entity-User/42 $instanceUrl = $this->urlGenerator->generate('dh_auditor_show_entity_stream', [ 'entity' => 'App-Entity-User', 'id' => 42, ]); // With filters - /audit/App-Entity-User?type=update&user=42 $filteredUrl = $this->urlGenerator->generate('dh_auditor_show_entity_stream', [ 'entity' => 'App-Entity-User', 'type' => 'update', 'user' => '42', ]); // Anonymous filter - /audit/App-Entity-User?user=__anonymous__ $anonymousUrl = $this->urlGenerator->generate('dh_auditor_show_entity_stream', [ 'entity' => 'App-Entity-User', 'user' => '__anonymous__', ]); // Transaction view - /audit/transaction/{hash} $transactionUrl = $this->urlGenerator->generate('dh_auditor_show_transaction_stream', [ 'hash' => 'abc123def456', ]); return [ 'list' => $listUrl, 'entity' => $entityUrl, 'instance' => $instanceUrl, 'filtered' => $filteredUrl, 'anonymous' => $anonymousUrl, 'transaction' => $transactionUrl, ]; } public function convertNamespace(): void { // Convert namespace to URL parameter $param = UrlHelper::namespaceToParam('App\Entity\User'); // Returns: 'App-Entity-User' // Convert URL parameter back to namespace $namespace = UrlHelper::paramToNamespace('App-Entity-User'); // Returns: 'App\Entity\User' } } ``` ## Console Commands Use the built-in console commands for schema management and cache control. ```bash # Update audit schema (for multi-database setups) bin/console audit:schema:update --dump-sql # Preview SQL bin/console audit:schema:update --force # Apply changes # Clean old audit logs (keep last 12 months) bin/console audit:clean --keep=12 # Clear activity graph cache bin/console audit:cache:clear # Clear all bin/console audit:cache:clear --entity="App\Entity\User" # Clear specific entity ``` ## Template Customization Override audit viewer templates to customize the appearance. ```twig {# templates/bundles/DHAuditorBundle/layout.html.twig #} {% extends 'base.html.twig' %} {% block stylesheets %} {{ parent() }} <link rel="stylesheet" href="{{ asset('bundles/dhauditor/app.css') }}"> {% endblock %} {% block body %} <div class="container mx-auto px-4"> {% block dh_auditor_content %}{% endblock %} </div> {% endblock %} {% block javascripts %} {{ parent() }} {% endblock %} ``` ``` # Template override structure templates/bundles/DHAuditorBundle/ ├── Audit/ │ ├── audits.html.twig # Entity list │ ├── entity_stream.html.twig # Entity audit stream │ ├── transaction_stream.html.twig # Transaction view │ ├── entry.html.twig # Single entry │ └── helpers/ │ ├── helper.html.twig # Helper macros │ └── pager.html.twig # Pagination └── layout.html.twig # Base layout ``` ## Summary Auditor Bundle is ideal for applications requiring comprehensive audit trails for compliance, security monitoring, or debugging purposes. Common use cases include financial applications requiring transaction audit logs, healthcare systems needing HIPAA-compliant change tracking, e-commerce platforms tracking order modifications, and any system where understanding "who changed what and when" is critical. The bundle handles the complexity of capturing changes at the Doctrine ORM level, ensuring no modification goes unrecorded. Integration follows standard Symfony patterns with YAML configuration and PHP attributes for entity-level customization. The bundle automatically hooks into Symfony's security system for user attribution and provides extensibility points for custom user providers, security providers, and role checkers. For advanced use cases, custom audit providers can store entries in alternative backends (Elasticsearch, external APIs) while DoctrineProvider handles change detection. The built-in viewer provides immediate value for debugging and auditing, while the Reader API enables custom reporting and analytics integration.