Weave 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

Comparator Laravel Package

sebastian/comparator

Compare PHP values for equality with dedicated comparators for scalars, arrays, objects, dates, and more. Part of PHPUnit, it reports detailed differences via ComparisonFailure exceptions and ships a Factory for selecting the right comparator.

View on GitHub
Deep Wiki
Context7

Getting Started

sebastian/comparator almost always arrives transitively with PHPUnit — check before adding it explicitly:

composer why sebastian/comparator
composer show sebastian/comparator   # note the resolved version

Adding it as a direct dependency is only worth it when you also want to use it outside assertions (diffing payloads, snapshot checks, domain-level equality). If you do require it, pin nothing looser than PHPUnit's own constraint or Composer will downgrade one of them:

composer require --dev sebastian/comparator

Where to look first: SebastianBergmann\Comparator\Factory (the dispatcher), Comparator (the base class with accepts() / assertEquals()), and ComparisonFailure (the exception carrying expected/actual/diff). The built-in comparator list is essentially your feature map: scalars, numerics, arrays, objects, DateTime, DateInterval, DOMNode, SplObjectStorage, enums, BcMath\Number, closures, mocks.

First use case — timezone-safe date assertions, which is where naive assertEquals on formatted strings bites you:

use SebastianBergmann\Comparator\Factory;
use SebastianBergmann\Comparator\ComparisonFailure;

$factory    = new Factory;                                  // private instance: no global state
$comparator = $factory->getComparatorFor($expected, $actual);

try {
    // $delta in seconds for date types
    $comparator->assertEquals($expected, $actual, delta: 0.0);
} catch (ComparisonFailure $failure) {
    report($failure->getMessage());
}

In a Laravel test the same thing is already happening behind $this->assertEquals(), so the fastest win is understanding which comparator your assertion routes to — that explains most "why is this passing/failing?" moments.

Implementation Patterns

1. Lean on the flags instead of hand-rolled normalization

Comparator::assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) maps directly onto PHPUnit's API. Use them rather than sort()-ing arrays yourself:

// Order-agnostic API payload check
$this->assertEquals($expectedJson, $response->json(), canonicalize: true);

// Float tolerance
$this->assertEquals(0.1 + 0.2, 0.3, 0.000001);

// Case-insensitive slug/header comparison
$this->assertEquals('application/json', $header, ignoreCase: true);

2. Convert Eloquent models to arrays before comparing

ObjectComparator walks public/accessible properties recursively. Eloquent models carry $original, $changes, $relations, $connection, the resolver, and event dispatcher — comparing two models directly produces noise or false negatives. Compare the serialized shape:

$this->assertEquals(
    User::factory()->make(['id' => 1])->toArray(),
    $model->fresh()->toArray(),
    canonicalize: true
);

If you genuinely need model-level equality (identity + dirty state), that is the classic case for a custom comparator (below) rather than a raw object diff.

3. Register a custom comparator once, in the base TestCase

PHPUnit resolves comparators through Factory::getInstance(), so register() on the singleton makes your comparator apply to every assertEquals() in the suite:

// tests/TestCase.php
protected function setUp(): void
{
    parent::setUp();

    Factory::getInstance()->register(new MoneyComparator);
}
final class MoneyComparator extends Comparator
{
    public function accepts(mixed $expected, mixed $actual): bool
    {
        return $expected instanceof Money && $actual instanceof Money;
    }

    public function assertEquals(
        mixed $expected, mixed $actual, float $delta = 0.0,
        bool $canonicalize = false, bool $ignoreCase = false
    ): void {
        if ($expected->getCurrency() !== $actual->getCurrency()
            || abs($expected->getAmount() - $actual->getAmount()) > $delta) {
            throw new ComparisonFailure(
                $expected, $actual,
                (string) $expected, (string) $actual,
                'Failed asserting that two Money values are equal.'
            );
        }
    }
}

Key details: overriding accepts() with a narrow instanceof check, and preferring to extend ObjectComparator/ArrayComparator when your type contains nested structures — the base implementations delegate child comparisons back through $this->getFactory(), which register() wires up for you.

4. Use ComparisonFailure as a diff engine, not just an exception

For non-assertion contexts — contract tests writing artifacts, "why did this job payload change?" investigations — catch the failure and pull the diff:

