Installation:
composer require bpolaszek/doctrine-changeset
Ensure your project uses Doctrine ORM (Laravel Eloquent is not supported).
Service Registration:
Add the EntityTracker to your Laravel service container (e.g., in config/app.php under bindings or via a service provider):
$this->app->bind(EntityTracker::class, function ($app) {
return new EntityTracker($app->make(EntityManagerInterface::class));
});
First Use Case:
Inject EntityTracker into a service or controller and track changes on an entity:
use BenTools\DoctrineChangeSet\Tracker\EntityTracker;
public function updateBook(Book $book, EntityTracker $tracker) {
$book->setName('New Title');
if ($tracker->hasChanged($book, 'name')) {
// Handle the change (e.g., log, audit, or trigger events)
$changeSet = $tracker->getChangeSet($book, 'name');
logger()->info("Name changed from {$changeSet->from} to {$changeSet->to}");
}
}
Nested Object Tracking:
Use the #[TrackChanges] attribute on properties to track changes in nested objects:
use BenTools\DoctrineChangeSet\Attribute\TrackChanges;
class Author {
#[TrackChanges]
public AuthorDetails $details;
}
Change Detection:
Use hasChanged() to check if an entity or specific property has changed:
if ($tracker->hasChanged($entity)) {
// Entity has any changes
}
if ($tracker->hasChanged($entity, 'propertyName')) {
// Specific property changed
}
ChangeSet Retrieval: Fetch detailed change information for auditing or validation:
$changeSet = $tracker->getChangeSet($entity, 'propertyName');
$oldValue = $changeSet->from;
$newValue = $changeSet->to;
Conditional Logic:
Use ChangeSet methods to enforce business rules:
if ($changeSet->hadPreviousValue('active') && !$changeSet->hasNewValue('active')) {
// Handle deactivation logic
}
Nested Object Handling:
Track changes in nested objects by annotating properties with #[TrackChanges]:
class Order {
#[TrackChanges]
public OrderDetails $details;
}
$order->details->setStatus('shipped');
$tracker->hasChanged($order, 'details'); // true
Event Listeners:
Combine with Doctrine lifecycle events (e.g., preUpdate) to react to changes:
$em->getEventManager()->addEventListener(
\Doctrine\ORM\Events::preUpdate,
function ($event) {
$entity = $event->getObject();
$tracker = $event->getEntityManager()->getRepository()->getEntityTracker();
if ($tracker->hasChanged($entity)) {
// Custom logic before update
}
}
);
Audit Logging: Log changesets to a separate table for compliance or debugging:
$auditLog = new AuditLog();
$auditLog->entityClass = get_class($entity);
$auditLog->property = $propertyName;
$auditLog->oldValue = $changeSet->from;
$auditLog->newValue = $changeSet->to;
$em->persist($auditLog);
Validation: Validate changes before persisting:
$changeSet = $tracker->getChangeSet($entity, 'price');
if ($changeSet->hasNewValue() && $changeSet->to < 0) {
throw new \InvalidArgumentException("Price cannot be negative");
}
Testing:
Mock EntityTracker in unit tests to verify change detection logic:
$tracker = $this->createMock(EntityTracker::class);
$tracker->method('hasChanged')->with($entity, 'name')->willReturn(true);
EntityManager Scope:
The EntityTracker is tied to the EntityManager. Ensure you’re using the same EntityManager instance when checking changes (e.g., avoid mixing EntityManager instances in multi-DB setups).
Nested Object Limitations:
#[TrackChanges] only works for properties typed as object. Primitive types (e.g., string, int) or collections require manual tracking.
Change Detection Timing: Changes are detected after the property is modified. For example:
$entity->setName('foo');
$tracker->hasChanged($entity, 'name'); // true (detected after setName)
$entity->name = 'bar'; // Direct assignment may not trigger tracking unless EntityTracker is configured to handle it.
Performance:
Frequent calls to hasChanged() or getChangeSet() on large entities may impact performance. Cache results if checking the same properties repeatedly.
Doctrine Events:
Changesets are cleared after preFlush or postFlush events. If you need to persist changesets, do so in preUpdate/prePersist listeners.
Verify Tracking:
If hasChanged() returns false unexpectedly, ensure:
EntityManager is the same instance used to persist the entity.#[TrackChanges] is applied correctly.ChangeSet Inspection:
Use var_dump($changeSet) to debug ChangeSet objects:
$changeSet = $tracker->getChangeSet($entity, 'property');
var_dump($changeSet->from, $changeSet->to, $changeSet->getMetadata());
Clear Changes Manually:
If changes are not being cleared as expected, manually reset the UnitOfWork:
$em->getUnitOfWork()->clear($entity);
Custom ChangeSet Methods:
Extend the ChangeSet class to add domain-specific methods:
class CustomChangeSet extends \BenTools\DoctrineChangeSet\ChangeSet {
public function isPriceIncreased(): bool {
return $this->from < $this->to && $this->property === 'price';
}
}
Bulk Change Detection: For bulk operations, iterate over entities and collect changesets:
$changesets = [];
foreach ($entities as $entity) {
if ($tracker->hasChanged($entity)) {
$changesets[$entity->getId()] = $tracker->getChangeSet($entity);
}
}
Configuration:
If using Laravel’s service container, bind EntityTracker with a specific EntityManager:
$this->app->bind(EntityTracker::class, function ($app) {
return new EntityTracker($app->make('em.default')); // or 'em.secondary'
});
Laravel Eloquent Alternative:
While this package is Doctrine-specific, consider using Laravel’s built-in synchronizedToDatabase() or fresh() for Eloquent models if you’re not using raw Doctrine.
Testing Edge Cases: Test scenarios like:
$entity->property = null).How can I help you explore Laravel packages today?