Install the Bundle (via Composer):
composer require bobv/entity-history-bundle
Note: Requires fruitcake/laravel-doctrine for Laravel-Doctrine integration.
Configure Doctrine Event Listeners:
Add to config/packages/doctrine.yaml:
doctrine:
orm:
event_listeners:
App\Doctrine\HistorySubscriber: ~
Annotate an Entity:
use Bobvandevijver\EntityHistoryBundle\Annotation\History;
/**
* @History
*/
#[ORM\Entity]
class User
{
// ...
}
First Use Case:
Test by creating/updating a User and verify the user_history table records changes:
$user = new User();
$user->name = 'John Doe';
$entityManager->persist($user);
$entityManager->flush();
// Check history
$history = $entityManager->getRepository('App\Entity\UserHistory');
$records = $history->findBy(['entityId' => $user->id]);
Tracking Changes:
@History (or configure via YAML/XML).HistorySubscriber to conditionally log changes:
public function getSubscribedEvents()
{
return [
'preUpdate' => ['onPreUpdate', 100], // High priority
'prePersist' => ['onPrePersist', 100],
];
}
Querying History:
$historyRepo = $entityManager->getRepository('App\Entity\UserHistory');
$history = $historyRepo->findBy(['entityId' => $user->id]);
$qb = $historyRepo->createQueryBuilder('h')
->where('h.entityId = :id')
->andWhere('h.revision >= :revision')
->setParameter('id', $user->id)
->setParameter('revision', 1);
Integrating with Laravel:
class UserService {
public function getUserHistory(User $user) {
return $this->historyRepo->findBy(['entityId' => $user->id]);
}
}
Resource classes to format history responses:
public function toArray($request, UserHistory $history) {
return [
'field' => $history->getField(),
'old_value' => $history->getOldValue(),
'new_value' => $history->getNewValue(),
'changed_at' => $history->getChangedAt()->format('Y-m-d H:i:s'),
];
}
Soft Deletes:
Gedmo/SoftDeleteable to log deletions:
# config/packages/doctrine.yaml
gedmo_listener:
softdeleteable: true
HistorySubscriber to add user/ip tracking:
public function onPreUpdate(LifecycleEventArgs $args) {
$entity = $args->getEntity();
$history = new UserHistory();
$history->setUserId(auth()->id()); // Laravel auth
$history->setIpAddress(request()->ip());
// ...
}
$entityManager->getConnection()->getConfiguration()->setSQLLogger(null);
$entityManager->flush();
$historyRepo = $this->createMock(HistoryRepository::class);
$historyRepo->method('findBy')->willReturn([$mockHistory]);
$entityManager->getRepository('App\Entity\UserHistory')->willReturn($historyRepo);
Event Priority Conflicts:
100) for HistorySubscriber.Circular References:
User ↔ Order).fetchAssociative or lazy-load history data.Performance Bottlenecks:
CREATE INDEX idx_user_history_entity_id_revision ON user_history(entity_id, revision);
Missing Deletes:
SoftDeleteable runs after history recording.doctrine.yaml:
event_listeners:
App\Doctrine\SoftDeleteSubscriber: [softdelete, 50] # Lower priority
App\Doctrine\HistorySubscriber: [history, 100] # Higher priority
Schema Migrations:
ALTER TABLE:
php bin/console doctrine:schema:update --force --complete
HistorySubscriber:
public function onPreUpdate(LifecycleEventArgs $args) {
\Log::debug('Recording history for', ['entity' => get_class($args->getEntity())]);
}
EventManager to inspect events:
$eventManager = $entityManager->getEventManager();
$eventManager->addEventListener([LifecycleEventArgs::class, 'preUpdate'], function ($args) {
\Log::info('PreUpdate event fired for', ['entity' => $args->getEntity()]);
});
@History is annotated or configured in YAML.revision field is auto-incremented and unique.Custom History Table: Override the default table name in configuration:
bobv_entity_history:
table_name: custom_entity_history
Field-Level Control: Exclude specific fields from history:
/**
* @History(excludeFields={"password", "apiToken"})
*/
class User { ... }
Post-Processing:
Add a postUpdate listener to enrich history records:
public function onPostUpdate(LifecycleEventArgs $args) {
$history = $args->getEntity()->getHistory();
$history->setProcessedAt(new \DateTime());
}
Async Writes: Offload history recording to a queue (Laravel Queues):
public function onPreUpdate(LifecycleEventArgs $args) {
HistoryQueue::dispatch($args->getEntity());
}
Queue worker:
class HistoryQueueJob implements ShouldQueue {
public function handle() {
$entityManager->persist($this->history);
$entityManager->flush();
}
}
fruitcake/laravel-doctrine to bridge Symfony’s EventDispatcher:
// app/Providers/DoctrineServiceProvider.php
public function register() {
$this->app->bind('doctrine', function () {
return Doctrine::createEntityManager();
});
}
Auth to history records:
$history->setUserId(auth()->check() ? auth()->id() : null);
$eventManager = $this->app->make('doctrine')->getEventManager();
$eventManager->addEventListener([LifecycleEventArgs::class, 'preUpdate'], fn($args) => $this->historyRecorded = true);
How can I help you explore Laravel packages today?