adt / doctrine-loggable
Logging of changes in Doctrine entities
Requires
- php: ^8.0|^8.1|^8.2
- doctrine/common: ^3.5
- doctrine/orm: ^3.0
- nette/di: ^3.0
- nette/security: ^3.2
- nette/utils: ^2.5|^3.0|^4.0
Requires (Dev)
- phpunit/phpunit: ^11.5 | ^12.0 | ^13.0
- symfony/cache: ^6.0 | ^7.0
- symfony/console: ^6.0 | ^7.0
- symfony/var-exporter: ^6.4 | ^7.0
Suggests
- symfony/console: To run doctrine-loggable:convert-legacy-change-sets, the migration of change sets stored by version 3
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v4.0
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.6
- v1.5.5
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5
- v1.4.1
- v1.4
- v1.3.2
- v1.3.1
- v1.3
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2
- v1.1.1
- v1.1
- v1.0.1
- v1.0
- 0.2.5
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- dev-feature/json-change-sets
- dev-f-postgresql
- dev-old-php-version
- dev-test
This package is auto-updated.
Last update: 2026-08-31 11:14:18 UTC
README
Logging of changes in Doctrine entities. Every change is stored as one change_log row with the
change set serialized to JSON, so the log is readable straight from the database.
Installation
-
Install via composer:
composer require adt/doctrine-loggable
-
Register this extension in your config.neon:
extensions: - ADT\DoctrineLoggable\DI\LoggableExtension
-
Do database migrations
-
Add attributes to entities you wish to log
<?php use Doctrine\ORM\Mapping as ORM; use ADT\DoctrineLoggable\Attributes as ADA; #[ORM\Entity] #[ADA\LoggableEntity] class User { #[ORM\Column(nullable: true)] #[ADA\LoggableProperty] protected ?string $firstname = null; #[ORM\ManyToMany(targetEntity: Role::class, inversedBy: 'users')] #[ADA\LoggableProperty] protected Collection $roles; } #[ORM\Entity] #[ADA\LoggableIdentification(fields: ['name'])] class Role { #[ORM\Column] protected string $name; #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'roles')] protected Collection $users; }
Stored format
The change_set column is a JSON column handled by the change_set DBAL type, which the extension
registers for you. ChangeLog::getChangeSet() returns the usual ChangeSet object graph.
{
"version": 1,
"action": "edit",
"entity": {
"class": "App\\Model\\Entities\\Product",
"id": "42",
"identification": { "name": "Pivo 12°" }
},
"properties": {
"name": { "type": "scalar", "old": "Pivo", "new": "Pivo 12°" },
"price": { "type": "scalar", "old": 39.0, "new": 45.0 },
"validFrom": {
"type": "scalar",
"old": null,
"new": { "@type": "datetime", "class": "DateTimeImmutable", "value": "2026-08-27T12:00:00+02:00" }
},
"category": {
"type": "toOne",
"old": { "class": "App\\Model\\Entities\\Category", "id": "3", "identification": { "name": "Nápoje" } },
"new": { "class": "App\\Model\\Entities\\Category", "id": "7", "identification": { "name": "Pivo" } },
"changeSet": null
},
"tags": {
"type": "toMany",
"added": [ { "class": "App\\Model\\Entities\\Tag", "id": "9", "identification": { "name": "akce" } } ],
"removed": [],
"changeSets": []
}
}
}
A value position holds either a plain JSON scalar or an envelope object with the @type key.
Arrays never appear raw in a value position, so @type is always an unambiguous marker.
Built-in envelope types are datetime, enum, array, binary, float (for NAN and INF)
and object as the last resort fallback.
The property type is a scalar/toOne/toMany discriminator rather than a class name, so the
classes of this library can be moved or renamed without breaking existing logs.
The change set graph may contain cycles. A change set referenced more than once gets a $id and
every further occurrence is written as {"$ref": id}. Change sets referenced just once, which is
almost always the case, carry no ids at all.
Custom value types
Values coming from custom Doctrine types (money, uuid, embeddables) end up in the object envelope,
which keeps their readable representation but does not restore the original object. Register a
handler to make the round trip lossless:
use ADT\DoctrineLoggable\Serializer\ValueHandler; use ADT\DoctrineLoggable\Serializer\ValueSerializer; class MoneyHandler implements ValueHandler { public function getType(): string { return 'money'; } public function supports(mixed $value): bool { return $value instanceof Money; } public function encode(mixed $value, ValueSerializer $serializer): array { return ['amount' => $value->getAmount(), 'currency' => $value->getCurrency()]; } public function decode(array $data, ValueSerializer $serializer): Money { return new Money($data['amount'], $data['currency']); } }
doctrineLoggable: valueHandlers: - App\Log\MoneyHandler
Handlers registered this way are tried before the built-in ones, so they can override them.
Upgrading from 3.x
The change_set column changes from LONGBLOB holding a PHP serialize() payload to a JSON
column. Convert the existing rows first, while the column is still a BLOB:
php bin/console doctrine-loggable:convert-legacy-change-sets --dry-run php bin/console doctrine-loggable:convert-legacy-change-sets
The command reads and rewrites the rows with plain SQL, so the change_set DBAL type never sees a
legacy payload. It commits per batch (--batch-size, 500 by default), recognises rows that already
hold JSON and leaves them alone, so it is safe to run again. A row that fails to convert is
reported and left untouched, and the command exits with a failure so you do not change the column
type on top of data that did not make it.
Only when it reports no failures:
ALTER TABLE change_log MODIFY change_set JSON NOT NULL;
symfony/console is optional. Without it the command is not registered and you can drive
ADT\DoctrineLoggable\Service\LegacyChangeSetConverter yourself.
Adt\DoctrineLoggable\ChangeSet\* is now declared as ADT\DoctrineLoggable\ChangeSet\*, which is
what the PSR-4 prefix always said. The old casing keeps autoloading, PHP treats both as the same
class.
Tests
composer install vendor/bin/phpunit