Skip to main content
Version: 4.x

Querying Audits

Read and query audit entries with the powerful Reader and Query APIs

This guide covers how to read and query audit entries using the Reader and Query APIs.

🔍 Overview

The DoctrineProvider includes a powerful querying system:

  • Reader - Factory for creating queries and utilities
  • Query - Builds and executes audit queries
  • Filters - Filter results by various criteria
  • Entry - Represents a single audit log entry

📖 The Reader Class

Creating a Reader

<?php

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader;

$reader = new Reader($doctrineProvider);

Creating a Query

// Simple query for all audits of an entity
$query = $reader->createQuery(User::class);
$audits = $query->execute();

// Query with options
$query = $reader->createQuery(User::class, [
'object_id' => 123, // Specific entity ID
'type' => 'update', // Action type
'page' => 1, // Pagination
'page_size' => 20,
]);

Query Options

OptionTypeDefaultDescription
object_idint|string|arraynullFilter by entity ID(s)
typestring|arraynullFilter by action type(s)
blame_idint|string|arraynullFilter by user ID(s) who made changes
user_idint|string|arraynullAlias for blame_id
transaction_hashstring|arraynullFilter by transaction hash(es)
pageint|null1Page number (1-based)
page_sizeint|null50Results per page
strictbooltrueUse discriminator for inheritance

📝 Basic Queries

Get All Audits for an Entity

$query = $reader->createQuery(User::class);
$audits = $query->execute();

foreach ($audits as $entry) {
echo sprintf(
"[%s] %s on User #%s by %s\n",
$entry->getCreatedAt()->format('Y-m-d H:i:s'),
$entry->getType(),
$entry->getObjectId(),
$entry->getUsername() ?? 'unknown'
);
}

Get Audits for a Specific Entity

$query = $reader->createQuery(User::class, [
'object_id' => 123,
]);
$audits = $query->execute();

Get Audits by Type

// Only updates
$query = $reader->createQuery(User::class, [
'type' => 'update',
]);

// Multiple types
$query = $reader->createQuery(User::class, [
'type' => ['insert', 'update'],
]);

Get Audits by User

// Changes made by a specific user
$query = $reader->createQuery(User::class, [
'blame_id' => 42,
]);

// Changes by multiple users
$query = $reader->createQuery(User::class, [
'blame_id' => [42, 43, 44],
]);

Get Audits by Transaction

// All changes from a single transaction
$query = $reader->createQuery(User::class, [
'transaction_hash' => 'abc123def456...',
]);

// Or across all entities
$audits = $reader->getAuditsByTransactionHash('abc123def456...');
// Returns: ['App\Entity\User' => [...], 'App\Entity\Post' => [...]]

🔧 The Query Class

For more control, work directly with the Query object:

$query = $reader->createQuery(User::class);

// Add custom ordering
$query->resetOrderBy();
$query->addOrderBy(Query::CREATED_AT, 'ASC');
$query->addOrderBy(Query::ID, 'ASC');

// Add custom limit
$query->limit(100, 0); // limit, offset

$audits = $query->execute();

Resetting Query Parts

You can reset specific parts of a query using resetQueryPart():

$query = $reader->createQuery(User::class);

// Reset order by clauses
$query->resetQueryPart('orderBy');

// Reset limit and offset
$query->resetQueryPart('limit');

// Reset all filters
$query->resetQueryPart('filters');
PartDescription
orderByResets all ORDER BY clauses
limitResets LIMIT and OFFSET to 0
filtersResets all filters to empty arrays

TIP

You can also use the shorthand resetOrderBy() method which is equivalent to resetQueryPart('orderBy').

Query Constants

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Query;

Query::TYPE // 'type'
Query::OBJECT_ID // 'object_id'
Query::DISCRIMINATOR // 'discriminator'
Query::TRANSACTION_HASH // 'transaction_hash'
Query::USER_ID // 'blame_id'
Query::CREATED_AT // 'created_at'
Query::ID // 'id'

🎯 Filters

SimpleFilter

For exact value matching:

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;

$query = $reader->createQuery(User::class);

// Single value
$query->addFilter(new SimpleFilter(Query::TYPE, 'update'));

// Multiple values (OR condition)
$query->addFilter(new SimpleFilter(Query::TYPE, ['insert', 'update']));

DateRangeFilter

For filtering by date range:

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\DateRangeFilter;

$query = $reader->createQuery(User::class, ['page_size' => null]);

// Audits from the last 30 days
$query->addFilter(new DateRangeFilter(
Query::CREATED_AT,
new \DateTime('-30 days'),
new \DateTime('now')
));

// Audits since a specific date
$query->addFilter(new DateRangeFilter(
Query::CREATED_AT,
new \DateTime('2024-01-01'),
null // No upper bound
));

// Audits until a specific date
$query->addFilter(new DateRangeFilter(
Query::CREATED_AT,
null, // No lower bound
new \DateTime('2024-06-30')
));

RangeFilter

For numeric ranges:

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\RangeFilter;

$query = $reader->createQuery(User::class, ['page_size' => null]);