try {
    $factory->getComparatorFor($before, $after)->assertEquals($before, $after, canonicalize: true);
} catch (ComparisonFailure $failure) {
    Storage::disk('local')->put(
        'diffs/job-payload-'.now()->timestamp.'.diff',
        $failure->getDiff()
    );
}

getExpectedAsString(), getActualAsString(), getDiff() and showDiff() are the useful members; guard getDiff() with showDiff() because a diff is not always produced.

5. Pest / helper wrapping

Keep the package out of application code and behind one helper or expectation so the team standardizes on it:

// tests/Pest.php
expect()->extend('toEqualCanonicalized', fn ($expected) => 
    $this->toBeInstanceOf(...)->and(...) // or fall through to assertEquals
);

More practically, define a trait method assertComparable($expected, $actual, float $delta = 0) that resolves a private Factory, so behaviour is identical everywhere and you never depend on global registration.

6. Version-gated workarounds

Several Laravel-flavoured pain points only exist in older versions — check the resolved version before writing a workaround:

  • 8.1.2 — arrays of equal objects no longer sorted by spl_object_id()
  • 8.1.1 — deterministic sort for mixed-type arrays
  • 8.2.0 — DateIntervalComparator, XML diff fixes (comments, C14N failures, non-ASCII), serializable ComparisonFailure, DateTimeComparator pair tracking in nested structures
  • 8.2.1 — closure comparison by value, canonicalized string keys preserved

If you're on a lower major, write the workaround; if you can bump PHPUnit (and therefore the comparator), delete the workaround.

Gotchas and Tips

  • You don't choose the version — PHPUnit does. sebastian/comparator 8.x requires PHP 8.4+ and drops 8.3; a Laravel app on PHP 8.2/8.3 will resolve to 6.x/7.x. So "we upgraded the comparator" really means "we upgraded PHP + PHPUnit." Never add a direct constraint that fights PHPUnit's.
  • assertEquals is loose. ScalarComparator/NumericComparator accept 1 vs '1' and '1' vs 1.0. This is the single biggest source of "test passed but shouldn't have." Use assertSame for scalars and reserve assertEquals for structured data.
  • canonicalize: true throws away order. It is correct for unordered sets/JSON payloads and wrong for ordered lists (queue order, sequence columns). Also remember it sorts recursively, so a nested ordered array gets silently reordered too.
  • Registering on Factory::getInstance() is global and sticky. A broad accepts() will shadow every built-in comparator for the rest of the process, including inside PHPUnit itself. Keep custom comparators narrow, and restore state in tearDown() (Factory::reset()) if you register per-test.
  • delta is comparator-specific. Absolute numeric tolerance for numbers, seconds for DateTime/DateInterval. Don't assume one meaning across types.
  • DateTime comparison is instant-based, so 04:13:35 America/New_York equals 03:13:35 America/Chicago. That's usually what you want from a Carbon assertion on an API response, but not what a "same wall clock in the user's timezone" test wants — assert the formatted string there instead.
  • Comparing Carbon to a string never works; compare Carbon to Carbon (or to a DateTimeInterface) so DateTimeComparator engages.
  • XML diff false positives (differences only in comments) and unreadable numeric character references were fixed in 8.2.0 — if your XML response assertions look wrong or ugly, that's a version issue, not your test.
  • ComparisonFailure serialization broke when a stack trace held non-serializable objects (fixed 8.2.0). Relevant if you serialize failures into a queued report, cache, or a data provider.
  • Performance: canonicalized comparison of multi-MB JSON sorts and recursively diffs everything. For large payloads assert structure (assertJsonStructure, specific keys, counts) or compare hashes; save full deep-diffs for small fixtures.
  • Debugging in CI: dump $failure->getExpectedAsString(), getActualAsString() and getDiff() to artifacts — the diff (context lines configurable since 8.1.0, and strict unified output since 8.1.4) is far more actionable than the truncated PHPUnit message.
  • Extension points are minimal: Factory::register() is the whole story — no config file, no service provider, no container binding. Treat it as dev-only tooling and keep it out of app/.
  • Version pin in CI: log composer show sebastian/comparator in your pipeline; mysterious pass/fail flips after a PHPUnit bump are usually a comparator major changing array/object semantics.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style