daimos/entity-changes-fetcher
Small PHP service that detects and reports changes made to an entity by comparing values before and after updates. Useful for auditing, logging, change tracking, and syncing—returns a structured list of modified fields and their old/new values.
Installation Add the package via Composer:
composer require daimos/entity-changes-fetcher
Publish the config (if needed):
php artisan vendor:publish --provider="Daimos\EntityChangesFetcher\EntityChangesFetcherServiceProvider"
Basic Usage
Inject the EntityChangesFetcher service into your controller or service:
use Daimos\EntityChangesFetcher\EntityChangesFetcher;
public function __construct(private EntityChangesFetcher $changesFetcher) {}
First Use Case: Detecting Changes Fetch changes between two instances of an Eloquent model:
$original = User::find(1);
$updated = User::find(1)->update(['name' => 'New Name']);
$changes = $this->changesFetcher->getChanges($original, $updated);
Output:
[
'name' => [
'old' => 'Old Name',
'new' => 'New Name',
],
]
Model Updates
Use in update methods to log or audit changes:
public function update(Request $request, User $user)
{
$original = $user->fresh();
$user->update($request->validated());
$changes = $this->changesFetcher->getChanges($original, $user->fresh());
// Log changes to a dedicated table or service
$this->logChanges($changes);
return $user;
}
Form Request Validation Compare submitted data with existing data to enforce rules:
public function validateChanges(Request $request, User $user)
{
$changes = $this->changesFetcher->getChanges(
$user->fresh(),
new User($request->all())
);
if ($changes['email'] && !str_contains($changes['email']['new'], '@company.com')) {
throw ValidationException::withMessages(['email' => 'Email must be company domain.']);
}
}
Event Listeners
Attach to updated events to trigger side effects:
public function handle(Updated $event)
{
$changes = $this->changesFetcher->getChanges(
$event->model->fresh(['deleted_at']), // Exclude soft-deletes if needed
$event->model
);
// Dispatch a notification or update related models
event(new EntityUpdated($event->model, $changes));
}
$this->changesFetcher->getChanges($original, $updated, ['full_name']);
$postChanges = $this->changesFetcher->getChanges($originalPost, $updatedPost);
$authorChanges = $this->changesFetcher->getChanges($originalPost->author, $updatedPost->author);
deleted_at from comparisons if using soft deletes:
$this->changesFetcher->getChanges($original, $updated, [], ['deleted_at']);
Timestamps
The fetcher may include created_at/updated_at in changes by default. Exclude them explicitly:
$changes = $this->changesFetcher->getChanges($original, $updated, [], ['timestamps']);
Casts and Accessors
Custom casts (e.g., date: 'Y-m-d') or accessors may not reflect raw attribute changes. Fetch raw attributes if needed:
$original->getAttributes(); // Raw attributes
Circular References
Avoid infinite loops when comparing nested relationships with circular references (e.g., User->posts->author->user).
Mass Assignment
Changes from fill() or update() may not match if mass assignment protection is enabled. Use forceFill() or $model->setRawAttributes().
$original and $updated attributes to debug discrepancies:
\Log::debug('Original:', $original->getAttributes());
\Log::debug('Updated:', $updated->getAttributes());
$original->setMutator('attribute', null);
Custom Comparators Override the default comparison logic by extending the fetcher:
class CustomChangesFetcher extends EntityChangesFetcher
{
protected function compareValues($old, $new)
{
// Custom logic (e.g., ignore case for strings)
return strtolower($old) !== strtolower($new);
}
}
Event-Based Changes Hook into Laravel’s events to trigger the fetcher automatically:
Event::listen('eloquent.updated', function ($model) {
$changes = app(EntityChangesFetcher::class)->getChanges(
$model->fresh(['deleted_at']),
$model
);
// Handle changes
});
Database-Level Triggers
For critical systems, combine with database triggers (e.g., PostgreSQL BEFORE UPDATE) to ensure no changes are missed.
fresh() in Loops
Fetching fresh instances in loops (e.g., bulk updates) can be expensive. Cache original instances or use transactions:
DB::transaction(function () {
$originals = User::whereIn('id', [1, 2, 3])->get(['id', 'name']);
foreach ($originals as $user) {
$user->update(['name' => 'Updated']);
$changes = $this->changesFetcher->getChanges($user, $user->fresh());
// Process changes
}
});
How can I help you explore Laravel packages today?