From 43df1693cdb37fbb308b4ace5f3f1e5412a3e855 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 8 Jan 2026 18:34:09 +0300 Subject: [PATCH 01/13] refactor: Improve phpDoc, typos --- system/Entity/Entity.php | 53 +++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 745b4cbe3dae..8bf90b66ace0 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -30,7 +30,6 @@ use CodeIgniter\Entity\Cast\URICast; use CodeIgniter\Entity\Exceptions\CastException; use CodeIgniter\I18n\Time; -use DateTime; use DateTimeInterface; use Exception; use JsonSerializable; @@ -79,14 +78,14 @@ class Entity implements JsonSerializable protected $casts = []; /** - * Custom convert handlers + * Custom convert handlers. * * @var array */ protected $castHandlers = []; /** - * Default convert handlers + * Default convert handlers. * * @var array */ @@ -131,17 +130,19 @@ class Entity implements JsonSerializable protected DataCaster $dataCaster; /** - * Holds info whenever properties have to be casted + * Holds info whenever properties have to be casted. */ private bool $_cast = true; /** - * Indicates whether all attributes are scalars (for optimization) + * Indicates whether all attributes are scalars (for optimization). */ private bool $_onlyScalars = true; /** * Allows filling in Entity parameters during construction. + * + * @param array $data */ public function __construct(?array $data = null) { @@ -162,7 +163,7 @@ public function __construct(?array $data = null) * properties, using any `setCamelCasedProperty()` methods * that may or may not exist. * - * @param array $data + * @param array|bool|float|int|object|string|null> $data * * @return $this */ @@ -184,9 +185,11 @@ public function fill(?array $data = null) * of this entity as an array. All values are accessed through the * __get() magic method so will have any casts, etc applied to them. * - * @param bool $onlyChanged If true, only return values that have changed since object creation + * @param bool $onlyChanged If true, only return values that have changed since object creation. * @param bool $cast If true, properties will be cast. * @param bool $recursive If true, inner entities will be cast as array as well. + * + * @return array */ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array { @@ -227,8 +230,10 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu /** * Returns the raw values of the current attributes. * - * @param bool $onlyChanged If true, only return values that have changed since object creation + * @param bool $onlyChanged If true, only return values that have changed since object creation. * @param bool $recursive If true, inner entities will be cast as array as well. + * + * @return array */ public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array { @@ -370,8 +375,6 @@ public function syncOriginal() * Checks a property to see if it has changed since the entity * was created. Or, without a parameter, checks if any * properties have changed. - * - * @param string|null $key class property */ public function hasChanged(?string $key = null): bool { @@ -500,7 +503,9 @@ private function normalizeValue(mixed $data): mixed } /** - * Set raw data array without any mutations + * Set raw data array without any mutations. + * + * @param array $data * * @return $this */ @@ -514,7 +519,7 @@ public function injectRawData(array $data) } /** - * Set raw data array without any mutations + * Set raw data array without any mutations. * * @return $this * @@ -527,9 +532,9 @@ public function setAttributes(array $data) /** * Checks the datamap to see if this property name is being mapped, - * and returns the db column name, if any, or the original property name. + * and returns the DB column name, if any, or the original property name. * - * @return string db column name + * @return string Database column name. */ protected function mapProperty(string $key) { @@ -537,7 +542,7 @@ protected function mapProperty(string $key) return $key; } - if (! empty($this->datamap[$key])) { + if (array_key_exists($key, $this->datamap) && $this->datamap[$key] !== '') { return $this->datamap[$key]; } @@ -545,10 +550,10 @@ protected function mapProperty(string $key) } /** - * Converts the given string|timestamp|DateTime|Time instance + * Converts the given string|timestamp|DateTimeInterface|Time instance * into the "CodeIgniter\I18n\Time" object. * - * @param DateTime|float|int|string|Time $value + * @param DateTimeInterface|float|int|string|Time $value * * @return Time * @@ -568,7 +573,7 @@ protected function mutateDate($value) * @param string $attribute Attribute name * @param string $method Allowed to "get" and "set" * - * @return array|bool|float|int|object|string|null + * @return array|bool|float|int|object|string|null * * @throws CastException */ @@ -581,9 +586,9 @@ protected function castAs($value, string $attribute, string $method = 'get') } /** - * Support for json_encode() + * Support for json_encode(). * - * @return array + * @return array */ #[ReturnTypeWillChange] public function jsonSerialize() @@ -592,7 +597,7 @@ public function jsonSerialize() } /** - * Change the value of the private $_cast property + * Change the value of the private $_cast property. * * @return bool|Entity */ @@ -616,7 +621,7 @@ public function cast(?bool $cast = null) * $this->my_property = $p; * $this->setMyProperty() = $p; * - * @param array|bool|float|int|object|string|null $value + * @param array|bool|float|int|object|string|null $value * * @return void * @@ -667,11 +672,9 @@ public function __set(string $key, $value = null) * $p = $this->my_property * $p = $this->getMyProperty() * - * @return array|bool|float|int|object|string|null + * @return array|bool|float|int|object|string|null * * @throws Exception - * - * @params string $key class property */ public function __get(string $key) { From 332b48c241bd33b629ed66dab83c48c05766d91c Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 8 Jan 2026 20:36:45 +0300 Subject: [PATCH 02/13] fix: Restore `$_cast` in `toArray()` --- system/Entity/Entity.php | 5 +++-- tests/system/Entity/EntityTest.php | 34 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 8bf90b66ace0..734954933109 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -193,7 +193,8 @@ public function fill(?array $data = null) */ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array { - $this->_cast = $cast; + $lastCastStatus = $this->_cast; + $this->_cast = $cast; $keys = array_filter(array_keys($this->attributes), static fn ($key): bool => ! str_starts_with($key, '_')); @@ -222,7 +223,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu } } - $this->_cast = true; + $this->_cast = $lastCastStatus; return $return; } diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 42135c816ed9..1bbff9e09073 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -1078,6 +1078,40 @@ public function testAsArraySwapped(): void ], $result); } + public function testAsArrayRestoringCastStatus(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'first' => null, + ]; + protected $original = [ + 'first' => null, + ]; + protected $casts = [ + 'first' => 'integer', + ]; + }; + $entity->first = '2026 Year'; + + // Disabled casting properties, but we will allow casting in the method. + $entity->cast(false); + $beforeCast = $this->getPrivateProperty($entity, '_cast'); + $result = $entity->toArray(true, true); + $afterCast = $this->getPrivateProperty($entity, '_cast'); + + $this->assertSame($result['first'], 2026); + $this->assertSame($beforeCast, $afterCast); + + // Enabled casting properties, but we will disallow casting in the method. + $entity->cast(true); + $beforeCast = $this->getPrivateProperty($entity, '_cast'); + $result = $entity->toArray(true, false); + $afterCast = $this->getPrivateProperty($entity, '_cast'); + + $this->assertSame($result['first'], '2026 Year'); + $this->assertSame($beforeCast, $afterCast); + } + public function testDataMappingIssetSwapped(): void { $entity = $this->getSimpleSwappedEntity(); From 7d290ed820851edd5df7bd75df37a320b6b3df66 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 8 Jan 2026 23:32:18 +0300 Subject: [PATCH 03/13] refactor: Remove deprecated `Entity::setAttributes()` --- system/Entity/Entity.php | 14 +----- tests/system/Entity/EntityTest.php | 42 +++++++++++++--- .../Models/ValidationModelRuleGroupTest.php | 6 +-- tests/system/Models/ValidationModelTest.php | 6 +-- utils/phpstan-baseline/empty.notAllowed.neon | 5 -- .../missingType.iterableValue.neon | 50 ------------------- 6 files changed, 41 insertions(+), 82 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 734954933109..25ea3d8e43f6 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -519,18 +519,6 @@ public function injectRawData(array $data) return $this; } - /** - * Set raw data array without any mutations. - * - * @return $this - * - * @deprecated Use injectRawData() instead. - */ - public function setAttributes(array $data) - { - return $this->injectRawData($data); - } - /** * Checks the datamap to see if this property name is being mapped, * and returns the DB column name, if any, or the original property name. @@ -652,7 +640,7 @@ public function __set(string $key, $value = null) } // If a "`set` + $key" method exists, it is also a setter. - if (method_exists($this, $method) && $method !== 'setAttributes') { + if (method_exists($this, $method)) { $this->{$method}($value); return; diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 1bbff9e09073..5f3695e540ef 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -75,6 +75,32 @@ public function testSetArrayToPropertyNamedAttributes(): void $this->assertSame($expected, $entity->toRawArray()); } + public function testSetGetAttributesMethod(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'foo' => null, + 'attributes' => null, + ]; + + public function setAttributes(string $value): self + { + $this->attributes['attributes'] = $value; + + return $this; + } + + public function getAttributes(): string + { + return $this->attributes['attributes']; + } + }; + + $entity->setAttributes('attributes'); + + $this->assertSame('attributes', $entity->getAttributes()); + } + public function testSimpleSetAndGet(): void { $entity = $this->getEntity(); @@ -358,11 +384,11 @@ public function testCastIntBool(): void ]; }; - $entity->setAttributes(['active' => '1']); + $entity->injectRawData(['active' => '1']); $this->assertTrue($entity->active); - $entity->setAttributes(['active' => '0']); + $entity->injectRawData(['active' => '0']); $this->assertFalse($entity->active); @@ -1096,19 +1122,19 @@ public function testAsArrayRestoringCastStatus(): void // Disabled casting properties, but we will allow casting in the method. $entity->cast(false); $beforeCast = $this->getPrivateProperty($entity, '_cast'); - $result = $entity->toArray(true, true); - $afterCast = $this->getPrivateProperty($entity, '_cast'); + $result = $entity->toArray(true, true); + $afterCast = $this->getPrivateProperty($entity, '_cast'); - $this->assertSame($result['first'], 2026); + $this->assertSame(2026, $result['first']); $this->assertSame($beforeCast, $afterCast); // Enabled casting properties, but we will disallow casting in the method. $entity->cast(true); $beforeCast = $this->getPrivateProperty($entity, '_cast'); - $result = $entity->toArray(true, false); - $afterCast = $this->getPrivateProperty($entity, '_cast'); + $result = $entity->toArray(true, false); + $afterCast = $this->getPrivateProperty($entity, '_cast'); - $this->assertSame($result['first'], '2026 Year'); + $this->assertSame('2026 Year', $result['first']); $this->assertSame($beforeCast, $afterCast); } diff --git a/tests/system/Models/ValidationModelRuleGroupTest.php b/tests/system/Models/ValidationModelRuleGroupTest.php index 37e0ae4e3e6e..8059e1fee7d2 100644 --- a/tests/system/Models/ValidationModelRuleGroupTest.php +++ b/tests/system/Models/ValidationModelRuleGroupTest.php @@ -380,7 +380,7 @@ public function testUpdateEntityWithPropertyCleanValidationRulesTrueAndCallingCl // Simulate to get the entity from the database. $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'id' => '1', 'field1' => 'value1', 'field2' => 'value2', @@ -421,7 +421,7 @@ public function testUpdateEntityWithPropertyCleanValidationRulesFalse(): void // Simulate to get the entity from the database. $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'id' => '1', 'field1' => 'value1', 'field2' => 'value2', @@ -457,7 +457,7 @@ public function testInsertEntityValidateEntireRules(): void }; $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'field1' => 'value1', // field2 is missing 'field3' => '', diff --git a/tests/system/Models/ValidationModelTest.php b/tests/system/Models/ValidationModelTest.php index 7ec08c9e36ac..744238076e13 100644 --- a/tests/system/Models/ValidationModelTest.php +++ b/tests/system/Models/ValidationModelTest.php @@ -393,7 +393,7 @@ public function testUpdateEntityWithPropertyCleanValidationRulesTrueAndCallingCl // Simulate to get the entity from the database. $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'id' => '1', 'field1' => 'value1', 'field2' => 'value2', @@ -434,7 +434,7 @@ public function testUpdateEntityWithPropertyCleanValidationRulesFalse(): void // Simulate to get the entity from the database. $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'id' => '1', 'field1' => 'value1', 'field2' => 'value2', @@ -470,7 +470,7 @@ public function testInsertEntityValidateEntireRules(): void }; $entity = new SimpleEntity(); - $entity->setAttributes([ + $entity->injectRawData([ 'field1' => 'value1', // field2 is missing 'field3' => '', diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index c198e3caaf9e..1a9cdfdb2776 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -197,11 +197,6 @@ parameters: count: 2 path: ../../system/Encryption/Handlers/SodiumHandler.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index c4bccbba1584..f1cf09f775a0 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -2322,56 +2322,6 @@ parameters: count: 1 path: ../../system/Entity/Cast/URICast.php - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:__set\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:castAs\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:fill\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:injectRawData\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:jsonSerialize\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:setAttributes\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:toArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - - - message: '#^Method CodeIgniter\\Entity\\Entity\:\:toRawArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Entity.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 From 9ac23b4c207c6bea12b4ebb1a085d3ab57942baf Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Thu, 8 Jan 2026 23:44:42 +0300 Subject: [PATCH 04/13] docs: Changelog --- user_guide_src/source/changelogs/v4.7.0.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index c4dcb7d7d40f..7896c684f734 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -206,6 +206,7 @@ Removed Deprecated Items - **BaseModel:** The deprecated method ``transformDataRowToArray()`` has been removed. - **Cache:** The deprecated return type ``false`` for ``CodeIgniter\Cache\CacheInterface::getMetaData()`` has been replaced with ``null`` type. - **CodeIgniter:** The deprecated ``CodeIgniter\CodeIgniter::resolvePlatformExtensions()`` has been removed. +- **Entity:** The deprecated ``CodeIgniter\Entity\Entity::setAttributes()`` has been removed. Use ``CodeIgniter\Entity\Entity::injectRawData()`` instead. - **IncomingRequest:** The deprecated methods has been removed: - ``CodeIgniter\HTTP\IncomingRequest\detectURI()`` - ``CodeIgniter\HTTP\IncomingRequest\detectPath()`` @@ -250,7 +251,6 @@ Libraries - **View:** Added the ability to override namespaced views (e.g., from modules/packages) by placing a matching file structure within the **app/Views/overrides** directory. See :ref:`Overriding Namespaced Views ` for details. - **Toolbar:** Fixed an issue where the Debug Toolbar was incorrectly injected into responses generated by third-party libraries (e.g., Dompdf) that use native PHP headers instead of the framework's Response object. - Commands ======== @@ -306,7 +306,6 @@ Changes - **Paths:** Added support for changing the location of the ``.env`` file via the ``Paths::$envDirectory`` property. - **Toolbar:** Added ``$disableOnHeaders`` property to **app/Config/Toolbar.php**. - ************ Deprecations ************ From 335abe5d0a8c9e3861d4bf412fc7397fd1d2c107 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 10 Jan 2026 14:52:08 +0300 Subject: [PATCH 05/13] fix: Use `$this->cast()` in EntityTest --- tests/system/Entity/EntityTest.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 5f3695e540ef..da7a47476871 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -1121,21 +1121,19 @@ public function testAsArrayRestoringCastStatus(): void // Disabled casting properties, but we will allow casting in the method. $entity->cast(false); - $beforeCast = $this->getPrivateProperty($entity, '_cast'); + $beforeCast = $entity->cast(); $result = $entity->toArray(true, true); - $afterCast = $this->getPrivateProperty($entity, '_cast'); $this->assertSame(2026, $result['first']); - $this->assertSame($beforeCast, $afterCast); + $this->assertSame($beforeCast, $entity->cast()); // Enabled casting properties, but we will disallow casting in the method. $entity->cast(true); - $beforeCast = $this->getPrivateProperty($entity, '_cast'); + $beforeCast = $entity->cast(); $result = $entity->toArray(true, false); - $afterCast = $this->getPrivateProperty($entity, '_cast'); $this->assertSame('2026 Year', $result['first']); - $this->assertSame($beforeCast, $afterCast); + $this->assertSame($beforeCast, $entity->cast()); } public function testDataMappingIssetSwapped(): void From f96cc881e81cfbf45210e5134663f263cec6036a Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 10 Jan 2026 14:53:48 +0300 Subject: [PATCH 06/13] docs: Add note to changelog --- user_guide_src/source/changelogs/v4.7.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 7896c684f734..9263043d9d61 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -322,6 +322,7 @@ Bugs Fixed - **Cookie:** The ``CookieInterface::SAMESITE_STRICT``, ``CookieInterface::SAMESITE_LAX``, and ``CookieInterface::SAMESITE_NONE`` constants are now written in ucfirst style to be consistent with usage in the rest of the framework. - **Cache:** Changed ``WincacheHandler::increment()`` and ``WincacheHandler::decrement()`` to return ``bool`` instead of ``mixed``. +- **Entity:** Calling ``CodeIgniter\Entity\Entity::toArray()`` always changed the value of ``$_cast`` to ``true``, instead of restoring the initial value. - **Toolbar:** Fixed **Maximum call stack size exceeded** crash when AJAX-like requests (HTMX, Turbo, Unpoly, etc.) were made on pages with Debug Toolbar enabled. See the repo's From 8c1b4803a786dbacc3c384647eb80e01c1e6118f Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 10 Jan 2026 16:57:23 +0300 Subject: [PATCH 07/13] refactor: Optional DataCaster creation in Entity --- system/Entity/Entity.php | 47 ++++++++++++++++----- tests/system/Entity/EntityTest.php | 34 +++++++++++++++ user_guide_src/source/changelogs/v4.7.0.rst | 8 ++++ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 25ea3d8e43f6..6d3e730bdb11 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -127,7 +127,7 @@ class Entity implements JsonSerializable /** * The data caster. */ - protected DataCaster $dataCaster; + protected ?DataCaster $dataCaster = null; /** * Holds info whenever properties have to be casted. @@ -146,12 +146,7 @@ class Entity implements JsonSerializable */ public function __construct(?array $data = null) { - $this->dataCaster = new DataCaster( - array_merge($this->defaultCastHandlers, $this->castHandlers), - null, - null, - false, - ); + $this->dataCaster = $this->dataCaster(); $this->syncOriginal(); @@ -568,10 +563,37 @@ protected function mutateDate($value) */ protected function castAs($value, string $attribute, string $method = 'get') { - return $this->dataCaster - // @TODO if $casts is readonly, we don't need the setTypes() method. - ->setTypes($this->casts) - ->castAs($value, $attribute, $method); + if ($this->dataCaster() instanceof DataCaster) { + return $this->dataCaster + // @TODO if $casts is readonly, we don't need the setTypes() method. + ->setTypes($this->casts) + ->castAs($value, $attribute, $method); + } + + return $value; + } + + /** + * This method allows you to refuse to contain an unnecessary DataCaster if you do not use casting. + */ + protected function dataCaster(): ?DataCaster + { + if (! $this->_cast) { + $this->dataCaster = null; + + return null; + } + + if (! $this->dataCaster instanceof DataCaster) { + $this->dataCaster = new DataCaster( + array_merge($this->defaultCastHandlers, $this->castHandlers), + null, + null, + false, + ); + } + + return $this->dataCaster; } /** @@ -598,6 +620,9 @@ public function cast(?bool $cast = null) $this->_cast = $cast; + // Synchronize option with DataCaster initialization + $this->dataCaster(); + return $this; } diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index da7a47476871..054091912e14 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -16,6 +16,7 @@ use ArrayIterator; use ArrayObject; use Closure; +use CodeIgniter\DataCaster\DataCaster; use CodeIgniter\Entity\Exceptions\CastException; use CodeIgniter\HTTP\URI; use CodeIgniter\I18n\Time; @@ -1485,6 +1486,39 @@ public function testJsonSerializableEntity(): void $this->assertSame(json_encode($entity->toArray()), json_encode($entity)); } + public function testDataCasterInit(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'first' => '12345', + ]; + protected $casts = [ + 'first' => 'integer', + ]; + }; + + $getDataCaster = $this->getPrivateMethodInvoker($entity, 'dataCaster'); + + $this->assertInstanceOf(DataCaster::class, $getDataCaster()); + $this->assertInstanceOf(DataCaster::class, $this->getPrivateProperty($entity, 'dataCaster')); + $this->assertSame(12345, $entity->first); + + // Disable casting, do not load DataCaster + $entity->cast(false); + $this->assertNull($getDataCaster()); + $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); + $this->assertIsString($entity->first); + + // Method castAs() depends on the $_cast option + $this->assertSame('12345', $this->getPrivateMethodInvoker($entity, 'castAs')('12345', 'first')); + + // Restore casting + $entity->cast(true); + $this->assertInstanceOf(DataCaster::class, $getDataCaster()); + $this->assertInstanceOf(DataCaster::class, $this->getPrivateProperty($entity, 'dataCaster')); + $this->assertSame(12345, $entity->first); + } + private function getEntity(): object { return new class () extends Entity { diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 9263043d9d61..548c4cbed2e4 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -111,6 +111,13 @@ parameter is ``true``. Previously, properties containing arrays were not recursi If you were relying on the old behavior where arrays remained unconverted, you will need to update your code. +Entity and DataCaster +--------------------- + +Previously, the ``DataCaster`` object was always initialized, even if you did not use the ``$_cast = false`` type casting. +Now, the object is created on-demand and deleted when type casting is disabled. +In general, the change does not break the existing process, it should be remembered that now in some cases ``$dataCaster`` may be nullable. + Encryption Handlers ------------------- @@ -302,6 +309,7 @@ Changes ******* - **Cookie:** The ``CookieInterface::EXPIRES_FORMAT`` has been changed to ``D, d M Y H:i:s T`` to follow the recommended format in RFC 7231. +- **Entity:** The protected property ``CodeIgniter\Entity\Entity::$dataCaster`` can be nullable. - **Format:** Added support for configuring ``json_encode()`` maximum depth via ``Config\Format::$jsonEncodeDepth``. - **Paths:** Added support for changing the location of the ``.env`` file via the ``Paths::$envDirectory`` property. - **Toolbar:** Added ``$disableOnHeaders`` property to **app/Config/Toolbar.php**. From 207559c4e58de9bcf8a60e98044ff3d00bf667c2 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 10 Jan 2026 16:58:59 +0300 Subject: [PATCH 08/13] fix: Allow DateTimeInterface for DatetimeCast --- system/Entity/Cast/DatetimeCast.php | 4 ++-- system/Entity/Entity.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php index 72206ca883f7..e49df121987e 100644 --- a/system/Entity/Cast/DatetimeCast.php +++ b/system/Entity/Cast/DatetimeCast.php @@ -14,7 +14,7 @@ namespace CodeIgniter\Entity\Cast; use CodeIgniter\I18n\Time; -use DateTime; +use DateTimeInterface; use Exception; class DatetimeCast extends BaseCast @@ -32,7 +32,7 @@ public static function get($value, array $params = []) return $value; } - if ($value instanceof DateTime) { + if ($value instanceof DateTimeInterface) { return Time::createFromInstance($value); } diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 6d3e730bdb11..798debfa3f8b 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -534,10 +534,10 @@ protected function mapProperty(string $key) } /** - * Converts the given string|timestamp|DateTimeInterface|Time instance + * Converts the given string|timestamp|DateTimeInterface instance * into the "CodeIgniter\I18n\Time" object. * - * @param DateTimeInterface|float|int|string|Time $value + * @param DateTimeInterface|float|int|string $value * * @return Time * From 027a0f201f9ff5b0d9a00a38d7c3ba95be0fff91 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sat, 10 Jan 2026 17:07:23 +0300 Subject: [PATCH 09/13] fix: Apply suggestion --- system/Entity/Entity.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 798debfa3f8b..1397a0e3512b 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -188,8 +188,8 @@ public function fill(?array $data = null) */ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array { - $lastCastStatus = $this->_cast; - $this->_cast = $cast; + $originalCast = $this->_cast; + $this->_cast = $cast; $keys = array_filter(array_keys($this->attributes), static fn ($key): bool => ! str_starts_with($key, '_')); @@ -218,7 +218,7 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu } } - $this->_cast = $lastCastStatus; + $this->_cast = $originalCast; return $return; } From 12aa25dad04efaecf9fc2fe576bcb5c2227e9277 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 11 Jan 2026 13:33:58 +0300 Subject: [PATCH 10/13] fix: Disable casting when an `$casts` is empty --- system/Entity/Entity.php | 2 +- user_guide_src/source/changelogs/v4.7.0.rst | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 1397a0e3512b..afc47d938799 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -578,7 +578,7 @@ protected function castAs($value, string $attribute, string $method = 'get') */ protected function dataCaster(): ?DataCaster { - if (! $this->_cast) { + if ($this->casts === [] || ! $this->_cast) { $this->dataCaster = null; return null; diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 548c4cbed2e4..e4dfe23689d0 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -114,7 +114,8 @@ your code. Entity and DataCaster --------------------- -Previously, the ``DataCaster`` object was always initialized, even if you did not use the ``$_cast = false`` type casting. +Previously, the ``DataCaster`` object was always initialized, even if you did not use the type casting: +configured ``$_cast = false`` or having an empty array ``$casts = []``. Now, the object is created on-demand and deleted when type casting is disabled. In general, the change does not break the existing process, it should be remembered that now in some cases ``$dataCaster`` may be nullable. @@ -207,6 +208,11 @@ Method Signature Changes - ``prepare(): void`` - ``respond(): void`` +Property Signature Changes +========================== + +- **Entity:** The protected property ``CodeIgniter\Entity\Entity::$dataCaster`` type has been changed from ``DataCaster`` to ``?DataCaster`` (nullable). + Removed Deprecated Items ======================== @@ -309,7 +315,6 @@ Changes ******* - **Cookie:** The ``CookieInterface::EXPIRES_FORMAT`` has been changed to ``D, d M Y H:i:s T`` to follow the recommended format in RFC 7231. -- **Entity:** The protected property ``CodeIgniter\Entity\Entity::$dataCaster`` can be nullable. - **Format:** Added support for configuring ``json_encode()`` maximum depth via ``Config\Format::$jsonEncodeDepth``. - **Paths:** Added support for changing the location of the ``.env`` file via the ``Paths::$envDirectory`` property. - **Toolbar:** Added ``$disableOnHeaders`` property to **app/Config/Toolbar.php**. From aa600102dcd24effa166630688ce9ee851969fc7 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 11 Jan 2026 17:48:04 +0300 Subject: [PATCH 11/13] fix: Return behavior $_cast in Entity --- system/Entity/Entity.php | 2 +- tests/system/Entity/EntityTest.php | 41 ++++++++++++++++++--- user_guide_src/source/changelogs/v4.7.0.rst | 6 +-- user_guide_src/source/models/entities.rst | 2 - 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index afc47d938799..22692e848474 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -578,7 +578,7 @@ protected function castAs($value, string $attribute, string $method = 'get') */ protected function dataCaster(): ?DataCaster { - if ($this->casts === [] || ! $this->_cast) { + if ($this->casts === []) { $this->dataCaster = null; return null; diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 054091912e14..7c19d9d09b89 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -1503,14 +1503,14 @@ public function testDataCasterInit(): void $this->assertInstanceOf(DataCaster::class, $this->getPrivateProperty($entity, 'dataCaster')); $this->assertSame(12345, $entity->first); - // Disable casting, do not load DataCaster + // Disable casting, but the DataCaster is initialized $entity->cast(false); - $this->assertNull($getDataCaster()); - $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); + $this->assertInstanceOf(DataCaster::class, $getDataCaster()); + $this->assertInstanceOf(DataCaster::class, $this->getPrivateProperty($entity, 'dataCaster')); $this->assertIsString($entity->first); - // Method castAs() depends on the $_cast option - $this->assertSame('12345', $this->getPrivateMethodInvoker($entity, 'castAs')('12345', 'first')); + // Method castAs() ignore on the $_cast option + $this->assertSame(12345, $this->getPrivateMethodInvoker($entity, 'castAs')('12345', 'first')); // Restore casting $entity->cast(true); @@ -1519,6 +1519,37 @@ public function testDataCasterInit(): void $this->assertSame(12345, $entity->first); } + public function testDataCasterInitEmptyCasts(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'first' => '12345', + ]; + protected $casts = []; + }; + + $getDataCaster = $this->getPrivateMethodInvoker($entity, 'dataCaster'); + + $this->assertNull($getDataCaster()); + $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); + $this->assertSame('12345', $entity->first); + + // Disable casting, the DataCaster was not initialized + $entity->cast(false); + $this->assertNull($getDataCaster()); + $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); + $this->assertSame('12345', $entity->first); + + // Method castAs() depends on the $_cast option + $this->assertSame('12345', $this->getPrivateMethodInvoker($entity, 'castAs')('12345', 'first')); + + // Restore casting + $entity->cast(true); + $this->assertNull($getDataCaster()); + $this->assertNull($this->getPrivateProperty($entity, 'dataCaster')); + $this->assertSame('12345', $entity->first); + } + private function getEntity(): object { return new class () extends Entity { diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index e4dfe23689d0..0dd3295b57d0 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -114,9 +114,9 @@ your code. Entity and DataCaster --------------------- -Previously, the ``DataCaster`` object was always initialized, even if you did not use the type casting: -configured ``$_cast = false`` or having an empty array ``$casts = []``. -Now, the object is created on-demand and deleted when type casting is disabled. +Previously, the ``DataCaster`` object was always initialized, even if you did not use the type casting ( +configured empty array ``$casts = []``). +Now, the object is created on-demand and it will be ``null`` when type casting is not configured. In general, the change does not break the existing process, it should be remembered that now in some cases ``$dataCaster`` may be nullable. Encryption Handlers diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 62daf63e4f0c..56c17114138e 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -40,8 +40,6 @@ Assume you have a database table named ``users`` that has the following schema:: password - string created_at - datetime -.. important:: ``attributes`` is a reserved word for internal use. Prior to v4.4.0, if you use it as a column name, the Entity does not work correctly. - Create the Entity Class ======================= From d2a845c93a0155ded9605591b980bb376b998ed6 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Sun, 11 Jan 2026 19:36:43 +0300 Subject: [PATCH 12/13] fix: Guide update --- user_guide_src/source/changelogs/v4.7.0.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 0dd3295b57d0..642fe0b39645 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -114,10 +114,10 @@ your code. Entity and DataCaster --------------------- -Previously, the ``DataCaster`` object was always initialized, even if you did not use the type casting ( -configured empty array ``$casts = []``). -Now, the object is created on-demand and it will be ``null`` when type casting is not configured. -In general, the change does not break the existing process, it should be remembered that now in some cases ``$dataCaster`` may be nullable. +Previously, the ``DataCaster`` object was always initialized, even when type casting was not configured (an empty ``$casts = []`` array). + +``DataCaster`` is now created on demand and will be ``null`` when type casting is not configured. +While this change should not affect typical usage, developers should be aware that ``$dataCaster`` may now be nullable in some cases. Encryption Handlers ------------------- From 6730a791a4006b2655cd324f272219446725ab08 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 12 Jan 2026 13:10:18 +0300 Subject: [PATCH 13/13] fix: Apply suggestions --- system/Entity/Entity.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 22692e848474..6541d985d33a 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -574,7 +574,8 @@ protected function castAs($value, string $attribute, string $method = 'get') } /** - * This method allows you to refuse to contain an unnecessary DataCaster if you do not use casting. + * Returns a DataCaster instance when casts are defined. + * If no casts are configured, no DataCaster is created and null is returned. */ protected function dataCaster(): ?DataCaster { @@ -620,9 +621,6 @@ public function cast(?bool $cast = null) $this->_cast = $cast; - // Synchronize option with DataCaster initialization - $this->dataCaster(); - return $this; }