// Audits for IDs between 100 and 200
$query->addFilter(new RangeFilter(Query::ID, 100, 200));

// Audits with ID >= 500
$query->addFilter(new RangeFilter(Query::ID, 500, null));

📄 Pagination

Using Query Options

$query = $reader->createQuery(User::class, [
'page' => 1,
'page_size' => 20,
]);
$audits = $query->execute();

Using the Paginate Method

$query = $reader->createQuery(User::class);
$result = $reader->paginate($query, $page = 1, $pageSize = 20);

// Result structure
[
'results' => ArrayIterator, // The audit entries
'currentPage' => 1,
'hasPreviousPage' => false,
'hasNextPage' => true,
'previousPage' => null,
'nextPage' => 2,
'numPages' => 5,
'haveToPaginate' => true,
'numResults' => 100,
'pageSize' => 20,
]

Disable Pagination

TIP

Set both page and page_size to null to get all results without pagination.

// Get all results without pagination
$query = $reader->createQuery(User::class, [
'page' => null,
'page_size' => null,
]);
$allAudits = $query->execute();

🔢 Counting Results

$query = $reader->createQuery(User::class);
$count = $query->count();

📦 The Entry Model

Each audit result is an Entry object:

/** @var Entry $entry */
foreach ($audits as $entry) {
// Basic info
$entry->getId(); // int - Audit entry ID
$entry->getType(); // string - insert, update, remove, etc.
$entry->getObjectId(); // string - Entity ID
$entry->getCreatedAt(); // DateTimeImmutable

// Transaction
$entry->getTransactionHash(); // string|null
$entry->getDiscriminator(); // string|null - For inheritance

// Changes
$entry->getDiffs(); // array - The actual changes
$entry->getDiffs(true); // array - Including @source metadata

// User attribution
$entry->getUserId(); // int|string|null - blame_id
$entry->getUsername(); // string|null - blame_user
$entry->getUserFqdn(); // string|null - User class
$entry->getUserFirewall(); // string|null - Firewall name
$entry->getIp(); // string|null - Client IP
}

📊 Reading Diffs

The getDiffs() method returns the changes:

Insert Diffs

// For insert operations:
[
'email' => [
'new' => 'john@example.com',
'old' => null,
],
'name' => [
'new' => 'John Doe',
'old' => null,
],
]

Update Diffs

// For update operations:
[
'email' => [
'new' => 'jane@example.com',
'old' => 'john@example.com',
],
]

Association Diffs

// For associate/dissociate operations:
[
'source' => [
'class' => 'App\\Entity\\Post',
'id' => 1,
'label' => 'My Post',
'field' => 'tags',
],
'target' => [
'class' => 'App\\Entity\\Tag',
'id' => 5,
'label' => 'PHP',
'field' => 'posts',
],
'is_owning_side' => true,
'table' => 'post_tag', // Join table
]

🔧 Utility Methods

Get Entity Table Names

// Get the source entity table name
$tableName = $reader->getEntityTableName(User::class);
// Returns: 'users'

// Get the audit table name
$auditTableName = $reader->getEntityAuditTableName(User::class);
// Returns: 'users_audit'

⚠️ Error Handling

AccessDeniedException

WARNING

Thrown when the role checker denies access to view audits for an entity.

use DH\Auditor\Exception\AccessDeniedException;

try {
$query = $reader->createQuery(SensitiveEntity::class);
$audits = $query->execute();
} catch (AccessDeniedException $e) {
// Handle access denied
}

InvalidArgumentException

CAUTION

Thrown when querying non-auditable entities.

use DH\Auditor\Exception\InvalidArgumentException;

try {
$query = $reader->createQuery(NonAuditableEntity::class);
} catch (InvalidArgumentException $e) {
// Entity App\Entity\NonAuditableEntity is not auditable.
}

📄 Complete Example

<?php

use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader;
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Query;
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\DateRangeFilter;
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\SimpleFilter;

$reader = new Reader($provider);

// Build a complex query
$query = $reader->createQuery(User::class, [
'page' => null,
'page_size' => null,
]);

// Add filters
$query->addFilter(new SimpleFilter(Query::TYPE, ['insert', 'update']));
$query->addFilter(new DateRangeFilter(
Query::CREATED_AT,
new \DateTime('-7 days'),
new \DateTime('now')
));

// Custom ordering
$query->resetOrderBy();
$query->addOrderBy(Query::CREATED_AT, 'DESC');

// Limit results
$query->limit(100);

// Execute and process
$audits = $query->execute();

foreach ($audits as $entry) {
$diffs = $entry->getDiffs();

echo sprintf(
"[%s] %s #%s by %s from %s\n",
$entry->getCreatedAt()->format('Y-m-d H:i:s'),
$entry->getType(),
$entry->getObjectId(),
$entry->getUsername() ?? 'system',
$entry->getIp() ?? 'unknown'
);

foreach ($diffs as $field => $change) {
echo sprintf(" - %s: %s → %s\n",
$field,
json_encode($change['old'] ?? null),
json_encode($change['new'] ?? null)
);
}
}

Next Steps