Skip to content

Commit c9751ee

Browse files
committed
feat: make toRawArray() properly convert arrays of entities
1 parent fe1e944 commit c9751ee

2 files changed

Lines changed: 236 additions & 17 deletions

File tree

system/Entity/Entity.php

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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();

tests/system/Entity/EntityTest.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,132 @@ public function testToRawArrayRecursive(): void
11811181
], $result);
11821182
}
11831183

1184+
public function testToRawArrayRecursiveWithArray(): void
1185+
{
1186+
$entity = $this->getEntity();
1187+
$entity->entities = [$this->getEntity(), $this->getEntity()];
1188+
1189+
$result = $entity->toRawArray(false, true);
1190+
1191+
$this->assertSame([
1192+
'foo' => null,
1193+
'bar' => null,
1194+
'default' => 'sumfin',
1195+
'created_at' => null,
1196+
'entities' => [[
1197+
'foo' => null,
1198+
'bar' => null,
1199+
'default' => 'sumfin',
1200+
'created_at' => null,
1201+
], [
1202+
'foo' => null,
1203+
'bar' => null,
1204+
'default' => 'sumfin',
1205+
'created_at' => null,
1206+
]],
1207+
], $result);
1208+
}
1209+
1210+
public function testToRawArrayRecursiveOnlyChangedWithArray(): void
1211+
{
1212+
$first = $this->getEntity();
1213+
$second = $this->getEntity();
1214+
1215+
$entity = $this->getEntity();
1216+
$entity->entities = [$first];
1217+
$entity->syncOriginal();
1218+
1219+
$entity->entities = [$first, $second];
1220+
1221+
$result = $entity->toRawArray(true, true);
1222+
1223+
$this->assertSame([
1224+
'entities' => [1 => [
1225+
'foo' => null,
1226+
'bar' => null,
1227+
'default' => 'sumfin',
1228+
'created_at' => null,
1229+
]],
1230+
], $result);
1231+
}
1232+
1233+
public function testToRawArrayRecursiveOnlyChangedWithArrayEntityModified(): void
1234+
{
1235+
$first = $this->getEntity();
1236+
$second = $this->getEntity();
1237+
$first->foo = 'original';
1238+
$second->foo = 'also_original';
1239+
1240+
$entity = $this->getEntity();
1241+
$entity->entities = [$first, $second];
1242+
$entity->syncOriginal();
1243+
1244+
$second->foo = 'modified';
1245+
1246+
$result = $entity->toRawArray(true, true);
1247+
1248+
$this->assertSame([
1249+
'entities' => [1 => [
1250+
'foo' => 'modified',
1251+
'bar' => null,
1252+
'default' => 'sumfin',
1253+
'created_at' => null,
1254+
]],
1255+
], $result);
1256+
}
1257+
1258+
public function testToRawArrayRecursiveOnlyChangedWithArrayMultipleEntitiesModified(): void
1259+
{
1260+
$first = $this->getEntity();
1261+
$second = $this->getEntity();
1262+
$third = $this->getEntity();
1263+
$first->foo = 'first';
1264+
$second->foo = 'second';
1265+
$third->foo = 'third';
1266+
1267+
$entity = $this->getEntity();
1268+
$entity->entities = [$first, $second, $third];
1269+
$entity->syncOriginal();
1270+
1271+
$first->foo = 'first_modified';
1272+
$third->foo = 'third_modified';
1273+
1274+
$result = $entity->toRawArray(true, true);
1275+
1276+
$this->assertSame([
1277+
'entities' => [
1278+
0 => [
1279+
'foo' => 'first_modified',
1280+
'bar' => null,
1281+
'default' => 'sumfin',
1282+
'created_at' => null,
1283+
],
1284+
2 => [
1285+
'foo' => 'third_modified',
1286+
'bar' => null,
1287+
'default' => 'sumfin',
1288+
'created_at' => null,
1289+
],
1290+
],
1291+
], $result);
1292+
}
1293+
1294+
public function testToRawArrayRecursiveOnlyChangedWithArrayNoEntitiesModified(): void
1295+
{
1296+
$first = $this->getEntity();
1297+
$second = $this->getEntity();
1298+
$first->foo = 'unchanged';
1299+
$second->foo = 'also_unchanged';
1300+
1301+
$entity = $this->getEntity();
1302+
$entity->entities = [$first, $second];
1303+
$entity->syncOriginal();
1304+
1305+
$result = $entity->toRawArray(true, true);
1306+
1307+
$this->assertSame([], $result);
1308+
}
1309+
11841310
public function testToRawArrayOnlyChanged(): void
11851311
{
11861312
$entity = $this->getEntity();

0 commit comments

Comments
 (0)