Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Entity History Bundle Laravel Package

bobv/entity-history-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup for Laravel (Doctrine ORM)

  1. Install the Bundle (via Composer):

    composer require bobv/entity-history-bundle
    

    Note: Requires fruitcake/laravel-doctrine for Laravel-Doctrine integration.

  2. Configure Doctrine Event Listeners: Add to config/packages/doctrine.yaml:

    doctrine:
        orm:
            event_listeners:
                App\Doctrine\HistorySubscriber: ~
    
  3. Annotate an Entity:

    use Bobvandevijver\EntityHistoryBundle\Annotation\History;
    
    /**
     * @History
     */
    #[ORM\Entity]
    class User
    {
        // ...
    }
    
  4. 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]);
    

Implementation Patterns

Workflows

  1. Tracking Changes:

    • Automatic: Annotate entities with @History (or configure via YAML/XML).
    • Manual: Extend HistorySubscriber to conditionally log changes:
      public function getSubscribedEvents()
      {
          return [
              'preUpdate' => ['onPreUpdate', 100], // High priority
              'prePersist' => ['onPrePersist', 100],
          ];
      }
      
  2. Querying History:

    • Fetch all changes for an entity:
      $historyRepo = $entityManager->getRepository('App\Entity\UserHistory');
      $history = $historyRepo->findBy(['entityId' => $user->id]);
      
    • Filter by field/date:
      $qb = $historyRepo->createQueryBuilder('h')
          ->where('h.entityId = :id')
          ->andWhere('h.revision >= :revision')
          ->setParameter('id', $user->id)
          ->setParameter('revision', 1);
      
  3. Integrating with Laravel:

    • Service Layer: Wrap Doctrine calls in a service to expose history methods:
      class UserService {
          public function getUserHistory(User $user) {
              return $this->historyRepo->findBy(['entityId' => $user->id]);
          }
      }
      
    • API Resources: Use Laravel’s 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'),
          ];
      }
      
  4. Soft Deletes:

    • Pair with Gedmo/SoftDeleteable to log deletions:
      # config/packages/doctrine.yaml
      gedmo_listener:
          softdeleteable: true
      

Integration Tips

  • Custom Metadata: Extend 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());
        // ...
    }
    
  • Bulk Operations: Disable history for bulk inserts/updates:
    $entityManager->getConnection()->getConfiguration()->setSQLLogger(null);
    $entityManager->flush();
    
  • Testing: Mock history in PHPUnit:
    $historyRepo = $this->createMock(HistoryRepository::class);
    $historyRepo->method('findBy')->willReturn([$mockHistory]);
    $entityManager->getRepository('App\Entity\UserHistory')->willReturn($historyRepo);
    

Gotchas and Tips

Pitfalls

  1. Event Priority Conflicts:

    • If other listeners modify entities after history recording, values may be stale.
    • Fix: Set high priority (100) for HistorySubscriber.
  2. Circular References:

    • History tables may fail if entities reference each other (e.g., UserOrder).
    • Fix: Use fetchAssociative or lazy-load history data.
  3. Performance Bottlenecks:

    • History queries slow down with large datasets.
    • Fix: Add indexes:
      CREATE INDEX idx_user_history_entity_id_revision ON user_history(entity_id, revision);
      
  4. Missing Deletes:

    • Soft deletes may not log if SoftDeleteable runs after history recording.
    • Fix: Reorder listeners in doctrine.yaml:
      event_listeners:
          App\Doctrine\SoftDeleteSubscriber: [softdelete, 50] # Lower priority
          App\Doctrine\HistorySubscriber: [history, 100]     # Higher priority
      
  5. Schema Migrations:

    • Adding new fields to history tables requires careful ALTER TABLE:
      php bin/console doctrine:schema:update --force --complete
      

Debugging

  • Check Listener Execution: Add logging to HistorySubscriber:
    public function onPreUpdate(LifecycleEventArgs $args) {
        \Log::debug('Recording history for', ['entity' => get_class($args->getEntity())]);
    }
    
  • Verify History Records: Use Doctrine’s EventManager to inspect events:
    $eventManager = $entityManager->getEventManager();
    $eventManager->addEventListener([LifecycleEventArgs::class, 'preUpdate'], function ($args) {
        \Log::info('PreUpdate event fired for', ['entity' => $args->getEntity()]);
    });
    
  • Common Issues:
    • No history records: Check if @History is annotated or configured in YAML.
    • Duplicate entries: Ensure revision field is auto-incremented and unique.
    • Stale data: Confirm no other listeners modify entities post-history.

Extension Points

  1. Custom History Table: Override the default table name in configuration:

    bobv_entity_history:
        table_name: custom_entity_history
    
  2. Field-Level Control: Exclude specific fields from history:

    /**
     * @History(excludeFields={"password", "apiToken"})
     */
    class User { ... }
    
  3. Post-Processing: Add a postUpdate listener to enrich history records:

    public function onPostUpdate(LifecycleEventArgs $args) {
        $history = $args->getEntity()->getHistory();
        $history->setProcessedAt(new \DateTime());
    }
    
  4. 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();
        }
    }
    

Laravel-Specific Quirks

  • DoctrineBridge: Use fruitcake/laravel-doctrine to bridge Symfony’s EventDispatcher:
    // app/Providers/DoctrineServiceProvider.php
    public function register() {
        $this->app->bind('doctrine', function () {
            return Doctrine::createEntityManager();
        });
    }
    
  • Auth Integration: Pass Laravel’s Auth to history records:
    $history->setUserId(auth()->check() ? auth()->id() : null);
    
  • Testing: Mock Doctrine events in Pest:
    $eventManager = $this->app->make('doctrine')->getEventManager();
    $eventManager->addEventListener([LifecycleEventArgs::class, 'preUpdate'], fn($args) => $this->historyRecorded = true);
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver