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

Property Observer Laravel Package

fsi/property-observer

DEPRECATED: This package will not receive updates and may be removed. FSi PropertyObserver tracks changes in registered object properties so you can detect if a value changed since the last check.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require fsi/property-observer:0.9.*
    

    (Pin to a specific version to avoid dependency conflicts with modern Laravel/Symfony packages.)

  2. First Use Case: Track changes to a single property in a plain PHP object:

    use FSi\Component\PropertyObserver\PropertyObserver;
    
    $user = new class { public $name = 'John'; };
    $observer = new PropertyObserver();
    
    // Save initial state
    $observer->saveValue($user, 'name');
    
    // Later...
    $user->name = 'Jane';
    if ($observer->hasChangedValue($user, 'name')) {
        // Property changed!
        echo "Name updated to: {$user->name}";
    }
    
  3. Where to Look First:

    • README.md: For basic installation and usage.
    • doc/usage.md: For examples (e.g., tracking multiple properties, clearing state).
    • src/FSi/Component/PropertyObserver/: Core classes (PropertyObserver, MultiplePropertyObserver).

Implementation Patterns

Core Workflows

1. Single Property Tracking

$observer = new PropertyObserver();
$observer->saveValue($object, 'propertyName');
if ($observer->hasChangedValue($object, 'propertyName')) {
    // Handle change
}

2. Multiple Properties (Batch Tracking)

Use MultiplePropertyObserver for efficiency:

$observer = new MultiplePropertyObserver();
$observer->saveValues($object, ['prop1', 'prop2']);
if ($observer->hasChangedValues($object)) {
    foreach ($observer->getChangedValues($object) as $property => $oldValue) {
        // $oldValue is the original value
    }
}

3. Laravel Integration (Non-Eloquent Objects)

Wrap in a service for reusability:

class PropertyChangeService
{
    public function trackChanges(object $object, string|array $properties): array
    {
        $observer = new MultiplePropertyObserver();
        $observer->saveValues($object, (array)$properties);
        return $observer->getChangedValues($object);
    }
}

Usage in a Controller:

$service = app(PropertyChangeService::class);
$changes = $service->trackChanges($dto, ['field1', 'field2']);

4. Audit Logging

Combine with Laravel’s logging:

$observer = new PropertyObserver();
$observer->saveValue($user, 'email');

if ($observer->hasChangedValue($user, 'email')) {
    \Log::info('Email changed from ' . $observer->getSavedValue($user, 'email') .
        ' to ' . $user->email);
}

5. Optimistic Locking

Reset state after each request:

$observer = new MultiplePropertyObserver();
$observer->saveValues($entity, ['version', 'data']);

// Later...
if ($observer->hasChangedValues($entity)) {
    throw new \RuntimeException('Concurrent modification detected!');
}
$observer->clear(); // Reset for next request

Laravel-Specific Tips

  • Service Provider Binding:
    $this->app->bind(PropertyChangeService::class, function ($app) {
        return new PropertyChangeService();
    });
    
  • Event Integration:
    if ($observer->hasChangedValue($model, 'status')) {
        event(new ModelUpdated($model, 'status'));
    }
    
  • Form Request Validation:
    public function rules()
    {
        return [
            'email' => 'required|email',
            // Track changes for custom validation
        ];
    }
    
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            $observer = new PropertyObserver();
            $observer->saveValue($this->user, 'email');
            if ($observer->hasChangedValue($this->user, 'email')) {
                $validator->errors()->add('email', 'Email must match original value.');
            }
        });
    }
    

Gotchas and Tips

