WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 110 additions & 17 deletions system/Entity/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,37 +232,109 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu
*/
public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array
{
$return = [];
$convert = static function ($value) use (&$convert, $recursive) {
if (! $recursive) {
return $value;
}

if (! $onlyChanged) {
if ($recursive) {
return array_map(static function ($value) use ($onlyChanged, $recursive) {
if ($value instanceof self) {
$value = $value->toRawArray($onlyChanged, $recursive);
} elseif (is_callable([$value, 'toRawArray'])) {
$value = $value->toRawArray();
}
if ($value instanceof self) {
// Always output full array for nested entities
return $value->toRawArray(false, true);
}

if (is_array($value)) {
$result = [];

return $value;
}, $this->attributes);
foreach ($value as $k => $v) {
$result[$k] = $convert($v);
}

return $result;
}

if (is_object($value) && is_callable([$value, 'toRawArray'])) {
return $value->toRawArray();
}

return $this->attributes;
return $value;
};

// When returning everything
if (! $onlyChanged) {
return $recursive
? array_map($convert, $this->attributes)
: $this->attributes;
}

// When filtering by changed values only
$return = [];

foreach ($this->attributes as $key => $value) {
// Special handling for arrays of entities in recursive mode
// Skip hasChanged() and do per-entity comparison directly
if ($recursive && is_array($value) && $this->containsOnlyEntities($value)) {
$originalValue = $this->original[$key] ?? null;

if (! is_string($originalValue)) {
// No original or invalid format, export all entities
$converted = [];

foreach ($value as $idx => $item) {
$converted[$idx] = $item->toRawArray(false, true);
}
$return[$key] = $converted;

continue;
}

// Decode original array structure for per-entity comparison
$originalArray = json_decode($originalValue, true);
$converted = [];

foreach ($value as $idx => $item) {
// Compare current entity against its original state
$currentNormalized = $this->normalizeValue($item);
$originalNormalized = $originalArray[$idx] ?? null;

// Only include if changed, new, or can't determine
if ($originalNormalized === null || $currentNormalized !== $originalNormalized) {
$converted[$idx] = $item->toRawArray(false, true);
}
}

// Only include this property if at least one entity changed
if ($converted !== []) {
$return[$key] = $converted;
}

continue;
}

// For all other cases, use hasChanged()
if (! $this->hasChanged($key)) {
continue;
}

if ($recursive) {
if ($value instanceof self) {
$value = $value->toRawArray($onlyChanged, $recursive);
} elseif (is_callable([$value, 'toRawArray'])) {
$value = $value->toRawArray();
// Special handling for arrays (mixed or not all entities)
if (is_array($value)) {
$converted = [];

foreach ($value as $idx => $item) {
$converted[$idx] = $item instanceof self ? $item->toRawArray(false, true) : $convert($item);
}
$return[$key] = $converted;

continue;
}

// default recursive conversion
$return[$key] = $convert($value);

continue;
}

// non-recursive changed value
$return[$key] = $value;
}

Expand Down Expand Up @@ -347,6 +419,27 @@ public function hasChanged(?string $key = null): bool
return $originalValue !== $currentValue;
}

/**
* Checks if an array contains only Entity instances.
* This allows optimization for per-entity change tracking.
*
* @param array<int|string, mixed> $data
*/
private function containsOnlyEntities(array $data): bool
{
if ($data === []) {
return false;
}

foreach ($data as $item) {
if (! $item instanceof self) {
return false;
}
}

return true;
}

/**
* Recursively normalize a value for comparison.
* Converts objects and arrays to a JSON-encodable format.
Expand All @@ -365,7 +458,7 @@ private function normalizeValue(mixed $data): mixed

if (is_object($data)) {
// Check for Entity instance (use raw values, recursive)
if ($data instanceof Entity) {
if ($data instanceof self) {
$objectData = $data->toRawArray(false, true);
} elseif ($data instanceof JsonSerializable) {
$objectData = $data->jsonSerialize();
Expand Down
126 changes: 126 additions & 0 deletions tests/system/Entity/EntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,132 @@ public function testToRawArrayRecursive(): void
], $result);
}

public function testToRawArrayRecursiveWithArray(): void
{
$entity = $this->getEntity();
$entity->entities = [$this->getEntity(), $this->getEntity()];

$result = $entity->toRawArray(false, true);

$this->assertSame([
'foo' => null,
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
'entities' => [[
'foo' => null,
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
], [
'foo' => null,
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
]],
], $result);
}

public function testToRawArrayRecursiveOnlyChangedWithArray(): void
{
$first = $this->getEntity();
$second = $this->getEntity();

$entity = $this->getEntity();
$entity->entities = [$first];
$entity->syncOriginal();

$entity->entities = [$first, $second];

$result = $entity->toRawArray(true, true);

$this->assertSame([
'entities' => [1 => [
'foo' => null,
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
]],
], $result);
}

public function testToRawArrayRecursiveOnlyChangedWithArrayEntityModified(): void
{
$first = $this->getEntity();
$second = $this->getEntity();
$first->foo = 'original';
$second->foo = 'also_original';

$entity = $this->getEntity();
$entity->entities = [$first, $second];
$entity->syncOriginal();

$second->foo = 'modified';

$result = $entity->toRawArray(true, true);

$this->assertSame([
'entities' => [1 => [
'foo' => 'modified',
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
]],
], $result);
}

public function testToRawArrayRecursiveOnlyChangedWithArrayMultipleEntitiesModified(): void
{
$first = $this->getEntity();
$second = $this->getEntity();
$third = $this->getEntity();
$first->foo = 'first';
$second->foo = 'second';
$third->foo = 'third';

$entity = $this->getEntity();
$entity->entities = [$first, $second, $third];
$entity->syncOriginal();

$first->foo = 'first_modified';
$third->foo = 'third_modified';

$result = $entity->toRawArray(true, true);

$this->assertSame([
'entities' => [
0 => [
'foo' => 'first_modified',
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
],
2 => [
'foo' => 'third_modified',
'bar' => null,
'default' => 'sumfin',
'created_at' => null,
],
],
], $result);
}

public function testToRawArrayRecursiveOnlyChangedWithArrayNoEntitiesModified(): void
{
$first = $this->getEntity();
$second = $this->getEntity();
$first->foo = 'unchanged';
$second->foo = 'also_unchanged';

$entity = $this->getEntity();
$entity->entities = [$first, $second];
$entity->syncOriginal();

$result = $entity->toRawArray(true, true);

$this->assertSame([], $result);
}

public function testToRawArrayOnlyChanged(): void
{
$entity = $this->getEntity();
Expand Down
5 changes: 5 additions & 0 deletions user_guide_src/source/changelogs/v4.7.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ as a change because only reference comparison was performed. Now, any modificati
state of objects or arrays will be properly detected. If you relied on the old shallow comparison
behavior, you will need to update your code accordingly.

The ``Entity::toRawArray()`` method now properly converts arrays of entities when the ``$recursive``
parameter is ``true``. Previously, properties containing arrays were not recursively processed.
If you were relying on the old behavior where arrays remained unconverted, you will need to update
your code.

Interface Changes
=================

Expand Down
Loading