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;
}

📊 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