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.
Installation:
composer require fsi/property-observer:0.9.*
(Pin to a specific version to avoid dependency conflicts with modern Laravel/Symfony packages.)
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}";
}
Where to Look First:
doc/usage.md: For examples (e.g., tracking multiple properties, clearing state).src/FSi/Component/PropertyObserver/: Core classes (PropertyObserver, MultiplePropertyObserver).$observer = new PropertyObserver();
$observer->saveValue($object, 'propertyName');
if ($observer->hasChangedValue($object, 'propertyName')) {
// Handle change
}
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
}
}
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']);
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);
}
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
$this->app->bind(PropertyChangeService::class, function ($app) {
return new PropertyChangeService();
});
if ($observer->hasChangedValue($model, 'status')) {
event(new ModelUpdated($model, 'status'));
}
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.');
}
});
}
Private/Protected Properties:
symfony/property-access, which may fail on non-public properties.setAccessible(true) or expose getters/setters:
$observer->setPropertyAccessor(new \Symfony\Component\PropertyAccess\PropertyAccessorBuilder()
->getPropertyAccessor()
->enableMagicCall()
->enableExceptionOnInvalidIndex()
);
Memory Leaks:
MultiplePropertyObserver stores snapshots in memory. For long-running processes (e.g., CLI commands), call clear() frequently:
$observer->clear(); // Reset after batch processing
PHP 8.x Incompatibility:
symfony/property-access:2.x may throw errors with named arguments or union types.False Negatives:
__set), changes won’t be detected.enableMagicCall() on the property accessor.Laravel Caching Conflicts:
MultiplePropertyObserver across requests, stale data may persist.$savedValue = $observer->getSavedValue($object, 'property');
print_r($observer->getChangedValues($object));
$accessor = new \Symfony\Component\PropertyAccess\PropertyAccessorBuilder()
->getPropertyAccessor()
->enableExceptionOnInvalidIndex()
->enableMagicCall();
$observer->setPropertyAccessor($accessor);
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);
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;
}
}
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`
}
PropertyAccessor from symfony/property-access. To customize:
$observer->setPropertyAccessor($customAccessor);
Fork the Package:
symfony/property-access to v5.x for PHP 8.x support.composer.json:
"require": {
"symfony/property-access": "^5.4"
}
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};
}
}
Leverage Laravel’s Built-ins: For Eloquent models, use:
$originalValue = $model->getOriginal('attribute');
if ($originalValue !== $model->attribute) {
// Changed!
}
How can I help you explore Laravel packages today?