Skip to content

Commit f6b1974

Browse files
committed
feat(entity): deep change tracking for objects and arrays
1 parent 05c7217 commit f6b1974

File tree

4 files changed

+735
-3
lines changed

4 files changed

+735
-3
lines changed

system/Entity/Entity.php

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\Entity;
1515

16+
use BackedEnum;
1617
use CodeIgniter\DataCaster\DataCaster;
1718
use CodeIgniter\Entity\Cast\ArrayCast;
1819
use CodeIgniter\Entity\Cast\BooleanCast;
@@ -33,6 +34,7 @@
3334
use Exception;
3435
use JsonSerializable;
3536
use ReturnTypeWillChange;
37+
use UnitEnum;
3638

3739
/**
3840
* Entity encapsulation, for use with CodeIgniter\Model
@@ -131,6 +133,11 @@ class Entity implements JsonSerializable
131133
*/
132134
private bool $_cast = true;
133135

136+
/**
137+
* Indicates whether all attributes are scalars (for optimization)
138+
*/
139+
private bool $_onlyScalars = true;
140+
134141
/**
135142
* Allows filling in Entity parameters during construction.
136143
*/
@@ -263,11 +270,24 @@ public function toRawArray(bool $onlyChanged = false, bool $recursive = false):
263270
/**
264271
* Ensures our "original" values match the current values.
265272
*
273+
* Objects and arrays are normalized and JSON-encoded for reliable change detection,
274+
* while scalars are stored as-is for performance.
275+
*
266276
* @return $this
267277
*/
268278
public function syncOriginal()
269279
{
270-
$this->original = $this->attributes;
280+
$this->original = [];
281+
$this->_onlyScalars = true;
282+
283+
foreach ($this->attributes as $key => $value) {
284+
if (is_object($value) || is_array($value)) {
285+
$this->original[$key] = json_encode($this->normalizeValue($value));
286+
$this->_onlyScalars = false;
287+
} else {
288+
$this->original[$key] = $value;
289+
}
290+
}
271291

272292
return $this;
273293
}
@@ -283,7 +303,17 @@ public function hasChanged(?string $key = null): bool
283303
{
284304
// If no parameter was given then check all attributes
285305
if ($key === null) {
286-
return $this->original !== $this->attributes;
306+
if ($this->_onlyScalars) {
307+
return $this->original !== $this->attributes;
308+
}
309+
310+
foreach (array_keys($this->attributes) as $attributeKey) {
311+
if ($this->hasChanged($attributeKey)) {
312+
return true;
313+
}
314+
}
315+
316+
return false;
287317
}
288318

289319
$dbColumn = $this->mapProperty($key);
@@ -298,7 +328,64 @@ public function hasChanged(?string $key = null): bool
298328
return true;
299329
}
300330

301-
return $this->original[$dbColumn] !== $this->attributes[$dbColumn];
331+
// It was removed
332+
if (array_key_exists($dbColumn, $this->original) && ! array_key_exists($dbColumn, $this->attributes)) {
333+
return true;
334+
}
335+
336+
$originalValue = $this->original[$dbColumn];
337+
$currentValue = $this->attributes[$dbColumn];
338+
339+
// If original is a string, it was JSON-encoded (object or array)
340+
if (is_string($originalValue) && (is_object($currentValue) || is_array($currentValue))) {
341+
return $originalValue !== json_encode($this->normalizeValue($currentValue));
342+
}
343+
344+
// For scalars, use direct comparison
345+
return $originalValue !== $currentValue;
346+
}
347+
348+
/**
349+
* Recursively normalize a value for comparison.
350+
* Converts objects and arrays to a JSON-encodable format.
351+
*/
352+
private function normalizeValue(mixed $data): mixed
353+
{
354+
if (is_array($data)) {
355+
$normalized = [];
356+
357+
foreach ($data as $key => $value) {
358+
$normalized[$key] = $this->normalizeValue($value);
359+
}
360+
361+
return $normalized;
362+
}
363+
364+
if (is_object($data)) {
365+
// Check for Entity instance (use raw values, recursive)
366+
if ($data instanceof Entity) {
367+
$objectData = $data->toRawArray(false, true);
368+
} elseif ($data instanceof JsonSerializable) {
369+
$objectData = $data->jsonSerialize();
370+
} elseif (method_exists($data, 'toArray')) {
371+
$objectData = $data->toArray();
372+
} elseif ($data instanceof UnitEnum) {
373+
return [
374+
'__class' => $data::class,
375+
'__enum' => $data instanceof BackedEnum ? $data->value : $data->name,
376+
];
377+
} else {
378+
$objectData = get_object_vars($data);
379+
}
380+
381+
return [
382+
'__class' => $data::class,
383+
'__data' => $this->normalizeValue($objectData),
384+
];
385+
}
386+
387+
// Return scalars and null as-is
388+
return $data;
302389
}
303390

304391
/**

0 commit comments

Comments
 (0)