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
111 changes: 108 additions & 3 deletions system/Entity/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace CodeIgniter\Entity;

use BackedEnum;
use CodeIgniter\DataCaster\DataCaster;
use CodeIgniter\Entity\Cast\ArrayCast;
use CodeIgniter\Entity\Cast\BooleanCast;
Expand All @@ -30,9 +31,12 @@
use CodeIgniter\Entity\Exceptions\CastException;
use CodeIgniter\I18n\Time;
use DateTime;
use DateTimeInterface;
use Exception;
use JsonSerializable;
use ReturnTypeWillChange;
use Traversable;
use UnitEnum;

/**
* Entity encapsulation, for use with CodeIgniter\Model
Expand Down Expand Up @@ -131,6 +135,11 @@ class Entity implements JsonSerializable
*/
private bool $_cast = true;

/**
* Indicates whether all attributes are scalars (for optimization)
*/
private bool $_onlyScalars = true;

/**
* Allows filling in Entity parameters during construction.
*/
Expand Down Expand Up @@ -263,11 +272,24 @@ public function toRawArray(bool $onlyChanged = false, bool $recursive = false):
/**
* Ensures our "original" values match the current values.
*
* Objects and arrays are normalized and JSON-encoded for reliable change detection,
* while scalars are stored as-is for performance.
*
* @return $this
*/
public function syncOriginal()
{
$this->original = $this->attributes;
$this->original = [];
$this->_onlyScalars = true;

foreach ($this->attributes as $key => $value) {
if (is_object($value) || is_array($value)) {
$this->original[$key] = json_encode($this->normalizeValue($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->_onlyScalars = false;
} else {
$this->original[$key] = $value;
}
}

return $this;
}
Expand All @@ -283,7 +305,17 @@ public function hasChanged(?string $key = null): bool
{
// If no parameter was given then check all attributes
if ($key === null) {
return $this->original !== $this->attributes;
if ($this->_onlyScalars) {
return $this->original !== $this->attributes;
}

foreach (array_keys($this->attributes) as $attributeKey) {
if ($this->hasChanged($attributeKey)) {
return true;
}
}

return false;
}

$dbColumn = $this->mapProperty($key);
Expand All @@ -298,7 +330,80 @@ public function hasChanged(?string $key = null): bool
return true;
}

return $this->original[$dbColumn] !== $this->attributes[$dbColumn];
// It was removed
if (array_key_exists($dbColumn, $this->original) && ! array_key_exists($dbColumn, $this->attributes)) {
return true;
}

$originalValue = $this->original[$dbColumn];
$currentValue = $this->attributes[$dbColumn];

// If original is a string, it was JSON-encoded (object or array)
if (is_string($originalValue) && (is_object($currentValue) || is_array($currentValue))) {
return $originalValue !== json_encode($this->normalizeValue($currentValue), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

// For scalars, use direct comparison
return $originalValue !== $currentValue;
}

/**
* Recursively normalize a value for comparison.
* Converts objects and arrays to a JSON-encodable format.
*/
private function normalizeValue(mixed $data): mixed
{
if (is_array($data)) {
$normalized = [];

foreach ($data as $key => $value) {
$normalized[$key] = $this->normalizeValue($value);
}

return $normalized;
}

if (is_object($data)) {
// Check for Entity instance (use raw values, recursive)
if ($data instanceof Entity) {
$objectData = $data->toRawArray(false, true);
} elseif ($data instanceof JsonSerializable) {
$objectData = $data->jsonSerialize();
} elseif (method_exists($data, 'toArray')) {
$objectData = $data->toArray();
} elseif ($data instanceof Traversable) {
$objectData = iterator_to_array($data);
} elseif ($data instanceof DateTimeInterface) {
return [
'__class' => $data::class,
'__datetime' => $data->format(DATE_RFC3339_EXTENDED),
];
} elseif ($data instanceof UnitEnum) {
return [
'__class' => $data::class,
'__enum' => $data instanceof BackedEnum ? $data->value : $data->name,
];
} else {
$objectData = get_object_vars($data);

// Fallback for value objects with __toString()
// when properties are not accessible
if ($objectData === [] && method_exists($data, '__toString')) {
return [
'__class' => $data::class,
'__string' => (string) $data,
];
}
}

return [
'__class' => $data::class,
'__data' => $this->normalizeValue($objectData),
];
}

// Return scalars and null as-is
return $data;
}

/**
Expand Down
Loading
Loading