philiprehberger/laravel-model-diff

Track and display structured differences between Eloquent model versions with human-readable labels

Maintainers

Package info

github.com/philiprehberger/laravel-model-diff

pkg:composer/philiprehberger/laravel-model-diff

Fund package maintenance!

philiprehberger

Statistics

Installs: 42

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.2.0 2026-03-23 06:08 UTC

This package is auto-updated.

Last update: 2026-04-22 15:59:55 UTC


README

Tests Latest Version on Packagist Last updated

Track and display structured differences between Eloquent model versions with human-readable labels.

Requirements

  • PHP 8.2+
  • Laravel 11 or 12

Installation

Install via Composer:

composer require philiprehberger/laravel-model-diff

The service provider and facade are registered automatically via Laravel package auto-discovery.

Publishing the config

php artisan vendor:publish --tag=model-diff-config

This creates config/model-diff.php in your application.

Configuration

// config/model-diff.php

return [

    /*
     | Attributes excluded from every diff comparison.
     */
    'ignored_attributes' => [
        'created_at',
        'updated_at',
        'id',
    ],

    /*
     | Format string used when rendering date/datetime values in
     | DiffResult::toHumanReadable().
     */
    'date_format' => 'M j, Y g:i A',

];

Usage

Comparing two model instances

Pass two instances of the same model — a "before" snapshot and an "after" snapshot — to ModelDiff::compare():

use PhilipRehberger\ModelDiff\Facades\ModelDiff;

$before = User::find(42);
// ... some time passes, the record is updated ...
$after = User::find(42);

$result = ModelDiff::compare($before, $after);

if ($result->hasChanges()) {
    // ['name', 'email']
    $result->changedAttributes();

    // Array of AttributeChange objects
    $result->getChanges();

    // Plain arrays
    $result->toArray();

    // Keyed by human-readable label
    $result->toHumanReadable();
}

Comparing an unsaved dirty model

Use ModelDiff::fromDirty() to inspect changes on a model that has not yet been saved:

$user = User::find(42);
$user->name  = 'New Name';
$user->email = 'new@example.com';

// Do NOT call save() — inspect the dirty state
$result = ModelDiff::fromDirty($user);

$result->changedAttributes(); // ['name', 'email']

Excluding extra attributes at call-site

$result = ModelDiff::ignoring(['internal_notes', 'cache_key'])
    ->compare($before, $after);

Filtering results

Use only() and except() to narrow a DiffResult after comparison:

$result = ModelDiff::compare($before, $after);

// Keep only specific attributes
$nameAndEmail = $result->only(['name', 'email']);

// Exclude specific attributes
$withoutMetadata = $result->except(['metadata', 'internal_notes']);

Accessing individual values

Retrieve the old or new value of a single attribute without iterating:

$result = ModelDiff::compare($before, $after);

$result->getBefore('name'); // 'Alice'
$result->getAfter('name');  // 'Bob'

// Returns null when the attribute is not in the diff
$result->getBefore('unchanged_field'); // null

Human-Readable Labels

Using the HasDiffLabels trait

Add the HasDiffLabels trait to any model and define a $diffLabels map:

use PhilipRehberger\ModelDiff\Concerns\HasDiffLabels;

class Client extends Model
{
    use HasDiffLabels;

    protected array $diffLabels = [
        'company_name' => 'Company Name',
        'is_active'    => 'Active Status',
        'arr_monthly'  => 'Monthly ARR',
    ];
}

Attributes without an explicit entry are automatically humanized: billing_address becomes Billing Address.

Retrieving a label directly

$client = new Client();
$client->getDiffLabel('company_name'); // "Company Name"
$client->getDiffLabel('phone_number'); // "Phone Number"

API

DiffResult

Method Return type Description
hasChanges() bool true when at least one attribute changed
changedAttributes() string[] Names of changed attributes
getChanges() AttributeChange[] All change objects
only(string[] $attributes) DiffResult New result with only the specified attributes
except(string[] $attributes) DiffResult New result excluding the specified attributes
getBefore(string $attribute) mixed Old value for an attribute (null if not in diff)
getAfter(string $attribute) mixed New value for an attribute (null if not in diff)
toArray() array Plain array — one entry per change
toHumanReadable() array Keyed by label; values formatted for display

toArray() output

[
    [
        'attribute' => 'name',
        'old'       => 'Alice',
        'new'       => 'Bob',
        'label'     => 'Full Name',
    ],
    // ...
]

toHumanReadable() output

[
    'Full Name' => [
        'old' => 'Alice',
        'new' => 'Bob',
    ],
    'Published At' => [
        'old' => 'Jan 1, 2024 9:00 AM',
        'new' => 'Jun 20, 2025 2:30 PM',
    ],
    // ...
]

AttributeChange

Property Type Description
$attribute string Raw attribute name
$old mixed Normalized old value
$new mixed Normalized new value
$label string Human-readable label
foreach ($result->getChanges() as $change) {
    echo "{$change->label}: {$change->old}{$change->new}";
}

Cast-Aware Comparison

The package normalizes values before comparing them, so you never get false positives from type mismatches:

Cast type Normalization
date, datetime, immutable_date/datetime Parsed to Carbon and formatted with date_format config
timestamp Parsed to Carbon and formatted with date_format config
array, json, object, collection Decoded and compared by content, not by serialized string
boolean, bool Strict (bool) cast before comparison
integer, int Strict (int) cast
float, double, real Strict (float) cast
decimal:N Strict (float) cast
Backed enum (SomeEnum::class) Compared by ->value; stored as scalar in AttributeChange
Unit enum (SomeEnum::class without backing) Compared by ->name; stored as string in AttributeChange

Note: Associative arrays are compared order-insensitively — ['a' => 1, 'b' => 2] equals ['b' => 2, 'a' => 1]. Sequential (list) arrays are compared in order.

Using the Facade

The ModelDiff facade is registered automatically:

use PhilipRehberger\ModelDiff\Facades\ModelDiff;

$result = ModelDiff::compare($before, $after);
$result = ModelDiff::fromDirty($model);
$result = ModelDiff::ignoring(['token'])->compare($before, $after);

Using the Class Directly

If you prefer not to use the facade, resolve the class from the container or instantiate it directly:

use PhilipRehberger\ModelDiff\ModelDiff;

// Via DI
public function __construct(private ModelDiff $diff) {}

// Directly
$diff = new ModelDiff();
$result = $diff->compare($before, $after);

Development

composer install
vendor/bin/phpunit
vendor/bin/pint --test
vendor/bin/phpstan analyse

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT