Skip to main content
Version: 4.x

Upgrading to 4.x from 3.x

PHP 8.4+ modernization and new features

This document describes the backward incompatible changes introduced in auditor 4.0 and how to adapt your code.

⚠️ Requirements Changes​

PHP Version​

IMPORTANT

Minimum PHP version is now 8.4 (was 8.2 in 3.x)

This is required by Symfony 8.0 and leverages PHP 8.4 features.

Symfony Version​

IMPORTANT

Minimum Symfony version is now 8.0 (was 5.4 in 3.x)

Support for Symfony 5.4, 6.4, and 7.x has been dropped.

Doctrine Versions​

Package3.x Version4.x Version
Doctrine DBAL>= 3.2>= 4.0
Doctrine ORM>= 2.13>= 3.2

PHPUnit Version​

PHPUnit minimum version is now 12.0 (was 11.0 in 3.x)

🗑️ Removed Methods​

DoctrineHelper​

The following methods have been removed from DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper:

Removed MethodReplacement
DoctrineHelper::createSchemaManager()$connection->createSchemaManager()
DoctrineHelper::introspectSchema()$schemaManager->introspectSchema()
DoctrineHelper::getMigrateToSql()See migration example below

These methods were compatibility shims for older Doctrine DBAL versions that are no longer needed.

📦 Namespace Changes​

Annotation → Attribute​

TIP

The Annotation namespace has been renamed to Attribute to align with PHP 8+ terminology.

Before (3.x)After (4.0)
DH\Auditor\Provider\Doctrine\Auditing\Annotation\AuditableDH\Auditor\Provider\Doctrine\Auditing\Attribute\Auditable
DH\Auditor\Provider\Doctrine\Auditing\Annotation\IgnoreDH\Auditor\Provider\Doctrine\Auditing\Attribute\Ignore
DH\Auditor\Provider\Doctrine\Auditing\Annotation\SecurityDH\Auditor\Provider\Doctrine\Auditing\Attribute\Security
DH\Auditor\Provider\Doctrine\Auditing\Annotation\AnnotationLoaderDH\Auditor\Provider\Doctrine\Auditing\Attribute\AttributeLoader

Before (3.x):

use DH\Auditor\Provider\Doctrine\Auditing\Annotation as Audit;

#[Audit\Auditable]
class MyEntity
{
#[Audit\Ignore]
private string $ignoredField;
}

After (4.0):

use DH\Auditor\Provider\Doctrine\Auditing\Attribute as Audit;

#[Audit\Auditable]
class MyEntity
{
#[Audit\Ignore]
private string $ignoredField;
}

Attribute → Core Namespace (4.x)​

TIP

PHP attributes have been promoted to the core DH\Auditor\Attribute namespace to be provider-agnostic (e.g. for future Eloquent provider support). The old provider-specific classes are now deprecated but still work as they extend the new ones.

Deprecated (4.x)Canonical (4.x)
DH\Auditor\Provider\Doctrine\Auditing\Attribute\AuditableDH\Auditor\Attribute\Auditable
DH\Auditor\Provider\Doctrine\Auditing\Attribute\IgnoreDH\Auditor\Attribute\Ignore
DH\Auditor\Provider\Doctrine\Auditing\Attribute\SecurityDH\Auditor\Attribute\Security

Before (deprecated):

use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Auditable;
use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Ignore;
use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Security;

After (canonical):

use DH\Auditor\Attribute\Auditable;
use DH\Auditor\Attribute\Ignore;
use DH\Auditor\Attribute\Security;

📊 Model Changes​

Entry Model - Property Hooks (PHP 8.4+)​

NOTE

The Entry model now uses PHP 8.4 property hooks. Getter methods have been replaced by direct property access.

Before (3.x)After (4.0)
$entry->getId()$entry->id
$entry->getType()$entry->type
$entry->getObjectId()$entry->objectId
$entry->getDiscriminator()$entry->discriminator
$entry->getTransactionHash()$entry->transactionHash
$entry->getUserId()$entry->userId
$entry->getUsername()$entry->username
$entry->getUserFqdn()$entry->userFqdn
$entry->getUserFirewall()$entry->userFirewall
$entry->getIp()$entry->ip
$entry->getCreatedAt()$entry->createdAt
(new)$entry->extraData

