sebastian/comparator
sebastian/comparator compares PHP values for equality with type-aware comparators. Use the Factory to select the right comparator and get helpful ComparisonFailure details when assertions fail—ideal for test suites and tooling.
Installation:
composer require --dev sebastian/comparator
Add as a dev dependency to avoid bloating production builds.
First Use Case:
Replace ad-hoc assertSame() or json_encode() comparisons in tests with type-aware assertions. Example:
use SebastianBergmann\Comparator\Factory;
use SebastianBergmann\Comparator\ComparisonFailure;
$factory = new Factory();
$comparator = $factory->getComparatorFor($actual, $expected);
try {
$comparator->assertEquals($actual, $expected);
$this->assertTrue(true); // Pass
} catch (ComparisonFailure $e) {
$this->fail($e->getMessage()); // Fail with diff
}
Key Entry Points:
Factory: Central class to instantiate comparators.ComparisonFailure: Exception with rich diff output.DateTimeComparator, ArrayComparator, ClosureComparator, etc. (see Implementation Patterns).Where to Look First:
Replace Laravel’s assertEquals() or assertSame() with type-aware comparisons:
// Before (flaky for objects/arrays)
$this->assertEquals($user->toArray(), $expectedArray);
// After (type-aware, handles object arrays)
$comparator = (new Factory())->getComparatorFor($user->toArray(), $expectedArray);
$this->assertTrue($comparator->assertEquals($user->toArray(), $expectedArray));
For order-agnostic testing (e.g., API responses, Eloquent collections):
$comparator = (new Factory())->getComparatorFor($actualArray, $expectedArray, 0.0, true);
$comparator->assertEquals($actualArray, $expectedArray);
// true = canonicalize (order-independent)
Handle timezones and precision:
$comparator = (new Factory())->getComparatorFor(
Carbon::parse('2023-01-01 12:00:00', 'America/New_York'),
Carbon::parse('2023-01-01 11:00:00', 'America/Chicago')
);
$comparator->assertEquals($dt1, $dt2); // Works with timezone offsets
Extend for Laravel-specific types (e.g., Carbon, Collection):
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparatorInterface;
class CarbonComparator implements ComparatorInterface
{
public function assertEquals($expected, $actual, $description = '', $delta = 0.0, $canonicalize = false)
{
// Custom Carbon logic (e.g., timezone normalization)
if (!$expected->eq($actual)) {
throw new ComparisonFailure($expected, $actual, $description);
}
}
}
// Register in Factory
$factory = new Factory();
$factory->registerComparator('Carbon\Carbon', new CarbonComparator());
Leverage rich diffs for debugging:
try {
$comparator->assertEquals($actual, $expected);
} catch (ComparisonFailure $e) {
$this->fail($e->getDiff());
// Outputs unified diff (e.g., for arrays/strings)
}
// Compare models with relationships (order-agnostic)
$comparator = (new Factory())->getComparatorFor(
$user->load('posts')->toArray(),
$expected,
0.0,
true // Canonicalize
);
$comparator->assertEquals($actual, $expected);
$response = $this->getJson('/api/users');
$comparator = (new Factory())->getComparatorFor(
$response->json(),
$expectedSchema,
0.0,
true // Ignore array order
);
$comparator->assertEquals($response->json(), $expectedSchema);
$seededUsers = User::all()->toArray();
$comparator = (new Factory())->getComparatorFor($seededUsers, $expectedUsers, 0.0, true);
$comparator->assertEquals($seededUsers, $expectedUsers);
$closure1 = fn() => 'result';
$closure2 = fn() => 'result';
$comparator = (new Factory())->getComparatorFor($closure1, $closure2);
$comparator->assertEquals($closure1, $closure2); // Works since v7.1.0
Object Array Sorting:
spl_object_id() sorting (fixed in v8.1.2).canonicalize = true for order-agnostic comparisons:
$comparator->assertEquals($actual, $expected, '', 0.0, true);
DateTime Precision:
DateTimeComparator with a delta tolerance:
$comparator = (new Factory())->getComparatorFor($dt1, $dt2, 1.0); // 1-second tolerance
Non-Serializable Diffs:
ComparisonFailure may fail to serialize if stack traces contain non-serializable objects (fixed in v8.2.1).getDiff() instead of throwing.XML/HTML Comparisons:
DOMNodeComparator may crash on malformed XML or ignore comments (fixed in v7.1.8).SimpleXMLElement for simpler cases.Closure Comparisons:
assertSame() for reference equality.PHP 8.5+ Warnings:
SplObjectStorage methods may trigger warnings (suppressed in v7.1.4+).Inspect Diffs:
$e->getDiff() to get a unified diff for arrays/strings:
catch (ComparisonFailure $e) {
$this->fail($e->getDiff());
}
Enable Canonicalization:
canonicalize = true to ignore array order/keys:
$comparator->assertEquals($actual, $expected, '', 0.0, true);
Custom Comparator Logging:
class CustomComparator implements ComparatorInterface {
public function assertEquals($expected, $actual, $description = '', $delta = 0.0, $canonicalize = false) {
error_log("Comparing: " . print_r($expected, true) . " vs " . print_r($actual, true));
// ...
}
}
Performance Tuning:
$factory = new Factory();
$arrayComparator = $factory->getComparatorFor([], []);
Laravel-Specific Quirks:
CarbonComparator or normalize timezones before comparison:
$dt1->setTimezone('UTC');
$dt2->setTimezone('UTC');
$comparator->assertEquals($collection->toArray(), $expected);
Register Custom Comparators:
$factory = new Factory();
$factory->registerComparator('App\Models\User', new UserComparator());
Override Default Comparators:
$factory = new Factory();
$factory->registerComparator('DateTime', new CustomDateTimeComparator());
How can I help you explore Laravel packages today?