sebastian/comparator
sebastian/comparator compares PHP values for equality with type-specific comparators. Use the Factory to select the right comparator for two values and assertEquals() to verify matches, throwing a ComparisonFailure when differences are found.
Installation: Add via Composer:
composer require --dev sebastian/comparator
(Note: Often auto-installed as a PHPUnit dependency.)
Basic Usage:
use SebastianBergmann\Comparator\Factory;
$factory = new Factory();
$comparator = $factory->getComparatorFor($expected, $actual);
try {
$comparator->assertEquals($expected, $actual);
} catch (ComparisonFailure $e) {
// Handle failure with detailed diff
dd($e->getDiff());
}
First Use Case:
DateTime with timezones, nested arrays).DateTime objects across different timezones:
$dt1 = new DateTime('2023-01-01', new DateTimeZone('UTC'));
$dt2 = new DateTime('2023-01-01', new DateTimeZone('America/New_York'));
$factory->getComparatorFor($dt1, $dt2)->assertEquals($dt1, $dt2);
Key Classes:
Factory: Entry point for comparator selection.ComparisonFailure: Exception with diff details.Comparator: Core interface for equality checks.Extend Laravel’s TestCase with comparator-based assertions:
use SebastianBergmann\Comparator\Factory;
class CustomTestCase extends TestCase
{
protected Factory $comparatorFactory;
protected function setUp(): void
{
$this->comparatorFactory = new Factory();
// Configure diff context (e.g., for CI readability)
$this->comparatorFactory->setDiffContextLines(3);
}
protected function assertCustomEquals($expected, $actual, string $message = ''): void
{
$comparator = $this->comparatorFactory->getComparatorFor($expected, $actual);
$comparator->assertEquals($expected, $actual, $message);
}
}
Compare Eloquent models or collections with custom logic:
use App\Models\User;
use SebastianBergmann\Comparator\Comparator;
$user1 = User::find(1);
$user2 = User::find(2)->load('posts');
$comparator = (new Factory())->getComparatorFor($user1, $user2);
try {
$comparator->assertEquals($user1->toArray(), $user2->toArray());
} catch (ComparisonFailure $e) {
$this->fail("Model mismatch:\n" . $e->getDiff());
}
Validate JSON responses with detailed diffs:
use Illuminate\Testing\TestResponse;
$response = $this->getJson('/api/users');
$expected = ['id' => 1, 'name' => 'John'];
$response->assertOk()
->assertJson($expected);
$comparator = (new Factory())->getComparatorFor($expected, $response->json());
$comparator->assertEquals($expected, $response->json());
Compare job payloads or execution results:
$job = new ProcessPodcast($podcast);
$job->handle();
$expectedPayload = ['status' => 'processed'];
$comparator = (new Factory())->getComparatorFor($expectedPayload, $job->payload);
$comparator->assertEquals($expectedPayload, $job->payload);
Factory instances for repeated comparisons:
$factory = new Factory(); // Initialize once
$comparator = $factory->getComparatorFor($expected, $actual);
Implement SebastianBergmann\Comparator\Comparator for domain-specific types (e.g., Laravel models):
use SebastianBergmann\Comparator\Comparator;
class ModelComparator implements Comparator
{
public function assertEquals($expected, $actual, $description = '', $delta = 0, $canonicalize = false, $ignoreCase = false): void
{
if (!$expected instanceof Model && !$actual instanceof Model) {
throw new \InvalidArgumentException('Expected Model instances');
}
// Custom logic (e.g., ignore timestamps)
$expectedData = $expected->toArray();
$actualData = $actual->toArray();
unset($expectedData['created_at'], $expectedData['updated_at']);
unset($actualData['created_at'], $actualData['updated_at']);
$comparator = (new Factory())->getComparatorFor($expectedData, $actualData);
$comparator->assertEquals($expectedData, $actualData, $description);
}
}
Register with the Factory:
$factory = new Factory();
$factory->registerComparator(new ModelComparator());
Timezone-Sensitive Comparisons:
DateTime objects are compared without timezone normalization by default. Use DateTimeImmutable or normalize timezones explicitly:
$dt1 = $expected->setTimezone(new DateTimeZone('UTC'));
$dt2 = $actual->setTimezone(new DateTimeZone('UTC'));
Floating-Point Precision:
Comparator::ASSERT_NORMALIZATION_DELTA for numeric comparisons:
$comparator->assertEquals(1.0000000000000001, 1.0, '', 0.0001);
Closure Comparisons:
ClosureComparator for behavior-based checks:
$closureComparator = (new Factory())->getClosureComparator();
$closureComparator->assertEquals($expectedClosure, $actualClosure);
Array Canonicalization:
$comparator->assertEquals($expected, $actual, '', 0, true);
Diff Context Overhead:
setDiffContextLines(10)) can bloat failure messages. Default to 3 for CI/CD:
$factory->setDiffContextLines(3);
Inspect Differences:
try {
$comparator->assertEquals($expected, $actual);
} catch (ComparisonFailure $e) {
dd($e->getDiff(), $e->getExpected(), $e->getActual());
}
Log Diffs to Files:
file_put_contents(
storage_path('logs/comparison_diff.log'),
$e->getDiff()
);
Test with Edge Cases:
NaN, INF, empty arrays, nested objects, and cyclic references (e.g., stdClass with self-references).Global vs. Instance Settings:
Factory::setDiffContextLines() affects all comparators created by that instance. Use separate factories for different contexts:
$factoryCI = new Factory(); // Context lines = 3
$factoryDev = new Factory(); // Context lines = 10
PHP 8.5+ Compatibility:
SplObjectStorage methods (handled automatically in v7.1.1+).Custom Comparator Registration:
$factory = new Factory();
$factory->registerComparator(new MyCustomComparator());
Override Default Comparators:
DateTime) by registering a custom implementation with higher priority.Integrate with Laravel Events:
Illuminate\Testing\TestFailed to log comparator diffs:
Event::listen(TestFailed::class, function (TestFailed $e) {
if ($e->failedTest instanceof ComparisonFailure) {
Log::error('Test failed with diff:', ['diff' => $e->failedTest->getDiff()]);
}
});
Custom Diff Formatters:
ComparisonFailure to modify diff output (e.g., highlight changes in JSON):
class JsonComparisonFailure extends ComparisonFailure
{
public function getDiff(): string
{
return json_encode($this->getExpected(), JSON_PRETTY_PRINT) .
"\n---\n" .
json_encode($this->getActual(), JSON_PRETTY_PRINT);
}
}
How can I help you explore Laravel packages today?