Skip to content

Commit 5be197b

Browse files
committed
handle value objects with __toString()
1 parent f51f62e commit 5be197b

File tree

4 files changed

+51
-2
lines changed

4 files changed

+51
-2
lines changed

system/Entity/Entity.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ private function normalizeValue(mixed $data): mixed
385385
];
386386
} else {
387387
$objectData = get_object_vars($data);
388+
389+
// Fallback for value objects with __toString()
390+
// when properties are not accessible
391+
if ($objectData === [] && method_exists($data, '__toString')) {
392+
return [
393+
'__class' => $data::class,
394+
'__string' => (string) $data,
395+
];
396+
}
388397
}
389398

390399
return [

tests/system/Entity/EntityTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,4 +2244,42 @@ public function testHasChangedWithTraversable(): void
22442244
$entity->items = new ArrayObject([$obj3, $obj2]);
22452245
$this->assertTrue($entity->hasChanged('items'));
22462246
}
2247+
2248+
public function testHasChangedWithValueObjectsUsingToString(): void
2249+
{
2250+
// Define a value object class
2251+
$emailClass = new class () {
2252+
public static function create(string $email): object
2253+
{
2254+
return new class ($email) {
2255+
public function __construct(private readonly string $email)
2256+
{
2257+
}
2258+
2259+
public function __toString(): string
2260+
{
2261+
return $this->email;
2262+
}
2263+
};
2264+
}
2265+
};
2266+
2267+
$entity = new class () extends Entity {
2268+
protected $attributes = [
2269+
'email' => null,
2270+
];
2271+
};
2272+
2273+
$entity->email = $emailClass::create('[email protected]');
2274+
$entity->syncOriginal();
2275+
2276+
$this->assertFalse($entity->hasChanged('email'));
2277+
2278+
$entity->email = $emailClass::create('[email protected]');
2279+
$this->assertTrue($entity->hasChanged('email'));
2280+
2281+
$entity->syncOriginal();
2282+
$entity->email = $emailClass::create('[email protected]');
2283+
$this->assertFalse($entity->hasChanged('email'));
2284+
}
22472285
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ for objects and arrays instead of shallow comparison. This means:
5454
including timezone information.
5555
- **Collections** (``Traversable``) such as ``ArrayObject`` and ``ArrayIterator`` are converted
5656
to arrays for comparison.
57+
- **Value objects** with ``__toString()`` method are compared by their string representation when
58+
properties are not accessible (fallback for objects with private properties).
5759
- **Nested entities** (using ``toRawArray()``), ``JsonSerializable`` objects, and objects with
5860
``toArray()`` methods are recursively normalized for accurate change detection.
5961
- **Scalar values** (strings, integers, floats, booleans, null) continue to use direct comparison

user_guide_src/source/models/entities.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,5 +382,5 @@ Objects and Arrays
382382

383383
For objects and arrays, the Entity JSON-encodes and normalizes the values for comparison. This means that modifications
384384
to nested structures, object properties, array elements, nested entities (using ``toRawArray()``), enums (``BackedEnum``
385-
and ``UnitEnum``), datetime objects (``DateTimeInterface``), collections (``Traversable``), and objects implementing
386-
``JsonSerializable`` or ``toArray()`` will be properly detected.
385+
and ``UnitEnum``), datetime objects (``DateTimeInterface``), collections (``Traversable``), value objects with
386+
``__toString()``, and objects implementing ``JsonSerializable`` or ``toArray()`` will be properly detected.

0 commit comments

Comments
 (0)