From c9cc31a8cf40c5e93d05e97c7a5846cbe12c176e Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 24 Nov 2023 11:51:16 +0900 Subject: [PATCH 01/35] feat: add Model field casting for find*() methods --- deptrac.yaml | 1 + system/BaseModel.php | 74 +++++++- system/Model.php | 53 +++++- tests/_support/Entity/CustomUser.php | 61 +++++++ .../Models/UserCastsTimestampModel.php | 44 +++++ .../system/Models/DataConverterModelTest.php | 164 ++++++++++++++++++ 6 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 tests/_support/Entity/CustomUser.php create mode 100644 tests/_support/Models/UserCastsTimestampModel.php create mode 100644 tests/system/Models/DataConverterModelTest.php diff --git a/deptrac.yaml b/deptrac.yaml index 1545572028e7..83d24b4eca2e 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -203,6 +203,7 @@ parameters: - I18n Model: - Database + - DataConverter - Entity - I18n - Pager diff --git a/system/BaseModel.php b/system/BaseModel.php index 99cc055ce1b0..2f848b49edef 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -19,6 +19,8 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\Query; +use CodeIgniter\DataConverter\DataConverter; +use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\I18n\Time; use CodeIgniter\Pager\Pager; @@ -99,13 +101,23 @@ abstract class BaseModel * Used by asArray() and asObject() to provide * temporary overrides of model default. * - * @var string + * @var 'array'|'object'|class-string */ protected $tempReturnType; /** * Whether we should limit fields in inserts * and updates to those available in $allowedFields or not. + * @var array [column => type] + */ + protected $casts = []; + + protected ?DataConverter $converter = null; + + /** + * If this model should use "softDeletes" and + * simply set a date when rows are deleted, or + * do hard deletes. * * @var bool */ @@ -346,6 +358,17 @@ public function __construct(?ValidationInterface $validation = null) $this->validation = $validation; $this->initialize(); + $this->createDataConverter(); + } + + /** + * Creates DataConverter instance. + */ + protected function createDataConverter(): void + { + if ($this->casts !== []) { + $this->converter = new DataConverter($this->casts); + } } /** @@ -1684,7 +1707,7 @@ public function asArray() * class vars with the same name as the collection columns, * or at least allows them to be created. * - * @param string $class Class Name + * @param 'object'|class-string $class Class Name * * @return $this */ @@ -1883,4 +1906,51 @@ public function allowEmptyInserts(bool $value = true): self return $this; } + + /** + * Takes database data array and creates a return type object. + * + * @param class-string $classname + * @param array $row Raw data from database + */ + protected function reconstructObject(string $classname, array $row): object + { + $phpData = $this->converter->fromDataSource($row); + + // "reconstruct" is a reserved method name. + if (method_exists($classname, 'reconstruct')) { + return $classname::reconstruct($phpData); + } + + $classObj = new $classname(); + + $classSet = Closure::bind(function ($key, $value) { + $this->{$key} = $value; + }, $classObj, $classname); + + foreach ($phpData as $key => $value) { + $classSet($key, $value); + } + + return $classObj; + } + + /** + * Converts database data array to return type value. + * + * @param array $row Raw data from database + * @param 'array'|'object'|class-string $returnType + */ + protected function convertToReturnType(array $row, string $returnType): array|object + { + if ($returnType === 'array') { + return $this->converter->fromDataSource($row); + } + + if ($returnType === 'object') { + return (object) $this->converter->fromDataSource($row); + } + + return $this->reconstructObject($returnType, $row); + } } diff --git a/system/Model.php b/system/Model.php index 831812048c7d..1275071a2e96 100644 --- a/system/Model.php +++ b/system/Model.php @@ -186,6 +186,11 @@ protected function doFind(bool $singleton, $id = null) { $builder = $this->builder(); + if ($this->casts !== []) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } @@ -202,6 +207,12 @@ protected function doFind(bool $singleton, $id = null) $row = $builder->get()->getResult($this->tempReturnType); } + if ($this->casts !== []) { + $row = $this->convertToReturnType($row, $returnType); + + $this->tempReturnType = $returnType; + } + return $row; } @@ -216,7 +227,15 @@ protected function doFind(bool $singleton, $id = null) */ protected function doFindColumn(string $columnName) { - return $this->select($columnName)->asArray()->find(); + $results = $this->select($columnName)->asArray()->find(); + + if ($this->casts !== []) { + foreach ($results as $i => $row) { + $results[$i] = $this->converter->fromDataSource($row); + } + } + + return $results; } /** @@ -238,13 +257,28 @@ protected function doFindAll(?int $limit = null, int $offset = 0) $builder = $this->builder(); + if ($this->casts !== []) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } - return $builder->limit($limit, $offset) + $results = $builder->limit($limit, $offset) ->get() ->getResult($this->tempReturnType); + + if ($this->casts !== []) { + foreach ($results as $i => $row) { + $results[$i] = $this->convertToReturnType($row, $returnType); + } + + $this->tempReturnType = $returnType; + } + + return $results; } /** @@ -259,6 +293,11 @@ protected function doFirst() { $builder = $this->builder(); + if ($this->casts !== []) { + $returnType = $this->tempReturnType; + $this->asArray(); + } + if ($this->tempUseSoftDeletes) { $builder->where($this->table . '.' . $this->deletedField, null); } elseif ($this->useSoftDeletes && ($builder->QBGroupBy === []) && $this->primaryKey) { @@ -271,7 +310,15 @@ protected function doFirst() $builder->orderBy($this->table . '.' . $this->primaryKey, 'asc'); } - return $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType); + $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType); + + if ($this->casts !== []) { + $row = $this->convertToReturnType($row, $returnType); + + $this->tempReturnType = $returnType; + } + + return $row; } /** diff --git a/tests/_support/Entity/CustomUser.php b/tests/_support/Entity/CustomUser.php new file mode 100644 index 000000000000..96d81e93a340 --- /dev/null +++ b/tests/_support/Entity/CustomUser.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Entity; + +use CodeIgniter\I18n\Time; +use LogicException; + +/** + * This is a custom Entity class. + */ +class CustomUser +{ + private function __construct( + private readonly int $id, + private string $name, + private string $email, + private string $country, + private readonly ?Time $created_at, + private readonly ?Time $updated_at, + ) { + } + + public static function reconstruct(array $data): static + { + return new static( + $data['id'], + $data['name'], + $data['email'], + $data['country'], + $data['created_at'], + $data['updated_at'], + ); + } + + public function __isset(string $name) + { + if (! property_exists($this, $name)) { + throw new LogicException('No such property: ' . $name); + } + + return isset($this->{$name}); + } + + public function __get(string $name) + { + if (! property_exists($this, $name)) { + throw new LogicException('No such property: ' . $name); + } + + return $this->{$name}; + } +} diff --git a/tests/_support/Models/UserCastsTimestampModel.php b/tests/_support/Models/UserCastsTimestampModel.php new file mode 100644 index 000000000000..15b19200fab0 --- /dev/null +++ b/tests/_support/Models/UserCastsTimestampModel.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Models; + +use CodeIgniter\Model; + +class UserCastsTimestampModel extends Model +{ + protected $table = 'user'; + protected $allowedFields = [ + 'name', + 'email', + 'country', + ]; + protected $casts = [ + 'id' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + protected $returnType = 'array'; + protected $useSoftDeletes = true; + protected $useTimestamps = true; + protected $dateFormat = 'datetime'; + + public function initialize() + { + parent::initialize(); + + if ($this->db->DBDriver === 'SQLSRV') { + // SQL Server returns a string like `2023-11-27 01:44:04.000`. + $this->casts['created_at'] = 'datetime[Y-m-d H:i:s.v]'; + $this->casts['updated_at'] = 'datetime[Y-m-d H:i:s.v]'; + } + } +} diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php new file mode 100644 index 000000000000..2bac53bff9ae --- /dev/null +++ b/tests/system/Models/DataConverterModelTest.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Models; + +use CodeIgniter\I18n\Time; +use Tests\Support\Entity\CustomUser; +use Tests\Support\Models\UserCastsTimestampModel; + +/** + * @group DatabaseLive + * + * @internal + */ +final class DataConverterModelTest extends LiveModelTestCase +{ + protected $migrate = true; + protected $migrateOnce = true; + protected $refresh = false; + protected $seed = ''; + + public function testFindAsArray(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->find($id); + + $this->assertIsInt($user['id']); + $this->assertInstanceOf(Time::class, $user['created_at']); + } + + /** + * @return int|string Insert ID + */ + private function prepareOneRecord(): int|string + { + $this->createModel(UserCastsTimestampModel::class); + $this->db->table('user')->truncate(); + + $data = [ + 'name' => 'John Smith', + 'email' => 'john@example.com', + 'country' => 'US', + ]; + + return $this->model->insert($data, true); + } + + public function testFindAsObject(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject()->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFindAsCustomObject(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFindAllAsArray(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->findAll(); + + $this->assertIsInt($users[0]['id']); + $this->assertInstanceOf(Time::class, $users[0]['created_at']); + $this->assertIsInt($users[1]['id']); + $this->assertInstanceOf(Time::class, $users[1]['created_at']); + } + + private function prepareTwoRecords(): void + { + $this->prepareOneRecord(); + + $data = [ + 'name' => 'Mike Smith', + 'email' => 'mike@example.com', + 'country' => 'CA', + ]; + $this->model->insert($data); + } + + public function testFindAllAsObject(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject()->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + + public function testFindAllAsCustomObject(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject(CustomUser::class)->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + + public function testFindColumn(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->findColumn('created_at'); + + $this->assertInstanceOf(Time::class, $users[0]); + $this->assertInstanceOf(Time::class, $users[1]); + } + + public function testFirstAsArray(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->first(); + + $this->assertIsInt($user['id']); + $this->assertInstanceOf(Time::class, $user['created_at']); + } + + public function testFirstAsObject(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject()->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + + public function testFirstAsCustomObject(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject(CustomUser::class)->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } +} From e37e5cd20b607a7b7bd40516a3b5f92259edca98 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 16:12:13 +0900 Subject: [PATCH 02/35] feat: add Model field casting for insert()/update() methods --- system/BaseModel.php | 38 +++++++-- tests/_support/Entity/CustomUser.php | 7 +- .../Models/UserCastsTimestampModel.php | 1 + .../system/Models/DataConverterModelTest.php | 83 ++++++++++++++++++- 4 files changed, 120 insertions(+), 9 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 2f848b49edef..fe5eaf3e3aa2 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1818,18 +1818,24 @@ protected function transformDataToArray($row, string $type): array throw DataException::forEmptyDataset($type); } + if ($this->casts !== []) { + if ($row instanceof stdClass) { + $row = (array) $row; + } elseif (is_object($row)) { + $row = $this->extractAsArray($row); + } + + $row = $this->converter->toDataSource($row); + } // If $row is using a custom class with public or protected // properties representing the collection elements, we need to grab // them as an array. - if (is_object($row) && ! $row instanceof stdClass) { - if ($type === 'update' && ! $this->updateOnlyChanged) { - $onlyChanged = false; - } + elseif (is_object($row) && ! $row instanceof stdClass) { // If it validates with entire rules, all fields are needed. - elseif ($this->skipValidation === false && $this->cleanValidationRules === false) { + if ($this->skipValidation === false && $this->cleanValidationRules === false) { $onlyChanged = false; } else { - $onlyChanged = ($type === 'update'); + $onlyChanged = ($type === 'update' && $this->updateOnlyChanged); } $row = $this->objectToArray($row, $onlyChanged, true); @@ -1935,6 +1941,26 @@ protected function reconstructObject(string $classname, array $row): object return $classObj; } + /** + * Takes an object and extract all properties as an array. + * + * @return array + */ + protected function extractAsArray(object $object): array + { + $array = (array) $object; + + $output = []; + + foreach ($array as $key => $value) { + $key = preg_replace('/\000.*\000/', '', $key); + + $output[$key] = $value; + } + + return $output; + } + /** * Converts database data array to return type value. * diff --git a/tests/_support/Entity/CustomUser.php b/tests/_support/Entity/CustomUser.php index 96d81e93a340..f168af79c496 100644 --- a/tests/_support/Entity/CustomUser.php +++ b/tests/_support/Entity/CustomUser.php @@ -22,7 +22,7 @@ class CustomUser private function __construct( private readonly int $id, private string $name, - private string $email, + private array $email, private string $country, private readonly ?Time $created_at, private readonly ?Time $updated_at, @@ -41,6 +41,11 @@ public static function reconstruct(array $data): static ); } + public function addEmail(string $email): void + { + $this->email[] = $email; + } + public function __isset(string $name) { if (! property_exists($this, $name)) { diff --git a/tests/_support/Models/UserCastsTimestampModel.php b/tests/_support/Models/UserCastsTimestampModel.php index 15b19200fab0..0776b874fa1f 100644 --- a/tests/_support/Models/UserCastsTimestampModel.php +++ b/tests/_support/Models/UserCastsTimestampModel.php @@ -23,6 +23,7 @@ class UserCastsTimestampModel extends Model ]; protected $casts = [ 'id' => 'int', + 'email' => 'json-array', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index 2bac53bff9ae..06c8eb45a708 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -47,7 +47,7 @@ private function prepareOneRecord(): int|string $data = [ 'name' => 'John Smith', - 'email' => 'john@example.com', + 'email' => ['john@example.com'], 'country' => 'US', ]; @@ -92,7 +92,7 @@ private function prepareTwoRecords(): void $data = [ 'name' => 'Mike Smith', - 'email' => 'mike@example.com', + 'email' => ['mike@example.com'], 'country' => 'CA', ]; $this->model->insert($data); @@ -161,4 +161,83 @@ public function testFirstAsCustomObject(): void $this->assertIsInt($user->id); $this->assertInstanceOf(Time::class, $user->created_at); } + + public function testInsertArray(): void + { + $this->prepareOneRecord(); + + $data = [ + 'name' => 'Joe Smith', + 'email' => ['joe@example.com'], + 'country' => 'GB', + ]; + $id = $this->model->insert($data, true); + + $user = $this->model->find($id); + $this->assertSame(['joe@example.com'], $user['email']); + } + + public function testInsertObject(): void + { + $this->prepareOneRecord(); + + $data = (object) [ + 'name' => 'Joe Smith', + 'email' => ['joe@example.com'], + 'country' => 'GB', + ]; + $id = $this->model->insert($data, true); + + $user = $this->model->find($id); + $this->assertSame(['joe@example.com'], $user['email']); + } + + public function testUpdateArray(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->find($id); + + $user['email'][] = 'private@example.org'; + $this->model->update($user['id'], $user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testUpdateObject(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->asObject()->find($id); + + $user->email[] = 'private@example.org'; + $this->model->update($user->id, $user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testUpdateCustomObject(): void + { + $id = $this->prepareOneRecord(); + /** @var CustomUser $user */ + $user = $this->model->asObject(CustomUser::class)->find($id); + + $user->addEmail('private@example.org'); + $this->model->update($user->id, $user); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } } From e3903eb3336dcd978c29e768c7a7e01837063581 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 16:23:52 +0900 Subject: [PATCH 03/35] test: add tests for save() method --- .../system/Models/DataConverterModelTest.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index 06c8eb45a708..1f24da801600 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -240,4 +240,53 @@ public function testUpdateCustomObject(): void 'private@example.org', ], $user->email); } + + public function testSaveArray(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->find($id); + + $user['email'][] = 'private@example.org'; + $this->model->save($user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testSaveObject(): void + { + $id = $this->prepareOneRecord(); + $user = $this->model->asObject()->find($id); + + $user->email[] = 'private@example.org'; + $this->model->save($user); + + $user = $this->model->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user['email']); + } + + public function testSaveCustomObject(): void + { + $id = $this->prepareOneRecord(); + /** @var CustomUser $user */ + $user = $this->model->asObject(CustomUser::class)->find($id); + + $user->addEmail('private@example.org'); + $this->model->save($user); + + $user = $this->model->asObject(CustomUser::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } } From 2345424fbea23dd219b523c3eb7c50bb18cca0b3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 16:33:44 +0900 Subject: [PATCH 04/35] refactor: extract useCasts() method --- system/BaseModel.php | 12 ++++++++++-- system/Model.php | 14 +++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index fe5eaf3e3aa2..eed3ab043d66 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -366,11 +366,19 @@ public function __construct(?ValidationInterface $validation = null) */ protected function createDataConverter(): void { - if ($this->casts !== []) { + if ($this->useCasts()) { $this->converter = new DataConverter($this->casts); } } + /** + * Are casts used? + */ + protected function useCasts(): bool + { + return $this->casts !== []; + } + /** * Initializes the instance with any additional steps. * Optionally implemented by child classes. @@ -1818,7 +1826,7 @@ protected function transformDataToArray($row, string $type): array throw DataException::forEmptyDataset($type); } - if ($this->casts !== []) { + if ($this->useCasts()) { if ($row instanceof stdClass) { $row = (array) $row; } elseif (is_object($row)) { diff --git a/system/Model.php b/system/Model.php index 1275071a2e96..740cc422de6d 100644 --- a/system/Model.php +++ b/system/Model.php @@ -186,7 +186,7 @@ protected function doFind(bool $singleton, $id = null) { $builder = $this->builder(); - if ($this->casts !== []) { + if ($this->useCasts()) { $returnType = $this->tempReturnType; $this->asArray(); } @@ -207,7 +207,7 @@ protected function doFind(bool $singleton, $id = null) $row = $builder->get()->getResult($this->tempReturnType); } - if ($this->casts !== []) { + if ($this->useCasts()) { $row = $this->convertToReturnType($row, $returnType); $this->tempReturnType = $returnType; @@ -229,7 +229,7 @@ protected function doFindColumn(string $columnName) { $results = $this->select($columnName)->asArray()->find(); - if ($this->casts !== []) { + if ($this->useCasts()) { foreach ($results as $i => $row) { $results[$i] = $this->converter->fromDataSource($row); } @@ -257,7 +257,7 @@ protected function doFindAll(?int $limit = null, int $offset = 0) $builder = $this->builder(); - if ($this->casts !== []) { + if ($this->useCasts()) { $returnType = $this->tempReturnType; $this->asArray(); } @@ -270,7 +270,7 @@ protected function doFindAll(?int $limit = null, int $offset = 0) ->get() ->getResult($this->tempReturnType); - if ($this->casts !== []) { + if ($this->useCasts()) { foreach ($results as $i => $row) { $results[$i] = $this->convertToReturnType($row, $returnType); } @@ -293,7 +293,7 @@ protected function doFirst() { $builder = $this->builder(); - if ($this->casts !== []) { + if ($this->useCasts()) { $returnType = $this->tempReturnType; $this->asArray(); } @@ -312,7 +312,7 @@ protected function doFirst() $row = $builder->limit(1, 0)->get()->getFirstRow($this->tempReturnType); - if ($this->casts !== []) { + if ($this->useCasts()) { $row = $this->convertToReturnType($row, $returnType); $this->tempReturnType = $returnType; From c94aaccf0934d0075c8b983da87eff57d5e5c7c0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 17:37:45 +0900 Subject: [PATCH 05/35] refactor: add $type property --- system/DataConverter/DataConverter.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 09088bd34b0b..e4b8dfb9d010 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -31,8 +31,10 @@ final class DataConverter * @param array $types [column => type] * @param array $castHandlers Custom convert handlers */ - public function __construct(array $types, array $castHandlers = []) - { + public function __construct( + private array $types, + array $castHandlers = [] + ) { $this->dataCaster = new DataCaster($castHandlers, $types); } From bf68d85ad41d8b3be458149c34a691386b95f0f1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 19:13:15 +0900 Subject: [PATCH 06/35] refactor: cast only type defined fields --- system/DataConverter/DataConverter.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index e4b8dfb9d010..5ab24dc1f67b 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -45,13 +45,13 @@ public function __construct( */ public function fromDataSource(array $data): array { - $output = []; - - foreach ($data as $field => $value) { - $output[$field] = $this->dataCaster->castAs($value, $field, 'get'); + foreach (array_keys($this->types) as $field) { + if (array_key_exists($field, $data)) { + $data[$field] = $this->dataCaster->castAs($data[$field], $field, 'get'); + } } - return $output; + return $data; } /** @@ -61,12 +61,12 @@ public function fromDataSource(array $data): array */ public function toDataSource(array $phpData): array { - $output = []; - - foreach ($phpData as $field => $value) { - $output[$field] = $this->dataCaster->castAs($value, $field, 'set'); + foreach (array_keys($this->types) as $field) { + if (array_key_exists($field, $phpData)) { + $phpData[$field] = $this->dataCaster->castAs($phpData[$field], $field, 'set'); + } } - return $output; + return $phpData; } } From d137bbea774af45c4c05243678885c7800495c2f Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 19:15:19 +0900 Subject: [PATCH 07/35] feat: add reconstruct() and extract() --- system/DataConverter/DataConverter.php | 120 ++++++++++++- .../DataConverter/DataConverterTest.php | 169 +++++++++++++++++- 2 files changed, 285 insertions(+), 4 deletions(-) diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 5ab24dc1f67b..c63fc39f5e4a 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -13,12 +13,16 @@ namespace CodeIgniter\DataConverter; +use Closure; use CodeIgniter\DataCaster\DataCaster; +use CodeIgniter\Entity\Entity; /** * PHP data <==> DataSource data converter * * @see \CodeIgniter\DataConverter\DataConverterTest + * + * @template TEntity of object */ final class DataConverter { @@ -28,12 +32,30 @@ final class DataConverter private DataCaster $dataCaster; /** - * @param array $types [column => type] * @param array $castHandlers Custom convert handlers */ public function __construct( + /** + * Type definitions. + * + * @var array [column => type] + */ private array $types, - array $castHandlers = [] + array $castHandlers = [], + /** + * Static reconstruct method name or closure to reconstruct an object. + * Used by reconstruct(). + * + * @phpstan-var (Closure(array): TEntity)|string|null + */ + private Closure|string|null $reconstructor = 'reconstruct', + /** + * Extract method name or closure to extract data from an object. + * Used by extract(). + * + * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null + */ + private Closure|string|null $extractor = null, ) { $this->dataCaster = new DataCaster($castHandlers, $types); } @@ -69,4 +91,98 @@ public function toDataSource(array $phpData): array return $phpData; } + + /** + * Takes database data array and creates a specified type object. + * + * @param class-string $classname + * @phpstan-param class-string $classname + * @param array $row Raw data from database + * + * @phpstan-return TEntity + */ + public function reconstruct(string $classname, array $row): object + { + $phpData = $this->fromDataSource($row); + + // Use static reconstruct method. + if (is_string($this->reconstructor) && method_exists($classname, $this->reconstructor)) { + $method = $this->reconstructor; + + return $classname::$method($phpData); + } + + // Use closure to reconstruct. + if ($this->reconstructor instanceof Closure) { + $closure = $this->reconstructor; + + return $closure($phpData); + } + + $classObj = new $classname(); + + if ($classObj instanceof Entity) { + $classObj->injectRawData($phpData); + $classObj->syncOriginal(); + + return $classObj; + } + + $classSet = Closure::bind(function ($key, $value) { + $this->{$key} = $value; + }, $classObj, $classname); + + foreach ($phpData as $key => $value) { + $classSet($key, $value); + } + + return $classObj; + } + + /** + * Takes an object and extract properties as an array. + * + * @param bool $onlyChanged Only for CodeIgniter's Entity. If true, only returns + * values that have changed since object creation. + * @param bool $recursive Only for CodeIgniter's Entity. If true, inner + * entities will be cast as array as well. + * + * @return array + */ + public function extract(object $object, bool $onlyChanged = false, bool $recursive = false): array + { + // Use extractor method. + if (is_string($this->extractor) && method_exists($object, $this->extractor)) { + $method = $this->extractor; + $row = $object->{$method}($onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + // Use closure to extract. + if ($this->extractor instanceof Closure) { + $closure = $this->extractor; + $row = $closure($object, $onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + if ($object instanceof Entity) { + $row = $object->toRawArray($onlyChanged, $recursive); + + return $this->toDataSource($row); + } + + $array = (array) $object; + + $row = []; + + foreach ($array as $key => $value) { + $key = preg_replace('/\000.*\000/', '', $key); + + $row[$key] = $value; + } + + return $this->toDataSource($row); + } } diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 3c9f55c50c8e..3d1cb10a7351 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -13,10 +13,13 @@ namespace CodeIgniter\DataConverter; +use Closure; use CodeIgniter\HTTP\URI; use CodeIgniter\I18n\Time; use CodeIgniter\Test\CIUnitTestCase; use InvalidArgumentException; +use Tests\Support\Entity\CustomUser; +use Tests\Support\Entity\User; use TypeError; /** @@ -526,8 +529,170 @@ public function testNotNullable(): void $converter->toDataSource($dbData); } - private function createDataConverter(array $types, array $handlers = []): DataConverter + private function createDataConverter( + array $types, + array $handlers = [], + Closure|string|null $reconstructor = 'reconstruct', + Closure|string|null $extractor = null + ): DataConverter { + return new DataConverter($types, $handlers, $reconstructor, $extractor); + } + + public function testReconstructObjectWithReconstructMethod() + { + $types = [ + 'id' => 'int', + 'email' => 'json-array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + $converter = $this->createDataConverter($types); + + $dbData = [ + 'id' => '1', + 'name' => 'John Smith', + 'email' => '["john@example.com"]', + 'country' => 'US', + 'created_at' => '2023-12-02 07:35:57', + 'updated_at' => '2023-12-02 07:35:57', + ]; + $obj = $converter->reconstruct(CustomUser::class, $dbData); + + $this->assertIsInt($obj->id); + $this->assertIsArray($obj->email); + $this->assertInstanceOf(Time::class, $obj->created_at); + $this->assertInstanceOf(Time::class, $obj->updated_at); + } + + public function testReconstructObjectWithClosure() { - return new DataConverter($types, $handlers); + $types = [ + 'id' => 'int', + 'email' => 'json-array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + $reconstructor = static function ($array) { + $user = new User(); + $user->fill($array); + + return $user; + }; + $converter = $this->createDataConverter($types, [], $reconstructor); + + $dbData = [ + 'id' => '1', + 'name' => 'John Smith', + 'email' => '["john@example.com"]', + 'country' => 'US', + 'created_at' => '2023-12-02 07:35:57', + 'updated_at' => '2023-12-02 07:35:57', + ]; + $obj = $converter->reconstruct(CustomUser::class, $dbData); + + $this->assertIsInt($obj->id); + $this->assertIsArray($obj->email); + $this->assertInstanceOf(Time::class, $obj->created_at); + $this->assertInstanceOf(Time::class, $obj->updated_at); + } + + public function testExtract() + { + $types = [ + 'id' => 'int', + 'email' => 'json-array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + $converter = $this->createDataConverter($types); + + $phpData = [ + 'id' => 1, + 'name' => 'John Smith', + 'email' => ['john@example.com'], + 'country' => 'US', + 'created_at' => Time::parse('2023-12-02 07:35:57'), + 'updated_at' => Time::parse('2023-12-02 07:35:57'), + ]; + $obj = CustomUser::reconstruct($phpData); + + $array = $converter->extract($obj); + + $this->assertSame([ + 'id' => 1, + 'name' => 'John Smith', + 'email' => '["john@example.com"]', + 'country' => 'US', + 'created_at' => '2023-12-02 07:35:57', + 'updated_at' => '2023-12-02 07:35:57', + ], $array); + } + + public function testExtractWithExtractMethod() + { + $types = [ + 'id' => 'int', + 'email' => 'json-array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + $converter = $this->createDataConverter($types, [], null, 'toRawArray'); + + $phpData = [ + 'id' => 1, + 'name' => 'John Smith', + 'email' => ['john@example.com'], + 'country' => 'US', + 'created_at' => Time::parse('2023-12-02 07:35:57'), + 'updated_at' => Time::parse('2023-12-02 07:35:57'), + ]; + $obj = new User($phpData); + + $array = $converter->extract($obj); + + $this->assertSame([ + 'country' => 'US', + 'id' => 1, + 'name' => 'John Smith', + 'email' => '["john@example.com"]', + 'created_at' => '2023-12-02 07:35:57', + 'updated_at' => '2023-12-02 07:35:57', + ], $array); + } + + public function testExtractWithClosure() + { + $types = [ + 'id' => 'int', + 'email' => 'json-array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + $extractor = static function ($obj) { + $array['id'] = $obj->id; + $array['name'] = $obj->name; + $array['created_at'] = $obj->created_at; + + return $array; + }; + $converter = $this->createDataConverter($types, [], null, $extractor); + + $phpData = [ + 'id' => 1, + 'name' => 'John Smith', + 'email' => ['john@example.com'], + 'country' => 'US', + 'created_at' => Time::parse('2023-12-02 07:35:57'), + 'updated_at' => Time::parse('2023-12-02 07:35:57'), + ]; + $obj = CustomUser::reconstruct($phpData); + + $array = $converter->extract($obj); + + $this->assertSame([ + 'id' => 1, + 'name' => 'John Smith', + 'created_at' => '2023-12-02 07:35:57', + ], $array); } } From 9954a2b5e480441e10900953c7e356ece6752fc0 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sat, 2 Dec 2023 13:05:33 +0900 Subject: [PATCH 08/35] refactor: use DataConverter::reconstruct(), extract() --- system/BaseModel.php | 52 ++------------------------------------------ 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index eed3ab043d66..ff1893d5761a 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1830,7 +1830,7 @@ protected function transformDataToArray($row, string $type): array if ($row instanceof stdClass) { $row = (array) $row; } elseif (is_object($row)) { - $row = $this->extractAsArray($row); + $row = $this->converter->extract($row); } $row = $this->converter->toDataSource($row); @@ -1921,54 +1921,6 @@ public function allowEmptyInserts(bool $value = true): self return $this; } - /** - * Takes database data array and creates a return type object. - * - * @param class-string $classname - * @param array $row Raw data from database - */ - protected function reconstructObject(string $classname, array $row): object - { - $phpData = $this->converter->fromDataSource($row); - - // "reconstruct" is a reserved method name. - if (method_exists($classname, 'reconstruct')) { - return $classname::reconstruct($phpData); - } - - $classObj = new $classname(); - - $classSet = Closure::bind(function ($key, $value) { - $this->{$key} = $value; - }, $classObj, $classname); - - foreach ($phpData as $key => $value) { - $classSet($key, $value); - } - - return $classObj; - } - - /** - * Takes an object and extract all properties as an array. - * - * @return array - */ - protected function extractAsArray(object $object): array - { - $array = (array) $object; - - $output = []; - - foreach ($array as $key => $value) { - $key = preg_replace('/\000.*\000/', '', $key); - - $output[$key] = $value; - } - - return $output; - } - /** * Converts database data array to return type value. * @@ -1985,6 +1937,6 @@ protected function convertToReturnType(array $row, string $returnType): array|ob return (object) $this->converter->fromDataSource($row); } - return $this->reconstructObject($returnType, $row); + return $this->converter->reconstruct($returnType, $row); } } From 7d18cdb10c833ebb8106a16121a3cea349d69a30 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 28 Nov 2023 21:08:58 +0900 Subject: [PATCH 09/35] docs: add @used-by --- system/BaseModel.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/BaseModel.php b/system/BaseModel.php index ff1893d5761a..7fc9bf52d816 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1815,6 +1815,9 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec * @throws DataException * @throws InvalidArgumentException * @throws ReflectionException + * + * @used-by insert() + * @used-by update() */ protected function transformDataToArray($row, string $type): array { From 7a041b39aa13740070289a64025893a1c7f7a1be Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 29 Nov 2023 14:43:38 +0900 Subject: [PATCH 10/35] refactor: use $reconstructor parameter --- system/BaseModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 7fc9bf52d816..f95fff62de75 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -367,7 +367,7 @@ public function __construct(?ValidationInterface $validation = null) protected function createDataConverter(): void { if ($this->useCasts()) { - $this->converter = new DataConverter($this->casts); + $this->converter = new DataConverter($this->casts, [], 'reconstruct'); } } From 3013029040ea63431c7ab7434aa7d4cf9f84e125 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 29 Nov 2023 16:39:43 +0900 Subject: [PATCH 11/35] feat: Model supports casting with Entity If you use casting in Model with Entity, do not use casting in Entity. Using both casting at the same time does not work. When using casting in Model, Entity has correct typed PHP values in the attributes. This behavior is completely different from the previous behavior. Do not expect the attributes hold raw data from database. --- system/BaseModel.php | 17 +++-- .../system/Models/DataConverterModelTest.php | 71 +++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index f95fff62de75..f79e2d0f7fef 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1829,14 +1829,23 @@ protected function transformDataToArray($row, string $type): array throw DataException::forEmptyDataset($type); } + // If it validates with entire rules, all fields are needed. + $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false) + ? false : ($type === 'update'); + if ($this->useCasts()) { - if ($row instanceof stdClass) { + if (is_array($row)) { + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof stdClass) { $row = (array) $row; + $row = $this->converter->toDataSource($row); + } elseif ($row instanceof Entity) { + $row = $this->converter->extract($row, $onlyChanged); + // Convert any Time instances to appropriate $dateFormat + $row = $this->timeToString($row); } elseif (is_object($row)) { - $row = $this->converter->extract($row); + $row = $this->converter->extract($row, $onlyChanged); } - - $row = $this->converter->toDataSource($row); } // If $row is using a custom class with public or protected // properties representing the collection elements, we need to grab diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index 1f24da801600..07921e639eeb 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -13,6 +13,7 @@ use CodeIgniter\I18n\Time; use Tests\Support\Entity\CustomUser; +use Tests\Support\Entity\User; use Tests\Support\Models\UserCastsTimestampModel; /** @@ -74,6 +75,16 @@ public function testFindAsCustomObject(): void $this->assertInstanceOf(Time::class, $user->created_at); } + public function testFindAsEntity(): void + { + $id = $this->prepareOneRecord(); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + public function testFindAllAsArray(): void { $this->prepareTwoRecords(); @@ -122,6 +133,18 @@ public function testFindAllAsCustomObject(): void $this->assertInstanceOf(Time::class, $users[1]->created_at); } + public function testFindAllAsEntity(): void + { + $this->prepareTwoRecords(); + + $users = $this->model->asObject(User::class)->findAll(); + + $this->assertIsInt($users[0]->id); + $this->assertInstanceOf(Time::class, $users[0]->created_at); + $this->assertIsInt($users[1]->id); + $this->assertInstanceOf(Time::class, $users[1]->created_at); + } + public function testFindColumn(): void { $this->prepareTwoRecords(); @@ -162,6 +185,16 @@ public function testFirstAsCustomObject(): void $this->assertInstanceOf(Time::class, $user->created_at); } + public function testFirstAsEntity(): void + { + $this->prepareTwoRecords(); + + $user = $this->model->asObject(User::class)->first(); + + $this->assertIsInt($user->id); + $this->assertInstanceOf(Time::class, $user->created_at); + } + public function testInsertArray(): void { $this->prepareOneRecord(); @@ -241,6 +274,25 @@ public function testUpdateCustomObject(): void ], $user->email); } + public function testUpdateEntity(): void + { + $id = $this->prepareOneRecord(); + /** @var User $user */ + $user = $this->model->asObject(User::class)->find($id); + + $email = $user->email; + $email[] = 'private@example.org'; + $user->email = $email; + $this->model->update($user->id, $user); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } + public function testSaveArray(): void { $id = $this->prepareOneRecord(); @@ -289,4 +341,23 @@ public function testSaveCustomObject(): void 'private@example.org', ], $user->email); } + + public function testSaveEntity(): void + { + $id = $this->prepareOneRecord(); + /** @var User $user */ + $user = $this->model->asObject(User::class)->find($id); + + $email = $user->email; + $email[] = 'private@example.org'; + $user->email = $email; + $this->model->save($user); + + $user = $this->model->asObject(User::class)->find($id); + + $this->assertSame([ + 'john@example.com', + 'private@example.org', + ], $user->email); + } } From 5cf81ef80ab40daf37ec78849703b6a17fff164b Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 30 Nov 2023 16:50:29 +0900 Subject: [PATCH 12/35] chore: add skip_violations --- deptrac.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deptrac.yaml b/deptrac.yaml index 83d24b4eca2e..9563488bf6f2 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -249,6 +249,8 @@ parameters: - CodeIgniter\Entity\Exceptions\CastException CodeIgniter\DataCaster\Exceptions\CastException: - CodeIgniter\Entity\Exceptions\CastException + CodeIgniter\DataConverter\DataConverter: + - CodeIgniter\Entity\Entity CodeIgniter\Entity\Cast\URICast: - CodeIgniter\HTTP\URI CodeIgniter\Log\Handlers\ChromeLoggerHandler: From c5dbff332bc1ff2cf88351fdad84cb2b071c1545 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 7 Dec 2023 20:05:53 +0900 Subject: [PATCH 13/35] refactor: add `declare(strict_types=1)` --- tests/_support/Entity/CustomUser.php | 2 ++ tests/_support/Models/UserCastsTimestampModel.php | 2 ++ tests/system/Models/DataConverterModelTest.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/_support/Entity/CustomUser.php b/tests/_support/Entity/CustomUser.php index f168af79c496..028566014956 100644 --- a/tests/_support/Entity/CustomUser.php +++ b/tests/_support/Entity/CustomUser.php @@ -1,5 +1,7 @@ Date: Fri, 29 Dec 2023 10:47:43 +0900 Subject: [PATCH 14/35] refactor: fix inherited method visibility --- tests/_support/Models/UserCastsTimestampModel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/_support/Models/UserCastsTimestampModel.php b/tests/_support/Models/UserCastsTimestampModel.php index 01acd18fb5e7..a08bf097bfc6 100644 --- a/tests/_support/Models/UserCastsTimestampModel.php +++ b/tests/_support/Models/UserCastsTimestampModel.php @@ -34,7 +34,7 @@ class UserCastsTimestampModel extends Model protected $useTimestamps = true; protected $dateFormat = 'datetime'; - public function initialize() + protected function initialize() { parent::initialize(); From 059b085a3d54a62d0d654ced68ef8023c7f24183 Mon Sep 17 00:00:00 2001 From: kenjis Date: Fri, 29 Dec 2023 10:48:35 +0900 Subject: [PATCH 15/35] chore: add skip to use `$created_at` --- rector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rector.php b/rector.php index 74d43d1c8aba..ca1b4cf84d85 100644 --- a/rector.php +++ b/rector.php @@ -116,9 +116,10 @@ __DIR__ . '/system/Autoloader/Autoloader.php', ], - // session handlers have the gc() method with underscored parameter `$max_lifetime` UnderscoreToCamelCaseVariableNameRector::class => [ + // session handlers have the gc() method with underscored parameter `$max_lifetime` __DIR__ . '/system/Session/Handlers', + __DIR__ . '/tests/_support/Entity/CustomUser.php', ], DeclareStrictTypesRector::class => [ From 20ef7356f7cc0bc9d786de73b32e7c804f5ddf69 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 13:52:30 +0900 Subject: [PATCH 16/35] feat: add $casts to template for make:model --- system/Commands/Generators/Views/model.tpl.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php index 2ec776d9560b..be20d38c863d 100644 --- a/system/Commands/Generators/Views/model.tpl.php +++ b/system/Commands/Generators/Views/model.tpl.php @@ -20,6 +20,8 @@ class {class} extends Model protected bool $allowEmptyInserts = false; protected bool $updateOnlyChanged = true; + protected $casts = []; + // Dates protected $useTimestamps = false; protected $dateFormat = 'datetime'; From 9a6d5dac2c0dc99a0bbf071334ac21a8c9a9beb3 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 13:53:07 +0900 Subject: [PATCH 17/35] docs: add docs --- user_guide_src/source/models/model.rst | 83 ++++++++++++++++++++++ user_guide_src/source/models/model/057.php | 17 +++++ 2 files changed, 100 insertions(+) create mode 100644 user_guide_src/source/models/model/057.php diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 9c5499029988..e5fd7fde07b2 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -183,6 +183,17 @@ with "There is no data to update." will raise. Setting this property to ``false`` will ensure that all allowed fields of an Entity are submitted to the database and updated at any time. +$casts +------ + +.. versionadded:: 4.5.0 + +This allows you to convert data retrieved from a database into the appropriate +PHP type. +This option should be an array where the key is the name of the field, and the +value is the data type. See :ref:`model-field-casting` for details. +>>>>>>> 202e3568ce (docs: add docs) + Dates ----- @@ -294,6 +305,78 @@ $afterUpdateBatch These arrays allow you to specify callback methods that will be run on the data at the time specified in the property name. See :ref:`model-events`. +.. _model-field-casting: + +Model Field Casting +******************* + +.. versionadded:: 4.5.0 + +When retrieving data from a database, data of integer type may be converted to +string type in PHP. You may also want to convert date/time data into a Time +object in PHP. + +Model Field Casting allows you to convert data retrieved from a database into +the appropriate PHP type. + +.. important:: + If you use this feature with the :doc:`Entity <./entities>`, do not use + :ref:`Entity Property Casting `. Using both casting + at the same time does not work. + + Entity Property Casting works at (1)(4), but this casting works at (2)(3):: + + [App Code] --- (1) --> [Entity] --- (2) --> [Database] + [App Code] <-- (4) --- [Entity] <-- (3) --- [Database] + + When using this casting, Entity will have correct typed PHP values in the + attributes. This behavior is completely different from the previous behavior. + Do not expect the attributes hold raw data from database. + +Defining Data Types +=================== + +The ``$casts`` property sets its definition. This option should be an array +where the key is the name of the field, and the value is the data type: + +.. literalinclude:: model/057.php + +Data Types +========== + +The following types are provided by default. Add a question mark at the beginning +of type to mark the field as nullable, i.e., ``?int``, ``?datetime``. + ++---------------+----------------+---------------------------+ +| Type | PHP Value Type | DB Column Type | ++===============+================+===========================+ +|``int`` | int | int type | ++---------------+----------------+---------------------------+ +|``float`` | float | float (numeric) type | ++---------------+----------------+---------------------------+ +|``bool`` | bool | bool/int/string type | ++---------------+----------------+---------------------------+ +|``int-bool`` | bool | int type (1 or 0) | ++---------------+----------------+---------------------------+ +|``array`` | array | string type (serialized) | ++---------------+----------------+---------------------------+ +|``csv`` | array | string type (CSV) | ++---------------+----------------+---------------------------+ +|``json`` | stdClass | json/string type | ++---------------+----------------+---------------------------+ +|``json-array`` | array | json/string type | ++---------------+----------------+---------------------------+ +|``datetime`` | Time | datetime type | ++---------------+----------------+---------------------------+ +|``timestamp`` | Time | int type (UNIX timestamp) | ++---------------+----------------+---------------------------+ +|``uri`` | URI | string type | ++---------------+----------------+---------------------------+ + +.. note:: Casting as ``csv`` uses PHP's internal ``implode()`` and ``explode()`` + functions and assumes all values are string-safe and free of commas. For more + complex data casts try ``array`` or ``json``. + Working with Data ***************** diff --git a/user_guide_src/source/models/model/057.php b/user_guide_src/source/models/model/057.php new file mode 100644 index 000000000000..6b7ca3250109 --- /dev/null +++ b/user_guide_src/source/models/model/057.php @@ -0,0 +1,17 @@ + 'int', + 'birthdate' => '?datetime', + 'hobbies' => 'json-array', + 'active' => 'int-bool', + ]; + // ... +} From 6a0019390a28762b18308f78a5c732a9099633df Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 14:02:31 +0900 Subject: [PATCH 18/35] docs: add changelog --- user_guide_src/source/changelogs/v4.5.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.5.0.rst b/user_guide_src/source/changelogs/v4.5.0.rst index 591879b0ebfb..c2cadcae4418 100644 --- a/user_guide_src/source/changelogs/v4.5.0.rst +++ b/user_guide_src/source/changelogs/v4.5.0.rst @@ -295,6 +295,12 @@ Others Model ===== +Model Field Casting +------------------- + +Added a feature to convert data retrieved from a database into the appropriate +PHP type. See :ref:`model-field-casting` for details. + .. _v450-model-findall-limit-0-behavior: findAll(0) Behavior From 405a249e247cba1957ebd4a0ab1b578ff3ce82ee Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 14:38:56 +0900 Subject: [PATCH 19/35] docs: add about datetime format --- user_guide_src/source/models/model.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index e5fd7fde07b2..ad700972461d 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -373,9 +373,17 @@ of type to mark the field as nullable, i.e., ``?int``, ``?datetime``. |``uri`` | URI | string type | +---------------+----------------+---------------------------+ -.. note:: Casting as ``csv`` uses PHP's internal ``implode()`` and ``explode()`` - functions and assumes all values are string-safe and free of commas. For more - complex data casts try ``array`` or ``json``. +csv +--- + +Casting as ``csv`` uses PHP's internal ``implode()`` and ``explode()`` functions +and assumes all values are string-safe and free of commas. For more complex data +casts try ``array`` or ``json``. + +datetime +-------- + +You can set the datetime format like ``datetime[Y-m-d H:i:s.v]``. Working with Data ***************** From 303af1ddc0b532d5818f63ec31bf040b5b2a2938 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 15:00:57 +0900 Subject: [PATCH 20/35] docs: add @internal to DataCaster and DataConverter --- system/DataCaster/DataCaster.php | 6 ++++++ system/DataConverter/DataConverter.php | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index d68dd6babdc4..be47eef9231b 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -67,6 +67,8 @@ final class DataCaster /** * @param array|null $castHandlers Custom convert handlers * @param array|null $types [field => type] + * + * @internal */ public function __construct( ?array $castHandlers = null, @@ -101,6 +103,8 @@ public function __construct( * @param array $types [field => type] * * $return $this + * + * @internal */ public function setTypes(array $types): static { @@ -118,6 +122,8 @@ public function setTypes(array $types): static * @param string $field The field name * @param string $method Allowed to "get" and "set" * @phpstan-param 'get'|'set' $method + * + * @internal */ public function castAs(mixed $value, string $field, string $method = 'get'): mixed { diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index c63fc39f5e4a..4285810f0220 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -33,6 +33,8 @@ final class DataConverter /** * @param array $castHandlers Custom convert handlers + * + * @internal */ public function __construct( /** @@ -64,6 +66,8 @@ public function __construct( * Converts data from DataSource to PHP array with specified type values. * * @param array $data DataSource data + * + * @internal */ public function fromDataSource(array $data): array { @@ -80,6 +84,8 @@ public function fromDataSource(array $data): array * Converts PHP array to data for DataSource field types. * * @param array $phpData PHP data + * + * @internal */ public function toDataSource(array $phpData): array { @@ -100,6 +106,8 @@ public function toDataSource(array $phpData): array * @param array $row Raw data from database * * @phpstan-return TEntity + * + * @internal */ public function reconstruct(string $classname, array $row): object { @@ -148,6 +156,8 @@ public function reconstruct(string $classname, array $row): object * entities will be cast as array as well. * * @return array + * + * @internal */ public function extract(object $object, bool $onlyChanged = false, bool $recursive = false): array { From 5c32e46149b986e67a277ccc2a789e6172d4f0ff Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 15:56:08 +0900 Subject: [PATCH 21/35] docs: update doc comments --- system/DataCaster/DataCaster.php | 4 ++-- system/Entity/Entity.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index be47eef9231b..45ac215e41b5 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -115,8 +115,8 @@ public function setTypes(array $types): static /** * Provides the ability to cast an item as a specific data type. - * Add ? at the beginning of $type (i.e. ?string) to get `null` - * instead of casting $value if ($value === null). + * Add ? at the beginning of the type (i.e. ?string) to get `null` + * instead of casting $value when $value is null. * * @param mixed $value The value to convert * @param string $field The field name diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 61e2b0213ea1..c8ff87769a05 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -360,8 +360,8 @@ protected function mutateDate($value) /** * Provides the ability to cast an item as a specific data type. - * Add ? at the beginning of $type (i.e. ?string) to get NULL - * instead of casting $value if $value === null + * Add ? at the beginning of the type (i.e. ?string) to get `null` + * instead of casting $value when $value is null. * * @param bool|float|int|string|null $value Attribute value * @param string $attribute Attribute name From e354b40cb4033f7b0ae352a4e9dd062faf2174a9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 16:10:07 +0900 Subject: [PATCH 22/35] docs: remove @internal --- system/DataCaster/DataCaster.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index 45ac215e41b5..ea050887c29d 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -67,8 +67,6 @@ final class DataCaster /** * @param array|null $castHandlers Custom convert handlers * @param array|null $types [field => type] - * - * @internal */ public function __construct( ?array $castHandlers = null, @@ -122,8 +120,6 @@ public function setTypes(array $types): static * @param string $field The field name * @param string $method Allowed to "get" and "set" * @phpstan-param 'get'|'set' $method - * - * @internal */ public function castAs(mixed $value, string $field, string $method = 'get'): mixed { From 05889602a04ec46de36d6049dea91f7f0622a9a2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Sun, 4 Feb 2024 16:13:15 +0900 Subject: [PATCH 23/35] docs: add @TODO --- system/DataCaster/DataCaster.php | 4 +++- system/Entity/Entity.php | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index ea050887c29d..f907712328dd 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -98,9 +98,11 @@ public function __construct( /** * This method is only for Entity. * + * @TODO if Entity::$casts is readonly, we don't need this method. + * * @param array $types [field => type] * - * $return $this + * @return $this * * @internal */ diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index c8ff87769a05..9a319c641247 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -374,6 +374,7 @@ 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); } From 5aefb00251a6a8f6c2bd529be647ae10bb841564 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Feb 2024 09:54:18 +0900 Subject: [PATCH 24/35] feat: add $castHandlers to BaseModel To add custom cast handlers. --- system/BaseModel.php | 18 ++++++++++++++---- system/Commands/Generators/Views/model.tpl.php | 3 ++- .../Models/UserCastsTimestampModel.php | 7 ++++++- tests/system/Models/DataConverterModelTest.php | 3 +++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index f79e2d0f7fef..1562432e9a13 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -106,11 +106,18 @@ abstract class BaseModel protected $tempReturnType; /** - * Whether we should limit fields in inserts - * and updates to those available in $allowedFields or not. + * Array of column names and the type of value to cast. + * * @var array [column => type] */ - protected $casts = []; + protected array $casts = []; + + /** + * Custom convert handlers. + * + * @var array [type => classname] + */ + protected array $castHandlers = []; protected ?DataConverter $converter = null; @@ -367,7 +374,10 @@ public function __construct(?ValidationInterface $validation = null) protected function createDataConverter(): void { if ($this->useCasts()) { - $this->converter = new DataConverter($this->casts, [], 'reconstruct'); + $this->converter = new DataConverter( + $this->casts, + $this->castHandlers + ); } } diff --git a/system/Commands/Generators/Views/model.tpl.php b/system/Commands/Generators/Views/model.tpl.php index be20d38c863d..954404f854d4 100644 --- a/system/Commands/Generators/Views/model.tpl.php +++ b/system/Commands/Generators/Views/model.tpl.php @@ -20,7 +20,8 @@ class {class} extends Model protected bool $allowEmptyInserts = false; protected bool $updateOnlyChanged = true; - protected $casts = []; + protected array $casts = []; + protected array $castHandlers = []; // Dates protected $useTimestamps = false; diff --git a/tests/_support/Models/UserCastsTimestampModel.php b/tests/_support/Models/UserCastsTimestampModel.php index a08bf097bfc6..84814b551ff3 100644 --- a/tests/_support/Models/UserCastsTimestampModel.php +++ b/tests/_support/Models/UserCastsTimestampModel.php @@ -14,6 +14,7 @@ namespace Tests\Support\Models; use CodeIgniter\Model; +use Tests\Support\Entity\Cast\CastBase64; class UserCastsTimestampModel extends Model { @@ -23,12 +24,16 @@ class UserCastsTimestampModel extends Model 'email', 'country', ]; - protected $casts = [ + protected array $casts = [ 'id' => 'int', + 'name' => 'base64', 'email' => 'json-array', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; + protected array $castHandlers = [ + 'base64' => CastBase64::class, + ]; protected $returnType = 'array'; protected $useSoftDeletes = true; protected $useTimestamps = true; diff --git a/tests/system/Models/DataConverterModelTest.php b/tests/system/Models/DataConverterModelTest.php index bddf1606149f..c22fa39b5050 100644 --- a/tests/system/Models/DataConverterModelTest.php +++ b/tests/system/Models/DataConverterModelTest.php @@ -38,6 +38,9 @@ public function testFindAsArray(): void $this->assertIsInt($user['id']); $this->assertInstanceOf(Time::class, $user['created_at']); + $this->assertSame('John Smith', $user['name']); + // `name` is cast by custom CastBase64 handler. + $this->seeInDatabase('user', ['name' => 'Sm9obiBTbWl0aA==']); } /** From 2c576be0f02d08fd1d07ebf45731fe879d7da053 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Feb 2024 10:47:57 +0900 Subject: [PATCH 25/35] fix: replace TypeError with InvalidArgumentException The framework should not throw Error. --- system/DataCaster/Cast/BaseCast.php | 4 ++-- system/DataCaster/DataCaster.php | 3 +-- tests/system/DataConverter/DataConverterTest.php | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php index e33a22d178f6..9f8959431aee 100644 --- a/system/DataCaster/Cast/BaseCast.php +++ b/system/DataCaster/Cast/BaseCast.php @@ -13,7 +13,7 @@ namespace CodeIgniter\DataCaster\Cast; -use TypeError; +use InvalidArgumentException; abstract class BaseCast implements CastInterface { @@ -34,6 +34,6 @@ protected static function invalidTypeValueError(mixed $value): never $message .= ', and its value: ' . var_export($value, true); } - throw new TypeError($message); + throw new InvalidArgumentException($message); } } diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index f907712328dd..b4442de05529 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -27,7 +27,6 @@ use CodeIgniter\Entity\Cast\CastInterface as EntityCastInterface; use CodeIgniter\Entity\Exceptions\CastException; use InvalidArgumentException; -use TypeError; final class DataCaster { @@ -147,7 +146,7 @@ public function castAs(mixed $value, string $field, string $method = 'get'): mix if ($this->strict) { $message = 'Field "' . $field . '" is not nullable, but null was passed.'; - throw new TypeError($message); + throw new InvalidArgumentException($message); } } diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 3d1cb10a7351..22a635bb5eeb 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -20,7 +20,6 @@ use InvalidArgumentException; use Tests\Support\Entity\CustomUser; use Tests\Support\Entity\User; -use TypeError; /** * @internal @@ -473,7 +472,7 @@ public function testInvalidType(): void public function testInvalidValue(): void { - $this->expectException(TypeError::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( '[CodeIgniter\DataCaster\Cast\JsonCast] Invalid value type: bool, and its value: true' ); @@ -513,7 +512,7 @@ public function testInvalidCastHandler(): void public function testNotNullable(): void { - $this->expectException(TypeError::class); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Field "remark" is not nullable, but null was passed.'); $types = [ From 411bf80a4210ab4439a3209c635110f607a1fdd7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Feb 2024 11:15:00 +0900 Subject: [PATCH 26/35] docs: add about Custom Casting --- user_guide_src/source/models/model.rst | 41 ++++++++++++++++++++++ user_guide_src/source/models/model/058.php | 34 ++++++++++++++++++ user_guide_src/source/models/model/059.php | 23 ++++++++++++ user_guide_src/source/models/model/060.php | 24 +++++++++++++ user_guide_src/source/models/model/061.php | 23 ++++++++++++ user_guide_src/source/models/model/062.php | 24 +++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 user_guide_src/source/models/model/058.php create mode 100644 user_guide_src/source/models/model/059.php create mode 100644 user_guide_src/source/models/model/060.php create mode 100644 user_guide_src/source/models/model/061.php create mode 100644 user_guide_src/source/models/model/062.php diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index ad700972461d..52307bf38105 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -385,6 +385,47 @@ datetime You can set the datetime format like ``datetime[Y-m-d H:i:s.v]``. +Custom Casting +============== + +You can define your own conversion types. + +Creating Custom Handlers +------------------------ + +At first you need to create a handler class for your type. +Let's say the class will be located in the **app/Models/Cast** directory: + +.. literalinclude:: model/058.php + +If you don't need to change values when getting or setting a value. Then just +don't implement the appropriate method: + +.. literalinclude:: model/060.php + +Registering Custom Handlers +--------------------------- + +Now you need to register it: + +.. literalinclude:: model/059.php + +Parameters +---------- + +In some cases, one type is not enough. In this situation, you can use additional +parameters. Additional parameters are indicated in square brackets and listed +with a comma like ``type[param1, param2]``. + +.. literalinclude:: model/061.php + +.. literalinclude:: model/062.php + +.. note:: If the casting type is marked as nullable like ``?bool`` and the passed + value is not null, then the parameter with the value ``nullable`` will be + passed to the casting type handler. If casting type has predefined parameters, + then ``nullable`` will be added to the end of the list. + Working with Data ***************** diff --git a/user_guide_src/source/models/model/058.php b/user_guide_src/source/models/model/058.php new file mode 100644 index 000000000000..b8543fe29a5f --- /dev/null +++ b/user_guide_src/source/models/model/058.php @@ -0,0 +1,34 @@ + 'base64', + ]; + + // Bind the type to the handler + protected array $castHandlers = [ + 'base64' => CastBase64::class, + ]; + + // ... +} diff --git a/user_guide_src/source/models/model/060.php b/user_guide_src/source/models/model/060.php new file mode 100644 index 000000000000..6dfbedffa9d5 --- /dev/null +++ b/user_guide_src/source/models/model/060.php @@ -0,0 +1,24 @@ + 'class[App\SomeClass, param2, param3]', + ]; + + // Bind the type to the handler + protected array $castHandlers = [ + 'class' => SomeHandler::class, + ]; + + // ... +} diff --git a/user_guide_src/source/models/model/062.php b/user_guide_src/source/models/model/062.php new file mode 100644 index 000000000000..67e005f7cfc4 --- /dev/null +++ b/user_guide_src/source/models/model/062.php @@ -0,0 +1,24 @@ + + * string(13) "App\SomeClass" + * [1]=> + * string(6) "param2" + * [2]=> + * string(6) "param3" + * } + */ + } +} From 464f627bca8bbc94995bc77686519bd36a4b160b Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Feb 2024 11:33:04 +0900 Subject: [PATCH 27/35] refactor: add readonly --- system/DataCaster/DataCaster.php | 2 +- system/DataConverter/DataConverter.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index b4442de05529..bbb0884dc22c 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -61,7 +61,7 @@ final class DataCaster /** * Strict mode? Set to false for casts for Entity. */ - private bool $strict; + private readonly bool $strict; /** * @param array|null $castHandlers Custom convert handlers diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 4285810f0220..68cc76eadcba 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -29,7 +29,7 @@ final class DataConverter /** * The data caster. */ - private DataCaster $dataCaster; + private readonly DataCaster $dataCaster; /** * @param array $castHandlers Custom convert handlers @@ -42,7 +42,7 @@ public function __construct( * * @var array [column => type] */ - private array $types, + private readonly array $types, array $castHandlers = [], /** * Static reconstruct method name or closure to reconstruct an object. @@ -50,14 +50,14 @@ public function __construct( * * @phpstan-var (Closure(array): TEntity)|string|null */ - private Closure|string|null $reconstructor = 'reconstruct', + private readonly Closure|string|null $reconstructor = 'reconstruct', /** * Extract method name or closure to extract data from an object. * Used by extract(). * * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null */ - private Closure|string|null $extractor = null, + private readonly Closure|string|null $extractor = null, ) { $this->dataCaster = new DataCaster($castHandlers, $types); } From 2043a515a44f55af4716a43d11a763bef89e0cd2 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 5 Feb 2024 11:54:27 +0900 Subject: [PATCH 28/35] test: add PHPDoc types To fix PhpStorm errors. --- tests/_support/Entity/CustomUser.php | 7 +++++++ tests/system/DataConverter/DataConverterTest.php | 2 ++ 2 files changed, 9 insertions(+) diff --git a/tests/_support/Entity/CustomUser.php b/tests/_support/Entity/CustomUser.php index 028566014956..b29c7c59f7f0 100644 --- a/tests/_support/Entity/CustomUser.php +++ b/tests/_support/Entity/CustomUser.php @@ -18,6 +18,13 @@ /** * This is a custom Entity class. + * + * @property string $country + * @property Time|null $created_at + * @property array $email + * @property int $id + * @property string $name + * @property Time|null $updated_at */ class CustomUser { diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 22a635bb5eeb..33adcce1b3fa 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -555,6 +555,7 @@ public function testReconstructObjectWithReconstructMethod() 'created_at' => '2023-12-02 07:35:57', 'updated_at' => '2023-12-02 07:35:57', ]; + /** @var CustomUser $obj */ $obj = $converter->reconstruct(CustomUser::class, $dbData); $this->assertIsInt($obj->id); @@ -587,6 +588,7 @@ public function testReconstructObjectWithClosure() 'created_at' => '2023-12-02 07:35:57', 'updated_at' => '2023-12-02 07:35:57', ]; + /** @var CustomUser $obj */ $obj = $converter->reconstruct(CustomUser::class, $dbData); $this->assertIsInt($obj->id); From a8b1e4ed2e840dfadca9278bbb14821cc7d011a4 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 7 Feb 2024 20:51:41 +0900 Subject: [PATCH 29/35] docs: fix rebase mistake --- user_guide_src/source/models/model.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index 52307bf38105..e9e65c8ba11a 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -192,7 +192,6 @@ This allows you to convert data retrieved from a database into the appropriate PHP type. This option should be an array where the key is the name of the field, and the value is the data type. See :ref:`model-field-casting` for details. ->>>>>>> 202e3568ce (docs: add docs) Dates ----- From 45f10a3011bad4e8d88db1c90b437ab9670a7d07 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 12 Feb 2024 11:59:42 +0900 Subject: [PATCH 30/35] feat: use $db->dateFormat in DataCaster and DataConverter --- system/BaseModel.php | 3 ++- system/DataCaster/Cast/ArrayCast.php | 14 ++++++++++---- system/DataCaster/Cast/BaseCast.php | 14 ++++++++++---- system/DataCaster/Cast/BooleanCast.php | 7 +++++-- system/DataCaster/Cast/CSVCast.php | 14 ++++++++++---- system/DataCaster/Cast/CastInterface.php | 14 ++++++++++++-- system/DataCaster/Cast/FloatCast.php | 7 +++++-- system/DataCaster/Cast/IntBoolCast.php | 14 ++++++++++---- system/DataCaster/Cast/IntegerCast.php | 7 +++++-- system/DataCaster/Cast/JsonCast.php | 14 ++++++++++---- system/DataCaster/Cast/TimestampCast.php | 14 ++++++++++---- system/DataCaster/Cast/URICast.php | 14 ++++++++++---- system/DataCaster/DataCaster.php | 9 ++++++++- system/DataConverter/DataConverter.php | 6 +++++- system/Entity/Entity.php | 1 + tests/system/DataConverter/DataConverterTest.php | 13 +++++++------ user_guide_src/source/models/model/058.php | 14 ++++++++++---- user_guide_src/source/models/model/060.php | 7 +++++-- user_guide_src/source/models/model/062.php | 7 +++++-- 19 files changed, 140 insertions(+), 53 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 1562432e9a13..d0861b003a1e 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -376,7 +376,8 @@ protected function createDataConverter(): void if ($this->useCasts()) { $this->converter = new DataConverter( $this->casts, - $this->castHandlers + $this->castHandlers, + $this->db ); } } diff --git a/system/DataCaster/Cast/ArrayCast.php b/system/DataCaster/Cast/ArrayCast.php index 8aa044f4fe71..f3fb5476b5c6 100644 --- a/system/DataCaster/Cast/ArrayCast.php +++ b/system/DataCaster/Cast/ArrayCast.php @@ -21,8 +21,11 @@ */ class ArrayCast extends BaseCast implements CastInterface { - public static function get(mixed $value, array $params = []): array - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array { if (! is_string($value)) { self::invalidTypeValueError($value); } @@ -34,8 +37,11 @@ public static function get(mixed $value, array $params = []): array return (array) $value; } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { return serialize($value); } } diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php index 9f8959431aee..c3df0efee103 100644 --- a/system/DataCaster/Cast/BaseCast.php +++ b/system/DataCaster/Cast/BaseCast.php @@ -17,13 +17,19 @@ abstract class BaseCast implements CastInterface { - public static function get(mixed $value, array $params = []): mixed - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed { return $value; } - public static function set(mixed $value, array $params = []): mixed - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed { return $value; } diff --git a/system/DataCaster/Cast/BooleanCast.php b/system/DataCaster/Cast/BooleanCast.php index 646dd447f006..e4a3fde09345 100644 --- a/system/DataCaster/Cast/BooleanCast.php +++ b/system/DataCaster/Cast/BooleanCast.php @@ -21,8 +21,11 @@ */ class BooleanCast extends BaseCast { - public static function get(mixed $value, array $params = []): bool - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): bool { // For PostgreSQL if ($value === 't') { return true; diff --git a/system/DataCaster/Cast/CSVCast.php b/system/DataCaster/Cast/CSVCast.php index 89e24259073a..42dd3709a1db 100644 --- a/system/DataCaster/Cast/CSVCast.php +++ b/system/DataCaster/Cast/CSVCast.php @@ -21,8 +21,11 @@ */ class CSVCast extends BaseCast { - public static function get(mixed $value, array $params = []): array - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array { if (! is_string($value)) { self::invalidTypeValueError($value); } @@ -30,8 +33,11 @@ public static function get(mixed $value, array $params = []): array return explode(',', $value); } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! is_array($value)) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/Cast/CastInterface.php b/system/DataCaster/Cast/CastInterface.php index eef4983ce942..ff93dc2860bc 100644 --- a/system/DataCaster/Cast/CastInterface.php +++ b/system/DataCaster/Cast/CastInterface.php @@ -20,18 +20,28 @@ interface CastInterface * * @param mixed $value Data from database driver * @param list $params Additional param + * @param object|null $helper Helper object. E.g., database connection * * @return mixed PHP native value */ - public static function get(mixed $value, array $params = []): mixed; + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed; /** * Takes a PHP value, returns its value for DataSource. * * @param mixed $value PHP native value * @param list $params Additional param + * @param object|null $helper Helper object. E.g., database connection * * @return mixed Data to pass to database driver */ - public static function set(mixed $value, array $params = []): mixed; + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed; } diff --git a/system/DataCaster/Cast/FloatCast.php b/system/DataCaster/Cast/FloatCast.php index c2c6b943adce..7ced2e2653a7 100644 --- a/system/DataCaster/Cast/FloatCast.php +++ b/system/DataCaster/Cast/FloatCast.php @@ -21,8 +21,11 @@ */ class FloatCast extends BaseCast { - public static function get(mixed $value, array $params = []): float - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): float { if (! is_float($value) && ! is_string($value)) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/Cast/IntBoolCast.php b/system/DataCaster/Cast/IntBoolCast.php index 70c2aaa57674..56977c842f0e 100644 --- a/system/DataCaster/Cast/IntBoolCast.php +++ b/system/DataCaster/Cast/IntBoolCast.php @@ -21,8 +21,11 @@ */ final class IntBoolCast extends BaseCast { - public static function get(mixed $value, array $params = []): bool - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): bool { if (! is_int($value) && ! is_string($value)) { self::invalidTypeValueError($value); } @@ -30,8 +33,11 @@ public static function get(mixed $value, array $params = []): bool return (bool) $value; } - public static function set(mixed $value, array $params = []): int - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): int { if (! is_bool($value)) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/Cast/IntegerCast.php b/system/DataCaster/Cast/IntegerCast.php index 158f61b9dfbf..e16683b1fb8f 100644 --- a/system/DataCaster/Cast/IntegerCast.php +++ b/system/DataCaster/Cast/IntegerCast.php @@ -21,8 +21,11 @@ */ class IntegerCast extends BaseCast { - public static function get(mixed $value, array $params = []): int - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): int { if (! is_string($value) && ! is_int($value)) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/Cast/JsonCast.php b/system/DataCaster/Cast/JsonCast.php index b25a4b5966aa..316070aaedee 100644 --- a/system/DataCaster/Cast/JsonCast.php +++ b/system/DataCaster/Cast/JsonCast.php @@ -25,8 +25,11 @@ */ class JsonCast extends BaseCast { - public static function get(mixed $value, array $params = []): array|stdClass - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): array|stdClass { if (! is_string($value)) { self::invalidTypeValueError($value); } @@ -44,8 +47,11 @@ public static function get(mixed $value, array $params = []): array|stdClass return $output; } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { try { $output = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } catch (JsonException $e) { diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php index 0a871a28cbc9..52a4d88f9e46 100644 --- a/system/DataCaster/Cast/TimestampCast.php +++ b/system/DataCaster/Cast/TimestampCast.php @@ -23,8 +23,11 @@ */ class TimestampCast extends BaseCast { - public static function get(mixed $value, array $params = []): Time - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): Time { if (! is_int($value) && ! is_string($value)) { self::invalidTypeValueError($value); } @@ -32,8 +35,11 @@ public static function get(mixed $value, array $params = []): Time return Time::createFromTimestamp((int) $value); } - public static function set(mixed $value, array $params = []): int - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): int { if (! $value instanceof Time) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/Cast/URICast.php b/system/DataCaster/Cast/URICast.php index cf9b58df6eac..63f4d2271a78 100644 --- a/system/DataCaster/Cast/URICast.php +++ b/system/DataCaster/Cast/URICast.php @@ -23,8 +23,11 @@ */ class URICast extends BaseCast { - public static function get(mixed $value, array $params = []): URI - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): URI { if (! is_string($value)) { self::invalidTypeValueError($value); } @@ -32,8 +35,11 @@ public static function get(mixed $value, array $params = []): URI return new URI($value); } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! $value instanceof URI) { self::invalidTypeValueError($value); } diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index bbb0884dc22c..be618934dca3 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -63,6 +63,11 @@ final class DataCaster */ private readonly bool $strict; + /** + * Helper object. + */ + private ?object $helper; + /** * @param array|null $castHandlers Custom convert handlers * @param array|null $types [field => type] @@ -70,9 +75,11 @@ final class DataCaster public function __construct( ?array $castHandlers = null, ?array $types = null, + ?object $helper = null, bool $strict = true ) { $this->castHandlers = array_merge($this->castHandlers, $castHandlers); + $this->helper = $helper; if ($types !== null) { $this->setTypes($types); @@ -187,6 +194,6 @@ public function castAs(mixed $value, string $field, string $method = 'get'): mix throw CastException::forInvalidInterface($handler); } - return $handler::$method($value, $params); + return $handler::$method($value, $params, $this->helper); } } diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 68cc76eadcba..b79ede5b08ff 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -44,6 +44,10 @@ public function __construct( */ private readonly array $types, array $castHandlers = [], + /** + * Helper object. + */ + private readonly ?object $helper = null, /** * Static reconstruct method name or closure to reconstruct an object. * Used by reconstruct(). @@ -59,7 +63,7 @@ public function __construct( */ private readonly Closure|string|null $extractor = null, ) { - $this->dataCaster = new DataCaster($castHandlers, $types); + $this->dataCaster = new DataCaster($castHandlers, $types, $this->helper); } /** diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 9a319c641247..f7242103f2ce 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -137,6 +137,7 @@ public function __construct(?array $data = null) $this->dataCaster = new DataCaster( array_merge($this->defaultCastHandlers, $this->castHandlers), null, + null, false ); diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 33adcce1b3fa..b5e5069dbdb5 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -333,7 +333,7 @@ public function testDateTimeConvertDataFromDB(): void 'id' => 'int', 'date' => 'datetime', ]; - $converter = $this->createDataConverter($types); + $converter = $this->createDataConverter($types, [], db_connect()); $dbData = [ 'id' => '1', @@ -352,7 +352,7 @@ public function testDateTimeConvertDataFromDBWithFormat(): void 'id' => 'int', 'date' => 'datetime[j-M-Y]', ]; - $converter = $this->createDataConverter($types); + $converter = $this->createDataConverter($types, [], db_connect()); $dbData = [ 'id' => '1', @@ -531,10 +531,11 @@ public function testNotNullable(): void private function createDataConverter( array $types, array $handlers = [], + ?object $helper = null, Closure|string|null $reconstructor = 'reconstruct', Closure|string|null $extractor = null ): DataConverter { - return new DataConverter($types, $handlers, $reconstructor, $extractor); + return new DataConverter($types, $handlers, $helper, $reconstructor, $extractor); } public function testReconstructObjectWithReconstructMethod() @@ -545,7 +546,7 @@ public function testReconstructObjectWithReconstructMethod() 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; - $converter = $this->createDataConverter($types); + $converter = $this->createDataConverter($types, [], db_connect()); $dbData = [ 'id' => '1', @@ -578,7 +579,7 @@ public function testReconstructObjectWithClosure() return $user; }; - $converter = $this->createDataConverter($types, [], $reconstructor); + $converter = $this->createDataConverter($types, [], db_connect(), $reconstructor); $dbData = [ 'id' => '1', @@ -676,7 +677,7 @@ public function testExtractWithClosure() return $array; }; - $converter = $this->createDataConverter($types, [], null, $extractor); + $converter = $this->createDataConverter($types, [], db_connect(), null, $extractor); $phpData = [ 'id' => 1, diff --git a/user_guide_src/source/models/model/058.php b/user_guide_src/source/models/model/058.php index b8543fe29a5f..35ba857fd04e 100644 --- a/user_guide_src/source/models/model/058.php +++ b/user_guide_src/source/models/model/058.php @@ -8,8 +8,11 @@ // The class must inherit the CodeIgniter\DataCaster\Cast\BaseCast class class CastBase64 extends BaseCast { - public static function get(mixed $value, array $params = []): string - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! is_string($value)) { self::invalidTypeValueError($value); } @@ -23,8 +26,11 @@ public static function get(mixed $value, array $params = []): string return $decoded; } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! is_string($value)) { self::invalidTypeValueError($value); } diff --git a/user_guide_src/source/models/model/060.php b/user_guide_src/source/models/model/060.php index 6dfbedffa9d5..7405f894d48b 100644 --- a/user_guide_src/source/models/model/060.php +++ b/user_guide_src/source/models/model/060.php @@ -7,8 +7,11 @@ class CastBase64 extends BaseCast { - public static function get(mixed $value, array $params = []): string - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! is_string($value)) { self::invalidTypeValueError($value); } diff --git a/user_guide_src/source/models/model/062.php b/user_guide_src/source/models/model/062.php index 67e005f7cfc4..ebf86828edbc 100644 --- a/user_guide_src/source/models/model/062.php +++ b/user_guide_src/source/models/model/062.php @@ -6,8 +6,11 @@ class SomeHandler extends BaseCast { - public static function get(mixed $value, array $params = []): mixed - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): mixed { var_dump($params); /* * Output: From 7cf4b87116d4fe1d7edbea71284049a037a5f6da Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 12 Feb 2024 12:01:11 +0900 Subject: [PATCH 31/35] feat: use $db->dateFormat in DatetimeCast --- system/DataCaster/Cast/DatetimeCast.php | 29 +++++++++++++++---- .../Models/UserCastsTimestampModel.php | 4 +-- .../DataConverter/DataConverterTest.php | 6 ++-- user_guide_src/source/models/model.rst | 7 ++++- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index 3995964198c6..83e66d02217c 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -13,7 +13,9 @@ namespace CodeIgniter\DataCaster\Cast; +use CodeIgniter\Database\BaseConnection; use CodeIgniter\I18n\Time; +use InvalidArgumentException; /** * Class DatetimeCast @@ -23,22 +25,39 @@ */ class DatetimeCast extends BaseCast { - public static function get(mixed $value, array $params = []): Time - { + public static function get( + mixed $value, + array $params = [], + ?object $helper = null + ): Time { if (! is_string($value)) { self::invalidTypeValueError($value); } + if (! $helper instanceof BaseConnection) { + $message = 'The parameter $helper must be BaseConnection.'; + + throw new InvalidArgumentException($message); + } + /** * @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters */ - $format = $params[0] ?? 'Y-m-d H:i:s'; + $format = match ($params[0] ?? '') { + '' => $helper->dateFormat['datetime'], + 'ms' => $helper->dateFormat['datetime-ms'], + 'us' => $helper->dateFormat['datetime-us'], + default => throw new InvalidArgumentException('Invalid parameter: ' . $params[0]), + }; return Time::createFromFormat($format, $value); } - public static function set(mixed $value, array $params = []): string - { + public static function set( + mixed $value, + array $params = [], + ?object $helper = null + ): string { if (! $value instanceof Time) { self::invalidTypeValueError($value); } diff --git a/tests/_support/Models/UserCastsTimestampModel.php b/tests/_support/Models/UserCastsTimestampModel.php index 84814b551ff3..a61bd33472ab 100644 --- a/tests/_support/Models/UserCastsTimestampModel.php +++ b/tests/_support/Models/UserCastsTimestampModel.php @@ -45,8 +45,8 @@ protected function initialize() if ($this->db->DBDriver === 'SQLSRV') { // SQL Server returns a string like `2023-11-27 01:44:04.000`. - $this->casts['created_at'] = 'datetime[Y-m-d H:i:s.v]'; - $this->casts['updated_at'] = 'datetime[Y-m-d H:i:s.v]'; + $this->casts['created_at'] = 'datetime[ms]'; + $this->casts['updated_at'] = 'datetime[ms]'; } } } diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index b5e5069dbdb5..acb31074b3da 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -350,18 +350,18 @@ public function testDateTimeConvertDataFromDBWithFormat(): void { $types = [ 'id' => 'int', - 'date' => 'datetime[j-M-Y]', + 'date' => 'datetime[us]', ]; $converter = $this->createDataConverter($types, [], db_connect()); $dbData = [ 'id' => '1', - 'date' => '15-Feb-2009', + 'date' => '2009-02-15 00:00:01.123456', ]; $data = $converter->fromDataSource($dbData); $this->assertInstanceOf(Time::class, $data['date']); - $expectedDate = Time::createFromFormat('j-M-Y', '15-Feb-2009'); + $expectedDate = Time::createFromFormat('Y-m-d H:i:s.u', '2009-02-15 00:00:01.123456'); $this->assertSame($expectedDate->getTimestamp(), $data['date']->getTimestamp()); } diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index e9e65c8ba11a..1ef74241e75c 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -382,7 +382,12 @@ casts try ``array`` or ``json``. datetime -------- -You can set the datetime format like ``datetime[Y-m-d H:i:s.v]``. +You can pass a parameter like ``datetime[ms]`` for date/time with milliseconds, +or ``datetime[us]`` for date/time with microseconds. + +The datetime format is set in the ``dateFormat`` array of the +:ref:`database configuration ` in the +**app/Config/Database.php** file. Custom Casting ============== From 8f0ac004641809c6065114022cc9c16a5a18bf3f Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 12 Feb 2024 13:49:29 +0900 Subject: [PATCH 32/35] chore: DataCaster depends on Database --- deptrac.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deptrac.yaml b/deptrac.yaml index 9563488bf6f2..c68b66c9adf0 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -177,6 +177,7 @@ parameters: DataCaster: - I18n - URI + - Database DataConverter: - DataCaster Email: From 94c7f087b70b6ec719bbc94b37a0cec9c81c57e7 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 13 Feb 2024 08:53:12 +0900 Subject: [PATCH 33/35] chore: skip rector bug --- rector.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rector.php b/rector.php index ca1b4cf84d85..4ac9e5e51f83 100644 --- a/rector.php +++ b/rector.php @@ -33,6 +33,7 @@ use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; +use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector; use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; @@ -95,6 +96,11 @@ JsonThrowOnErrorRector::class, YieldDataProviderRector::class, + RemoveUnusedPromotedPropertyRector::class => [ + // Bug in rector 1.0.0. See https://github.com/rectorphp/rector-src/pull/5573 + __DIR__ . '/tests/_support/Entity/CustomUser.php', + ], + RemoveUnusedPrivateMethodRector::class => [ // private method called via getPrivateMethodInvoker __DIR__ . '/tests/system/Test/ReflectionHelperTest.php', From eb2eb0a6eb443bc70994e9a47334cd316de50cce Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 13 Feb 2024 09:35:29 +0900 Subject: [PATCH 34/35] fix: take $updateOnlyChanged into account when using casts --- system/BaseModel.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index d0861b003a1e..4be1b4a27186 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1841,8 +1841,11 @@ protected function transformDataToArray($row, string $type): array } // If it validates with entire rules, all fields are needed. - $onlyChanged = ($this->skipValidation === false && $this->cleanValidationRules === false) - ? false : ($type === 'update'); + if ($this->skipValidation === false && $this->cleanValidationRules === false) { + $onlyChanged = false; + } else { + $onlyChanged = ($type === 'update' && $this->updateOnlyChanged); + } if ($this->useCasts()) { if (is_array($row)) { @@ -1862,13 +1865,6 @@ protected function transformDataToArray($row, string $type): array // properties representing the collection elements, we need to grab // them as an array. elseif (is_object($row) && ! $row instanceof stdClass) { - // If it validates with entire rules, all fields are needed. - if ($this->skipValidation === false && $this->cleanValidationRules === false) { - $onlyChanged = false; - } else { - $onlyChanged = ($type === 'update' && $this->updateOnlyChanged); - } - $row = $this->objectToArray($row, $onlyChanged, true); } From 4ea7da130e51e3d3a8175803ca41bb8ed684e483 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 13 Feb 2024 09:40:11 +0900 Subject: [PATCH 35/35] refactor: add readonly --- system/DataCaster/DataCaster.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index be618934dca3..63650408e5f8 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -66,7 +66,7 @@ final class DataCaster /** * Helper object. */ - private ?object $helper; + private readonly ?object $helper; /** * @param array|null $castHandlers Custom convert handlers