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
| Package | 3.x Version | 4.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 Method | Replacement |
|---|---|
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\Auditable | DH\Auditor\Provider\Doctrine\Auditing\Attribute\Auditable |
DH\Auditor\Provider\Doctrine\Auditing\Annotation\Ignore | DH\Auditor\Provider\Doctrine\Auditing\Attribute\Ignore |
DH\Auditor\Provider\Doctrine\Auditing\Annotation\Security | DH\Auditor\Provider\Doctrine\Auditing\Attribute\Security |
DH\Auditor\Provider\Doctrine\Auditing\Annotation\AnnotationLoader | DH\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::INSERT | TransactionType::Insert->value |
Transaction::UPDATE | TransactionType::Update->value |
Transaction::REMOVE | TransactionType::Remove->value |
Transaction::ASSOCIATE | TransactionType::Associate->value |
Transaction::DISSOCIATE | TransactionType::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 Script | Purpose |
|---|---|
setup5 | Symfony 5.4 setup |
setup6 | Symfony 6.4 setup |
setup7 | Symfony 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(), andgetRoleChecker()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:
- Check PHPUnit version compatibility
- Review any mocked classes for API changes
- Check for deprecated method usage
❓ Need Help?
If you encounter issues during the upgrade:
- Check the GitHub Issues
- Review the Release Notes
- Open a new issue with:
- PHP version
- Symfony version
- Error messages
- Steps to reproduce