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.
[!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.
[!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.
| Package | 3.x Version | 4.x Version |
|---|---|---|
| Doctrine DBAL | >= 3.2 | >= 4.0 |
| Doctrine ORM | >= 2.13 | >= 3.2 |
PHPUnit minimum version is now 12.0 (was 11.0 in 3.x)
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.
[!TIP] The
Annotationnamespace has been renamed toAttributeto 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;
}
[!TIP] PHP attributes have been promoted to the core
DH\Auditor\Attributenamespace 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\Auditable |
DH\Auditor\Attribute\Auditable |
DH\Auditor\Provider\Doctrine\Auditing\Attribute\Ignore |
DH\Auditor\Attribute\Ignore |
DH\Auditor\Provider\Doctrine\Auditing\Attribute\Security |
DH\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;
[!NOTE] The
Entrymodel 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.
The Configuration class now uses PHP 8.4 asymmetric visibility:
| Before (3.x) | After (4.0) |
|---|---|
$config->isEnabled() |
$config->enabled |
$config->getTimezone() |
$config->timezone |
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; }
}
[!IMPORTANT] The automatic UTF-8 re-encoding of audit diffs has been removed from the default path.
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.
You are affected if your application:
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.
Add 'utf8_convert' => true to your DoctrineProvider configuration:
use DH\Auditor\Provider\Doctrine\Configuration;
$configuration = new Configuration([
// ... other options ...
'utf8_convert' => true,
]);
falseDBAL 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.
extra_data Column[!IMPORTANT] A new nullable JSON column
extra_datahas 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
extra_dataOption 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.
[!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) {
// ...
}
Console commands now use #[AsCommand] attribute:
#[AsCommand(
name: 'audit:schema:update',
description: 'Update audit tables structure',
)]
final class UpdateSchemaCommand extends Command
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 { }
}
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)
);
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.
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
These changes are internal and should not affect most users, but are documented for completeness:
Now uses PrimaryKeyConstraint directly without fallback to deprecated setPrimaryKey() method.
Now uses getPropertyAccessor() directly without fallback to deprecated getReflectionProperty() method.
Simplified to use getNativeConnection() directly. The getWrappedConnection() fallback has been removed.
[!NOTE] Transaction type constants have been moved from
Transactionmodel to a native PHP enumTransactionType.
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.
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.
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;
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.
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 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 { /* ... */ }
}
#[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
}
}
Ensure you're running PHP 8.4 or higher:
php -v
# PHP 8.4.x ...
Update your composer.json:
{
"require": {
"symfony/framework-bundle": "^8.0"
}
}
{
"require": {
"doctrine/dbal": "^4.0",
"doctrine/orm": "^3.2"
}
}
composer require damienharper/auditor:^4.0
Replace any removed method calls:
// Before
$schemaManager = DoctrineHelper::createSchemaManager($connection);
$schema = DoctrineHelper::introspectSchema($schemaManager);
// After
$schemaManager = $connection->createSchemaManager();
$schema = $schemaManager->introspectSchema();
./vendor/bin/phpunit
[!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
[!TIP] Ensure all dependencies are updated by running
composer update.
[!NOTE] Run the schema update command:
php bin/console audit:schema:update --force
If tests fail after upgrading:
If you encounter issues during the upgrade:
How can I help you explore Laravel packages today?