1313
1414namespace CodeIgniter \Entity ;
1515
16+ use BackedEnum ;
1617use CodeIgniter \DataCaster \DataCaster ;
1718use CodeIgniter \Entity \Cast \ArrayCast ;
1819use CodeIgniter \Entity \Cast \BooleanCast ;
3334use Exception ;
3435use JsonSerializable ;
3536use 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