@@ -232,37 +232,109 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu
232232 */
233233 public function toRawArray (bool $ onlyChanged = false , bool $ recursive = false ): array
234234 {
235- $ return = [];
235+ $ convert = static function ($ value ) use (&$ convert , $ recursive ) {
236+ if (! $ recursive ) {
237+ return $ value ;
238+ }
236239
237- if (! $ onlyChanged ) {
238- if ($ recursive ) {
239- return array_map (static function ($ value ) use ($ onlyChanged , $ recursive ) {
240- if ($ value instanceof self) {
241- $ value = $ value ->toRawArray ($ onlyChanged , $ recursive );
242- } elseif (is_callable ([$ value , 'toRawArray ' ])) {
243- $ value = $ value ->toRawArray ();
244- }
240+ if ($ value instanceof self) {
241+ // Always output full array for nested entities
242+ return $ value ->toRawArray (false , true );
243+ }
244+
245+ if (is_array ($ value )) {
246+ $ result = [];
245247
246- return $ value ;
247- }, $ this ->attributes );
248+ foreach ($ value as $ k => $ v ) {
249+ $ result [$ k ] = $ convert ($ v );
250+ }
251+
252+ return $ result ;
253+ }
254+
255+ if (is_object ($ value ) && is_callable ([$ value , 'toRawArray ' ])) {
256+ return $ value ->toRawArray ();
248257 }
249258
250- return $ this ->attributes ;
259+ return $ value ;
260+ };
261+
262+ // When returning everything
263+ if (! $ onlyChanged ) {
264+ return $ recursive
265+ ? array_map ($ convert , $ this ->attributes )
266+ : $ this ->attributes ;
251267 }
252268
269+ // When filtering by changed values only
270+ $ return = [];
271+
253272 foreach ($ this ->attributes as $ key => $ value ) {
273+ // Special handling for arrays of entities in recursive mode
274+ // Skip hasChanged() and do per-entity comparison directly
275+ if ($ recursive && is_array ($ value ) && $ this ->containsOnlyEntities ($ value )) {
276+ $ originalValue = $ this ->original [$ key ] ?? null ;
277+
278+ if (! is_string ($ originalValue )) {
279+ // No original or invalid format, export all entities
280+ $ converted = [];
281+
282+ foreach ($ value as $ idx => $ item ) {
283+ $ converted [$ idx ] = $ item ->toRawArray (false , true );
284+ }
285+ $ return [$ key ] = $ converted ;
286+
287+ continue ;
288+ }
289+
290+ // Decode original array structure for per-entity comparison
291+ $ originalArray = json_decode ($ originalValue , true );
292+ $ converted = [];
293+
294+ foreach ($ value as $ idx => $ item ) {
295+ // Compare current entity against its original state
296+ $ currentNormalized = $ this ->normalizeValue ($ item );
297+ $ originalNormalized = $ originalArray [$ idx ] ?? null ;
298+
299+ // Only include if changed, new, or can't determine
300+ if ($ originalNormalized === null || $ currentNormalized !== $ originalNormalized ) {
301+ $ converted [$ idx ] = $ item ->toRawArray (false , true );
302+ }
303+ }
304+
305+ // Only include this property if at least one entity changed
306+ if ($ converted !== []) {
307+ $ return [$ key ] = $ converted ;
308+ }
309+
310+ continue ;
311+ }
312+
313+ // For all other cases, use hasChanged()
254314 if (! $ this ->hasChanged ($ key )) {
255315 continue ;
256316 }
257317
258318 if ($ recursive ) {
259- if ($ value instanceof self) {
260- $ value = $ value ->toRawArray ($ onlyChanged , $ recursive );
261- } elseif (is_callable ([$ value , 'toRawArray ' ])) {
262- $ value = $ value ->toRawArray ();
319+ // Special handling for arrays (mixed or not all entities)
320+ if (is_array ($ value )) {
321+ $ converted = [];
322+
323+ foreach ($ value as $ idx => $ item ) {
324+ $ converted [$ idx ] = $ item instanceof self ? $ item ->toRawArray (false , true ) : $ convert ($ item );
325+ }
326+ $ return [$ key ] = $ converted ;
327+
328+ continue ;
263329 }
330+
331+ // default recursive conversion
332+ $ return [$ key ] = $ convert ($ value );
333+
334+ continue ;
264335 }
265336
337+ // non-recursive changed value
266338 $ return [$ key ] = $ value ;
267339 }
268340
@@ -347,6 +419,27 @@ public function hasChanged(?string $key = null): bool
347419 return $ originalValue !== $ currentValue ;
348420 }
349421
422+ /**
423+ * Checks if an array contains only Entity instances.
424+ * This allows optimization for per-entity change tracking.
425+ *
426+ * @param array<int|string, mixed> $data
427+ */
428+ private function containsOnlyEntities (array $ data ): bool
429+ {
430+ if ($ data === []) {
431+ return false ;
432+ }
433+
434+ foreach ($ data as $ item ) {
435+ if (! $ item instanceof self) {
436+ return false ;
437+ }
438+ }
439+
440+ return true ;
441+ }
442+
350443 /**
351444 * Recursively normalize a value for comparison.
352445 * Converts objects and arrays to a JSON-encodable format.
@@ -365,7 +458,7 @@ private function normalizeValue(mixed $data): mixed
365458
366459 if (is_object ($ data )) {
367460 // Check for Entity instance (use raw values, recursive)
368- if ($ data instanceof Entity ) {
461+ if ($ data instanceof self ) {
369462 $ objectData = $ data ->toRawArray (false , true );
370463 } elseif ($ data instanceof JsonSerializable) {
371464 $ objectData = $ data ->jsonSerialize ();
0 commit comments