Weave Code
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

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.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Installation: Add via Composer:

    composer require --dev sebastian/comparator
    

    (Note: Often auto-installed as a PHPUnit dependency.)

  2. 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());
    }
    
  3. First Use Case:

    • Debugging test failures with complex objects (e.g., DateTime with timezones, nested arrays).
    • Example: Compare two 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);
      
  4. Key Classes:

    • Factory: Entry point for comparator selection.
    • ComparisonFailure: Exception with diff details.
    • Comparator: Core interface for equality checks.

Implementation Patterns

1. Laravel-Specific Workflows

Custom Test Assertions

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);
    }
}

Debugging Model Relationships

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());
}

2. Integration with Laravel Services

API Response Validation

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());

Queue Job Assertions

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);

3. Performance Considerations

  • Avoid in Hot Paths: Use only in tests or debugging tools (not production logic).
  • Cache Comparators: Reuse Factory instances for repeated comparisons:
    $factory = new Factory(); // Initialize once
    $comparator = $factory->getComparatorFor($expected, $actual);
    

4. Extending Functionality

Custom Comparators

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());

Gotchas and Tips

Pitfalls

  1. 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'));
      
  2. Floating-Point Precision:

    • Use Comparator::ASSERT_NORMALIZATION_DELTA for numeric comparisons:
      $comparator->assertEquals(1.0000000000000001, 1.0, '', 0.0001);
      
  3. Closure Comparisons:

    • Closures are compared by reference, not by output. Use ClosureComparator for behavior-based checks:
      $closureComparator = (new Factory())->getClosureComparator();
      $closureComparator->assertEquals($expectedClosure, $actualClosure);
      
  4. Array Canonicalization:

    • Arrays with identical values but different keys are not considered equal by default. Enable canonicalization:
      $comparator->assertEquals($expected, $actual, '', 0, true);
      
  5. Diff Context Overhead:

    • Large context lines (e.g., setDiffContextLines(10)) can bloat failure messages. Default to 3 for CI/CD:
      $factory->setDiffContextLines(3);
      

Debugging Tips

  1. Inspect Differences:

    try {
        $comparator->assertEquals($expected, $actual);
    } catch (ComparisonFailure $e) {
        dd($e->getDiff(), $e->getExpected(), $e->getActual());
    }
    
  2. Log Diffs to Files:

    file_put_contents(
        storage_path('logs/comparison_diff.log'),
        $e->getDiff()
    );
    
  3. Test with Edge Cases:

    • NaN, INF, empty arrays, nested objects, and cyclic references (e.g., stdClass with self-references).

Configuration Quirks

  1. 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
      
  2. PHP 8.5+ Compatibility:

    • Avoid SplObjectStorage methods (handled automatically in v7.1.1+).
  3. Custom Comparator Registration:

    • Register comparators before creating the factory:
      $factory = new Factory();
      $factory->registerComparator(new MyCustomComparator());
      

Extension Points

  1. Override Default Comparators:

    • Replace built-in comparators (e.g., for DateTime) by registering a custom implementation with higher priority.
  2. Integrate with Laravel Events:

    • Listen to 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()]);
          }
      });
      
  3. Custom Diff Formatters:

    • Extend 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);
          }
      }
      
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.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport