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

Doctrine Watcher Laravel Package

bentools/doctrine-watcher

Monitor Doctrine entity inserts and updates with a lightweight event subscriber. Watch specific classes and properties, get a changeset with old/new values or additions/removals, and run callbacks to react to changes (e.g., email or roles updates).

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Package:
    composer require bentools/doctrine-watcher:0.2.*
    
  2. Register the Watcher: In your Laravel service provider (e.g., AppServiceProvider):
    use BenTools\DoctrineWatcher\Watcher\DoctrineWatcher;
    
    public function boot()
    {
        $em = $this->app->make('doctrine.orm.entity_manager');
        $watcher = new DoctrineWatcher();
        $em->getEventManager()->addEventSubscriber($watcher);
    }
    
  3. Add a Basic Watch:
    $watcher->watch(
        App\Entity\User::class,
        'email',
        function ($changeset, $operationType, $user) {
            if ($changeset->hasChanges()) {
                logger()->info("Email changed from {$changeset->getOldValue()} to {$changeset->getNewValue()}");
            }
        }
    );
    

First Use Case: Audit Logging

Track changes to sensitive fields (e.g., User::roles) and log them:

$watcher->watch(
    App\Entity\User::class,
    'roles',
    function ($changeset, $operationType, $user) {
        if ($changeset->hasAdditions()) {
            AuditLog::create([
                'entity' => 'User',
                'field' => 'roles',
                'old_value' => $changeset->getOldValue(),
                'new_value' => $changeset->getAdditions(),
                'user_id' => $user->getId(),
            ]);
        }
    }
);

Implementation Patterns

Core Workflow

  1. Register the Watcher:

    • Subscribe to Doctrine’s EventManager once (e.g., in a service provider).
    • Example:
      $watcher = new DoctrineWatcher(['trigger_on_persist' => true]);
      $em->getEventManager()->addEventSubscriber($watcher);
      
  2. Define Watches:

    • Use watch() for each entity/property pair.
    • Pattern: Group related watches (e.g., all User property watches in one place).
      $watcher->watch(User::class, 'email', $emailCallback);
      $watcher->watch(User::class, 'status', $statusCallback);
      
  3. Handle Callbacks:

    • Structure: Use a dedicated service/class for callbacks (e.g., UserChangeHandler).
      $watcher->watch(
          User::class,
          'email',
          [new UserChangeHandler(), 'handleEmailChange']
      );
      
    • Avoid Logic Bloat: Keep callbacks focused (e.g., logging, notifications). Offload complex logic to services.
  4. Configure Per-Watch:

    • Override defaults (e.g., trigger_on_persist) per property:
      $watcher->watch(
          User::class,
          'email',
          $callback,
          ['trigger_on_persist' => true, 'trigger_when_no_changes' => false]
      );
      

Integration Tips

  • Laravel-Specific:

    • Service Container: Bind the watcher to the container for DI:
      $this->app->singleton(DoctrineWatcher::class, function ($app) {
          $watcher = new DoctrineWatcher();
          $em = $app->make('doctrine.orm.entity_manager');
          $em->getEventManager()->addEventSubscriber($watcher);
          return $watcher;
      });
      
    • Config: Store watched properties in config/doctrine-watcher.php:
      'watches' => [
          App\Entity\User::class => [
              'email' => ['callback' => 'App\Services\UserChangeHandler@logEmailChange'],
              'roles' => ['callback' => 'App\Services\UserChangeHandler@logRolesChange'],
          ],
      ],
      
    • Dynamic Registration: Load watches from config in boot():
      $watcher = $this->app->make(DoctrineWatcher::class);
      foreach (config('doctrine-watcher.watches') as $entity => $properties) {
          foreach ($properties as $property => $config) {
              $watcher->watch($entity, $property, $config['callback']);
          }
      }
      
  • Testing:

    • Mock the Watcher: Use Doctrine\Common\EventManager mocks to test callbacks:
      $eventManager = $this->createMock(EventManager::class);
      $watcher = new DoctrineWatcher();
      $eventManager->expects($this->once())
          ->method('addEventSubscriber')
          ->with($watcher);
      
    • Entity Tests: Test watches by triggering postPersist/postUpdate:
      $em = $this->getEntityManager();
      $user = new User();
      $user->setEmail('new@example.com');
      $em->persist($user);
      $em->flush(); // Triggers watcher callbacks
      
  • Performance:

    • Bulk Operations: Disable watches during bulk inserts/updates:
      $watcher->disable(); // Temporarily pause all watches
      // Bulk operations...
      $watcher->enable();
      
    • Selective Disabling: Unregister watches for specific properties:
      $watcher->unwatch(User::class, 'email');
      

Gotchas and Tips

Pitfalls

  1. Event Timing:

    • Post-Operation Only: Callbacks run after persist()/flush(), so entities are already in the database. Avoid relying on pre-save state.
    • Transaction Safety: Callbacks execute within the same transaction. Throwing exceptions will roll back the operation.
  2. Property Types:

    • Collections/Arrays: The package tracks scalar changes (e.g., array_push/array_pop). For complex collections, implement custom logic:
      $watcher->watch(
          User::class,
          'roles',
          function ($changeset, $operationType, $user) {
              if ($changeset->hasChanges()) {
                  $oldRoles = $changeset->getOldValue() ?? [];
                  $newRoles = $changeset->getNewValue() ?? [];
                  // Custom diff logic...
              }
          }
      );
      
    • Nested Objects: Not supported. Use serialize()/unserialize() for deep comparison if needed.
  3. Configuration Overrides:

    • Global vs. Per-Watch: Defaults (e.g., trigger_on_persist) can be set globally or per-watch. Per-watch settings override globals.
    • Boolean Pitfall: trigger_when_no_changes defaults to false. Forgetting this may cause unnecessary callback invocations.
  4. EntityManager Scope:

    • Multiple EMs: If using multiple EntityManager instances (e.g., in Symfony), register the watcher on each EventManager.
    • Lazy Loading: Ensure entities are fully loaded before watching (e.g., avoid watching User::profile if profile is lazy-loaded).
  5. PHP 8+ Issues:

    • Strict Types: Callbacks may need type hints for $changeset and $operationType:
      function ($changeset, string $operationType, User $user)
      
    • Named Arguments: The package uses positional arguments. Avoid relying on named args in callbacks.

Debugging

  1. Callback Not Triggering:

    • Check Registration: Verify the watcher is subscribed to the EventManager:
      $listeners = $em->getEventManager()->getListeners();
      // Look for 'postPersist'/'postUpdate' entries with your watcher.
      
    • Entity/Property Mismatch: Ensure the fully qualified class name matches (e.g., App\Entity\User, not User).
    • Operation Type: Confirm the operation (INSERT/UPDATE) matches your watcher’s config (e.g., trigger_on_persist).
  2. Changeset Data Issues:

    • Old/New Values: For INSERT operations, getOldValue() may return null. Handle gracefully:
      $oldValue = $changeset->getOldValue() ?? 'N/A';
      
    • Collection Changes: Use hasAdditions()/hasRemovals() for arrays, not hasChanges():
      if ($changeset->hasAdditions()) {
          $added = $changeset->getAdditions(); // New items only
      }
      
  3. Performance Bottlenecks:

    • Callback Overhead: Profile slow operations. Use microtime() in callbacks to identify lag:
      $start = microtime(true);
      // Callback logic...
      logger()->debug("Callback took " . (microtime(true) - $start) . "s");
      
    • Memory Leaks: Avoid storing large data in callbacks (e.g., cache invalidation keys).

Tips

  1. **Idemp
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.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
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