Note: getDiffs() and getExtraData() methods remain as they contain business logic. extraData is also available as a virtual property.

Configuration - Asymmetric Visibility (PHP 8.4+)​

The Configuration class now uses PHP 8.4 asymmetric visibility:

Before (3.x)After (4.0)
$config->isEnabled()$config->enabled
$config->getTimezone()$config->timezone

User Model - Property Hooks (PHP 8.4+)​

The UserInterface now uses PHP 8.4 property hooks:

Before (3.x)After (4.0)
$user->getIdentifier()$user->identifier
$user->getUsername()$user->username

If you have custom UserInterface implementations, update them:

Before (3.x):

use DH\Auditor\User\UserInterface;

class MyUser implements UserInterface
{
public function getIdentifier(): ?string { return $this->id; }
public function getUsername(): ?string { return $this->name; }
}

After (4.0):

use DH\Auditor\User\UserInterface;

class MyUser implements UserInterface
{
public ?string $identifier { get => $this->id; }
public ?string $username { get => $this->name; }
}

🔡 UTF-8 Conversion Now Opt-In​

IMPORTANT

The automatic UTF-8 re-encoding of audit diffs has been removed from the default path.

What Changed​

Previously, auditor always called mb_convert_encoding() recursively on the entire diff array before persisting each audit entry. This guaranteed that any non-UTF-8 byte sequence in entity field values would be sanitized before being stored.

This pass now only runs when the utf8_convert option is explicitly enabled.

Who Is Affected​

You are affected if your application:

  • Uses a database connection that was not explicitly configured as UTF-8, and
  • Persists entity fields that may contain non-UTF-8 strings (e.g. data imported from ISO-8859-1 sources, legacy BLOBs, etc.)

Applications running on a modern stack (DBAL 4 + PHP 8.4 + UTF-8 database charset) are not affected — the conversion was already a no-op for them.

How to Fix​

Add 'utf8_convert' => true to your DoctrineProvider configuration:

use DH\Auditor\Provider\Doctrine\Configuration;

$configuration = new Configuration([
// ... other options ...
'utf8_convert' => true,
]);

Why the Default Is false​

DBAL 4 mandates PHP 8.4+ and enforces UTF-8 database connections. Traversing and re-encoding every diff array on every flush was unnecessary overhead for the vast majority of users. The option is preserved for applications that still process legacy non-UTF-8 data.

🆕 New extra_data Column​

Schema Change​

IMPORTANT

A new nullable JSON column extra_data has been added to all audit tables. Run the schema update command after upgrading.

php bin/console audit:schema:update --dump-sql   # Preview
php bin/console audit:schema:update --force # Apply

Two ways to populate extra_data​

Option 1 — Global provider (new in 4.0): set a callable on Configuration once and it runs automatically for every audit entry produced during a flush:

$configuration->setExtraDataProvider(function (): ?array {
return [
'ip' => $this->requestStack->getCurrentRequest()?->getClientIp(),
'role' => $this->security->getUser()?->getRoles(),
];
});

Option 2 — Per-entity listener: LifecycleEvent now exposes $event->entity (the audited object), allowing listeners to add entity-specific context:

#[AsEventListener(event: LifecycleEvent::class, priority: 10)]
final class AuditExtraDataListener
{
public function __invoke(LifecycleEvent $event): void
{
$payload = $event->getPayload();

if ($payload['entity'] === User::class && null !== $event->entity) {
$payload['extra_data'] = json_encode([
'department' => $event->entity->getDepartment(),
], JSON_THROW_ON_ERROR);
$event->setPayload($payload);
}
}
}

The provider runs first; the listener can override or extend its output. See the Extra Data guide for precedence rules, merging strategies, and caveats.

🔢 TransactionType Enum​

TIP

Transaction type constants have been replaced by a backed enum for better type safety.

Before (3.x)After (4.0)
Transaction::INSERTTransactionType::Insert->value
Transaction::UPDATETransactionType::Update->value
Transaction::REMOVETransactionType::Remove->value
Transaction::ASSOCIATETransactionType::Associate->value
Transaction::DISSOCIATETransactionType::Dissociate->value

Before (3.x):

use DH\Auditor\Model\Transaction;

if ($entry->getType() === Transaction::INSERT) {
// ...
}

After (4.0):

use DH\Auditor\Model\TransactionType;

if ($entry->type === TransactionType::Insert->value) {
// ...
}

🎵 Symfony Attributes​

Commands​

Console commands now use #[AsCommand] attribute:

#[AsCommand(
name: 'audit:schema:update',
description: 'Update audit tables structure',
)]
final class UpdateSchemaCommand extends Command

Event Listeners​

Event subscriber has been replaced by event listeners using #[AsEventListener]:

Before (3.x):

class AuditEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [LifecycleEvent::class => 'onAuditEvent'];
}
}

After (4.0):

#[AsEventListener(event: LifecycleEvent::class, method: 'onAuditEvent')]
final class AuditEventSubscriber
{
public function onAuditEvent(LifecycleEvent $event): void { }
}

🔄 Migration Example: getMigrateToSql​

Before (3.x):

use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;

$sqls = DoctrineHelper::getMigrateToSql($connection, $fromSchema, $toSchema);

After (4.0):

use Doctrine\DBAL\Schema\Comparator;

$platform = $connection->getDatabasePlatform();
$sqls = $platform->getAlterSchemaSQL(
(new Comparator($platform))->compareSchemas($fromSchema, $toSchema)
);

DoctrineHelper::getRealClassName()​

This method now only handles __CG__ proxies (Doctrine Common Gateway marker).

With PHP 8.4+, Doctrine ORM uses native lazy objects instead of proxy classes, so the previous proxy handling logic has been simplified.

Composer Scripts Changes​

The following composer scripts have been removed:

Removed ScriptPurpose
setup5Symfony 5.4 setup
setup6Symfony 6.4 setup
setup7Symfony 7.x setup

Use the new unified setup script instead:

composer setup

Internal Changes​

These changes are internal and should not affect most users, but are documented for completeness:

DoctrineHelper::setPrimaryKey()​

Now uses PrimaryKeyConstraint directly without fallback to deprecated setPrimaryKey() method.

DoctrineHelper::getReflectionPropertyValue()​

Now uses getPropertyAccessor() directly without fallback to deprecated getReflectionProperty() method.

PlatformHelper::getServerVersion()​

Simplified to use getNativeConnection() directly. The getWrappedConnection() fallback has been removed.

🚀 PHP 8.4+ Modernization Changes​

TransactionType Enum​

NOTE

Transaction type constants have been moved from Transaction model to a native PHP enum TransactionType.

Before (3.x):

use DH\Auditor\Model\Transaction;

$type = Transaction::INSERT;
$type = Transaction::UPDATE;
$type = Transaction::REMOVE;
$type = Transaction::ASSOCIATE;
$type = Transaction::DISSOCIATE;

After (4.0):

use DH\Auditor\Model\TransactionType;

// Using constants (recommended - backward compatible syntax)
$type = TransactionType::INSERT;
$type = TransactionType::UPDATE;
$type = TransactionType::REMOVE;
$type = TransactionType::ASSOCIATE;
$type = TransactionType::DISSOCIATE;

// Or using enum cases (when you need the enum instance)
$type = TransactionType::Insert;
$type = TransactionType::Update;
// etc.

Entry Model - Property Hooks​

The Entry model now uses PHP 8.4 property hooks. Getter methods have been replaced by direct property access.

Before (3.x)After (4.0)
$entry->getId()$entry->id
$entry->getType()$entry->type
$entry->getObjectId()$entry->objectId
$entry->getDiscriminator()$entry->discriminator
$entry->getTransactionHash()$entry->transactionHash
$entry->getUserId()$entry->userId
$entry->getUsername()$entry->username
$entry->getUserFqdn()$entry->userFqdn
$entry->getUserFirewall()$entry->userFirewall
$entry->getIp()$entry->ip
$entry->getCreatedAt()$entry->createdAt

Note: getDiffs() method is preserved as it contains business logic.

User Interface - Property Hooks​

The UserInterface now uses PHP 8.4 interface properties.

Before (3.x):

interface UserInterface
{
public function getIdentifier(): ?string;
public function getUsername(): ?string;
}

// Usage
$user->getIdentifier();
$user->getUsername();

After (4.0):

interface UserInterface
{
public ?string $identifier { get; }
public ?string $username { get; }
}

// Usage
$user->identifier;
$user->username;

Configuration - Asymmetric Visibility​

The Configuration class now uses asymmetric visibility for some properties.

Before (3.x)After (4.0)
$config->isEnabled()$config->enabled
$config->getTimezone()$config->timezone

Note: getUserProvider(), getSecurityProvider(), and getRoleChecker() methods are preserved.

Attribute Namespace Rename​

The Annotation namespace has been renamed to Attribute to better reflect PHP 8+ native attributes.

Before (3.x):

use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Ignore;
use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Security;
use DH\Auditor\Provider\Doctrine\Auditing\Annotation\AnnotationLoader;

After (4.0):

use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Auditable;
use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Ignore;
use DH\Auditor\Provider\Doctrine\Auditing\Attribute\Security;
use DH\Auditor\Provider\Doctrine\Auditing\Attribute\AttributeLoader;

Event Subscribers → Event Listeners​

Event subscribers now use Symfony's #[AsEventListener] attribute instead of EventSubscriberInterface.

Before (3.x):

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class AuditEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [LifecycleEvent::class => ['onAuditEvent', -1000000]];
}

public function onAuditEvent(LifecycleEvent $event): LifecycleEvent { /* ... */ }
}

After (4.0):

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(event: LifecycleEvent::class, priority: -1_000_000)]
class AuditEventSubscriber
{
public function __invoke(LifecycleEvent $event): LifecycleEvent { /* ... */ }
}

Console Commands with #[AsCommand]​

Console commands now use the #[AsCommand] attribute for metadata.

Before (3.x):

class CleanAuditLogsCommand extends Command
{
protected function configure(): void
{
$this
->setName('audit:clean')
->setDescription('Cleans audit tables')
// ...
}
}

After (4.0):

#[AsCommand(name: 'audit:clean', description: 'Cleans audit tables')]
class CleanAuditLogsCommand extends Command
{
protected function configure(): void
{
// Only options and arguments, no setName/setDescription
}
}

📋 Migration Steps​

1️⃣ Update PHP Version​

Ensure you're running PHP 8.4 or higher:

php -v
# PHP 8.4.x ...

2️⃣ Update Symfony Dependencies​

Update your composer.json:

{
"require": {
"symfony/framework-bundle": "^8.0"
}
}

3️⃣ Update Doctrine Dependencies​

{
"require": {
"doctrine/dbal": "^4.0",
"doctrine/orm": "^3.2"
}
}

4️⃣ Update auditor​

composer require damienharper/auditor:^4.0

5️⃣ Update Your Code​

Replace any removed method calls:

// Before
$schemaManager = DoctrineHelper::createSchemaManager($connection);
$schema = DoctrineHelper::introspectSchema($schemaManager);

// After
$schemaManager = $connection->createSchemaManager();
$schema = $schemaManager->introspectSchema();

6️⃣ Run Tests​

./vendor/bin/phpunit

7️⃣ Update Audit Schema​

IMPORTANT

After upgrading, ensure your audit tables are up to date.

php bin/console audit:schema:update --dump-sql
php bin/console audit:schema:update --force

🔧 Troubleshooting​

"Class not found" errors​

TIP

Ensure all dependencies are updated by running composer update.

Schema issues​

NOTE

Run the schema update command: php bin/console audit:schema:update --force

Test failures​

If tests fail after upgrading:

  1. Check PHPUnit version compatibility
  2. Review any mocked classes for API changes
  3. Check for deprecated method usage

❓ Need Help?​

If you encounter issues during the upgrade:

  1. Check the GitHub Issues
  2. Review the Release Notes
  3. Open a new issue with:
    • PHP version
    • Symfony version
    • Error messages
    • Steps to reproduce