Pitfalls

  1. Private/Protected Properties:

    • The package uses symfony/property-access, which may fail on non-public properties.
    • Fix: Use setAccessible(true) or expose getters/setters:
      $observer->setPropertyAccessor(new \Symfony\Component\PropertyAccess\PropertyAccessorBuilder()
          ->getPropertyAccessor()
          ->enableMagicCall()
          ->enableExceptionOnInvalidIndex()
      );
      
  2. Memory Leaks:

    • MultiplePropertyObserver stores snapshots in memory. For long-running processes (e.g., CLI commands), call clear() frequently:
      $observer->clear(); // Reset after batch processing
      
  3. PHP 8.x Incompatibility:

    • symfony/property-access:2.x may throw errors with named arguments or union types.
    • Workaround: Downgrade PHP to 7.4 or fork the package to update dependencies.
  4. False Negatives:

    • If properties are modified via magic methods (e.g., __set), changes won’t be detected.
    • Solution: Use enableMagicCall() on the property accessor.
  5. Laravel Caching Conflicts:

    • If using MultiplePropertyObserver across requests, stale data may persist.
    • Fix: Clear state per request or use a request-scoped service.

Debugging Tips

  • Check Saved Values:
    $savedValue = $observer->getSavedValue($object, 'property');
    
  • Inspect Changed Properties:
    print_r($observer->getChangedValues($object));
    
  • Enable Debugging for Property Access:
    $accessor = new \Symfony\Component\PropertyAccess\PropertyAccessorBuilder()
        ->getPropertyAccessor()
        ->enableExceptionOnInvalidIndex()
        ->enableMagicCall();
    $observer->setPropertyAccessor($accessor);
    

Extension Points

  1. Custom Property Access: Override the property accessor to handle edge cases:

    $accessor = new \Symfony\Component\PropertyAccess\PropertyAccessorBuilder()
        ->getPropertyAccessor()
        ->setExtractor(function ($object, $property) {
            // Custom logic for dynamic properties
            return $object->{$property} ?? null;
        });
    $observer->setPropertyAccessor($accessor);
    
  2. Event-Based Changes: Extend the observer to trigger Laravel events:

    class EventTriggeringObserver extends PropertyObserver
    {
        public function hasChangedValue($object, $property)
        {
            $hasChanged = parent::hasChangedValue($object, $property);
            if ($hasChanged) {
                event(new PropertyChanged($object, $property));
            }
            return $hasChanged;
        }
    }
    
  3. Database Integration: Use with Eloquent models for hybrid tracking:

    $observer = new PropertyObserver();
    $observer->saveValue($model, 'custom_field');
    
    if ($observer->hasChangedValue($model, 'custom_field')) {
        $model->touch(); // Update `updated_at`
    }
    

Configuration Quirks

  • Default Property Accessor: The package uses a default PropertyAccessor from symfony/property-access. To customize:
    $observer->setPropertyAccessor($customAccessor);
    
  • Case Sensitivity: Property names are case-sensitive. Ensure consistency when saving/checking values.

Migration Strategies

  1. Fork the Package:

    • Clone the repo and update symfony/property-access to v5.x for PHP 8.x support.
    • Example composer.json:
      "require": {
          "symfony/property-access": "^5.4"
      }
      
  2. Replace with Custom Logic: For simple cases, manually track changes:

    class ManualObserver
    {
        private $savedValues = [];
    
        public function saveValue(object $object, string $property, $value)
        {
            $this->savedValues[spl_object_hash($object) . $property] = $value;
        }
    
        public function hasChangedValue(object $object, string $property)
        {
            $key = spl_object_hash($object) . $property;
            return !isset($this->savedValues[$key]) ||
                   $this->savedValues[$key] !== $object->{$property};
        }
    }
    
  3. Leverage Laravel’s Built-ins: For Eloquent models, use:

    $originalValue = $model->getOriginal('attribute');
    if ($originalValue !== $model->attribute) {
        // Changed!
    }
    
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.
directorytree/privacy-filter-classifier
directorytree/privacy-filter
datacore/hub-sdk
develia/commons
cuci/prototurk-sdk
cuci/prototurk-sdk-symfony
develia/geo-bundle
dreamzy/livewire-charts
touchestate-sdk/php-sdk
22h/doctrine-garbage-collection-bundle
agtp/agtp-php
agtp/mod-php
splash/sonata-admin
splash/metadata
splash/openapi
splash/scopes
splash/toolkit
testo/output-teamcity
testo/bridge-symfony
spatie/flare-daemon-runtime