PHPackages                             philiprehberger/laravel-model-diff - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. philiprehberger/laravel-model-diff

ActiveLibrary[Database &amp; ORM](/categories/database)

philiprehberger/laravel-model-diff
==================================

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

v1.1.3(yesterday)15MITPHPPHP ^8.2CI passing

Since Mar 9Pushed todayCompare

[ Source](https://github.com/philiprehberger/laravel-model-diff)[ Packagist](https://packagist.org/packages/philiprehberger/laravel-model-diff)[ Docs](https://github.com/philiprehberger/laravel-model-diff)[ RSS](/packages/philiprehberger-laravel-model-diff/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (8)Versions (6)Used By (0)

Laravel Model Diff
==================

[](#laravel-model-diff)

[![Tests](https://github.com/philiprehberger/laravel-model-diff/actions/workflows/tests.yml/badge.svg)](https://github.com/philiprehberger/laravel-model-diff/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/78de63b176854edf17338ab92a7d72be9b58e708351fd0b551f611338fb1d430/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7068696c69707265686265726765722f6c61726176656c2d6d6f64656c2d646966662e737667)](https://packagist.org/packages/philiprehberger/laravel-model-diff)[![License](https://camo.githubusercontent.com/3809672f6ae94a02348311ddbe672c58698e05de8c703bc29db72d277c49092a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f7068696c69707265686265726765722f6c61726176656c2d6d6f64656c2d64696666)](LICENSE)

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

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11 or 12

Installation
------------

[](#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

[](#publishing-the-config)

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

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

### Configuration

[](#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
-----

[](#usage)

### Comparing two model instances

[](#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

[](#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

[](#excluding-extra-attributes-at-call-site)

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

### Human-Readable Labels

[](#human-readable-labels)

#### Using the HasDiffLabels trait

[](#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

[](#retrieving-a-label-directly)

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

API
---

[](#api)

### `DiffResult`

[](#diffresult)

MethodReturn typeDescription`hasChanges()``bool``true` when at least one attribute changed`changedAttributes()``string[]`Names of changed attributes`getChanges()``AttributeChange[]`All change objects`toArray()``array`Plain array — one entry per change`toHumanReadable()``array`Keyed by label; values formatted for display### toArray() output

[](#toarray-output)

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

### toHumanReadable() output

[](#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`

[](#attributechange)

PropertyTypeDescription`$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

[](#cast-aware-comparison)

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

Cast typeNormalization`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)` castBacked 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

[](#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

[](#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
-----------

[](#development)

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

License
-------

[](#license)

MIT

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance100

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

5

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cfd7d24cbbf32400fa13ce0bbe7a31edd2d66a6d4488eafdb3d64c5337bf0435?d=identicon)[philiprehberger](/maintainers/philiprehberger)

---

Top Contributors

[![philiprehberger](https://avatars.githubusercontent.com/u/8218077?v=4)](https://github.com/philiprehberger "philiprehberger (17 commits)")

---

Tags

difflaravelmodeleloquentAuditchangelog

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/philiprehberger-laravel-model-diff/health.svg)

```
[![Health](https://phpackages.com/badges/philiprehberger-laravel-model-diff/health.svg)](https://phpackages.com/packages/philiprehberger-laravel-model-diff)
```

###  Alternatives

[phaza/single-table-inheritance

Single Table Inheritance Trait

1515.8k](/packages/phaza-single-table-inheritance)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
