getProperties() as $property) { if ($property->getName() === 'id') { continue; } // Skip ID $property->setAccessible(true); $oldValue = $property->getValue($original); $newValue = $property->getValue($modified); if ($oldValue !== $newValue) { $changes[] = $property->getName(); $oldValues[$property->getName()] = $oldValue; $newValues[$property->getName()] = $newValue; } } // Assertions expect($changes)->toBe(['name', 'age']); expect($oldValues)->toBe(['name' => 'John Doe', 'age' => 30]); expect($newValues)->toBe(['name' => 'John Smith', 'age' => 31]); }); test('change tracking detects no changes when objects are identical', function () { $testClass = new class () { public function __construct( public readonly int $id = 1, public string $name = '', public string $email = '', public int $age = 0, ) { } }; $original = new $testClass(id: 1, name: 'John Doe', email: 'john@example.com', age: 30); $identical = new $testClass(id: 1, name: 'John Doe', email: 'john@example.com', age: 30); // Simulate change detection $changes = []; $reflectionClass = new ReflectionClass($original); foreach ($reflectionClass->getProperties() as $property) { if ($property->getName() === 'id') { continue; } $property->setAccessible(true); $oldValue = $property->getValue($original); $newValue = $property->getValue($identical); if ($oldValue !== $newValue) { $changes[] = $property->getName(); } } expect($changes)->toBeEmpty(); }); test('change tracking handles type-sensitive comparisons', function () { $testClass = new class () { public function __construct( public readonly int $id = 1, public mixed $value = null, ) { } }; $original = new $testClass(id: 1, value: 0); $modified = new $testClass(id: 1, value: '0'); // String vs int // Simulate change detection $changes = []; $reflectionClass = new ReflectionClass($original); foreach ($reflectionClass->getProperties() as $property) { if ($property->getName() === 'id') { continue; } $property->setAccessible(true); $oldValue = $property->getValue($original); $newValue = $property->getValue($modified); if ($oldValue !== $newValue) { $changes[] = $property->getName(); } } // Should detect change due to type difference (0 !== '0') expect($changes)->toBe(['value']); }); test('change tracking handles null values correctly', function () { $testClass = new class () { public function __construct( public readonly int $id = 1, public ?string $nullable = null, ) { } }; // Test 1: null to value $original = new $testClass(id: 1, nullable: null); $modified = new $testClass(id: 1, nullable: 'value'); $changes = []; $reflectionClass = new ReflectionClass($original); foreach ($reflectionClass->getProperties() as $property) { if ($property->getName() === 'id') { continue; } $property->setAccessible(true); $oldValue = $property->getValue($original); $newValue = $property->getValue($modified); if ($oldValue !== $newValue) { $changes[] = $property->getName(); } } expect($changes)->toBe(['nullable']); // Test 2: value to null $original2 = new $testClass(id: 1, nullable: 'value'); $modified2 = new $testClass(id: 1, nullable: null); $changes2 = []; foreach ($reflectionClass->getProperties() as $property) { if ($property->getName() === 'id') { continue; } $property->setAccessible(true); $oldValue = $property->getValue($original2); $newValue = $property->getValue($modified2); if ($oldValue !== $newValue) { $changes2[] = $property->getName(); } } expect($changes2)->toBe(['nullable']); });