Skip to content

Commit eac87d1

Browse files
authored
Merge pull request #8243 from kenjis/feat-model-type-casting
feat: add Model field casting
2 parents 6614567 + 4ea7da1 commit eac87d1

32 files changed

+1416
-95
lines changed

deptrac.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ parameters:
177177
DataCaster:
178178
- I18n
179179
- URI
180+
- Database
180181
DataConverter:
181182
- DataCaster
182183
Email:
@@ -203,6 +204,7 @@ parameters:
203204
- I18n
204205
Model:
205206
- Database
207+
- DataConverter
206208
- Entity
207209
- I18n
208210
- Pager
@@ -248,6 +250,8 @@ parameters:
248250
- CodeIgniter\Entity\Exceptions\CastException
249251
CodeIgniter\DataCaster\Exceptions\CastException:
250252
- CodeIgniter\Entity\Exceptions\CastException
253+
CodeIgniter\DataConverter\DataConverter:
254+
- CodeIgniter\Entity\Entity
251255
CodeIgniter\Entity\Cast\URICast:
252256
- CodeIgniter\HTTP\URI
253257
CodeIgniter\Log\Handlers\ChromeLoggerHandler:

rector.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Rector\Config\RectorConfig;
3434
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector;
3535
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
36+
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector;
3637
use Rector\DeadCode\Rector\If_\UnwrapFutureCompatibleIfPhpVersionRector;
3738
use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector;
3839
use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector;
@@ -95,6 +96,11 @@
9596
JsonThrowOnErrorRector::class,
9697
YieldDataProviderRector::class,
9798

99+
RemoveUnusedPromotedPropertyRector::class => [
100+
// Bug in rector 1.0.0. See https://github.com/rectorphp/rector-src/pull/5573
101+
__DIR__ . '/tests/_support/Entity/CustomUser.php',
102+
],
103+
98104
RemoveUnusedPrivateMethodRector::class => [
99105
// private method called via getPrivateMethodInvoker
100106
__DIR__ . '/tests/system/Test/ReflectionHelperTest.php',
@@ -116,9 +122,10 @@
116122
__DIR__ . '/system/Autoloader/Autoloader.php',
117123
],
118124

119-
// session handlers have the gc() method with underscored parameter `$max_lifetime`
120125
UnderscoreToCamelCaseVariableNameRector::class => [
126+
// session handlers have the gc() method with underscored parameter `$max_lifetime`
121127
__DIR__ . '/system/Session/Handlers',
128+
__DIR__ . '/tests/_support/Entity/CustomUser.php',
122129
],
123130

124131
DeclareStrictTypesRector::class => [

system/BaseModel.php

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use CodeIgniter\Database\Exceptions\DatabaseException;
2020
use CodeIgniter\Database\Exceptions\DataException;
2121
use CodeIgniter\Database\Query;
22+
use CodeIgniter\DataConverter\DataConverter;
23+
use CodeIgniter\Entity\Entity;
2224
use CodeIgniter\Exceptions\ModelException;
2325
use CodeIgniter\I18n\Time;
2426
use CodeIgniter\Pager\Pager;
@@ -99,13 +101,30 @@ abstract class BaseModel
99101
* Used by asArray() and asObject() to provide
100102
* temporary overrides of model default.
101103
*
102-
* @var string
104+
* @var 'array'|'object'|class-string
103105
*/
104106
protected $tempReturnType;
105107

106108
/**
107-
* Whether we should limit fields in inserts
108-
* and updates to those available in $allowedFields or not.
109+
* Array of column names and the type of value to cast.
110+
*
111+
* @var array<string, string> [column => type]
112+
*/
113+
protected array $casts = [];
114+
115+
/**
116+
* Custom convert handlers.
117+
*
118+
* @var array<string, class-string> [type => classname]
119+
*/
120+
protected array $castHandlers = [];
121+
122+
protected ?DataConverter $converter = null;
123+
124+
/**
125+
* If this model should use "softDeletes" and
126+
* simply set a date when rows are deleted, or
127+
* do hard deletes.
109128
*
110129
* @var bool
111130
*/
@@ -346,6 +365,29 @@ public function __construct(?ValidationInterface $validation = null)
346365
$this->validation = $validation;
347366

348367
$this->initialize();
368+
$this->createDataConverter();
369+
}
370+
371+
/**
372+
* Creates DataConverter instance.
373+
*/
374+
protected function createDataConverter(): void
375+
{
376+
if ($this->useCasts()) {
377+
$this->converter = new DataConverter(
378+
$this->casts,
379+
$this->castHandlers,
380+
$this->db
381+
);
382+
}
383+
}
384+
385+
/**
386+
* Are casts used?
387+
*/
388+
protected function useCasts(): bool
389+
{
390+
return $this->casts !== [];
349391
}
350392

351393
/**
@@ -1684,7 +1726,7 @@ public function asArray()
16841726
* class vars with the same name as the collection columns,
16851727
* or at least allows them to be created.
16861728
*
1687-
* @param string $class Class Name
1729+
* @param 'object'|class-string $class Class Name
16881730
*
16891731
* @return $this
16901732
*/
@@ -1784,6 +1826,9 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec
17841826
* @throws DataException
17851827
* @throws InvalidArgumentException
17861828
* @throws ReflectionException
1829+
*
1830+
* @used-by insert()
1831+
* @used-by update()
17871832
*/
17881833
protected function transformDataToArray($row, string $type): array
17891834
{
@@ -1795,20 +1840,31 @@ protected function transformDataToArray($row, string $type): array
17951840
throw DataException::forEmptyDataset($type);
17961841
}
17971842

1843+
// If it validates with entire rules, all fields are needed.
1844+
if ($this->skipValidation === false && $this->cleanValidationRules === false) {
1845+
$onlyChanged = false;
1846+
} else {
1847+
$onlyChanged = ($type === 'update' && $this->updateOnlyChanged);
1848+
}
1849+
1850+
if ($this->useCasts()) {
1851+
if (is_array($row)) {
1852+
$row = $this->converter->toDataSource($row);
1853+
} elseif ($row instanceof stdClass) {
1854+
$row = (array) $row;
1855+
$row = $this->converter->toDataSource($row);
1856+
} elseif ($row instanceof Entity) {
1857+
$row = $this->converter->extract($row, $onlyChanged);
1858+
// Convert any Time instances to appropriate $dateFormat
1859+
$row = $this->timeToString($row);
1860+
} elseif (is_object($row)) {
1861+
$row = $this->converter->extract($row, $onlyChanged);
1862+
}
1863+
}
17981864
// If $row is using a custom class with public or protected
17991865
// properties representing the collection elements, we need to grab
18001866
// them as an array.
1801-
if (is_object($row) && ! $row instanceof stdClass) {
1802-
if ($type === 'update' && ! $this->updateOnlyChanged) {
1803-
$onlyChanged = false;
1804-
}
1805-
// If it validates with entire rules, all fields are needed.
1806-
elseif ($this->skipValidation === false && $this->cleanValidationRules === false) {
1807-
$onlyChanged = false;
1808-
} else {
1809-
$onlyChanged = ($type === 'update');
1810-
}
1811-
1867+
elseif (is_object($row) && ! $row instanceof stdClass) {
18121868
$row = $this->objectToArray($row, $onlyChanged, true);
18131869
}
18141870

@@ -1883,4 +1939,23 @@ public function allowEmptyInserts(bool $value = true): self
18831939

18841940
return $this;
18851941
}
1942+
1943+
/**
1944+
* Converts database data array to return type value.
1945+
*
1946+
* @param array<string, mixed> $row Raw data from database
1947+
* @param 'array'|'object'|class-string $returnType
1948+
*/
1949+
protected function convertToReturnType(array $row, string $returnType): array|object
1950+
{
1951+
if ($returnType === 'array') {
1952+
return $this->converter->fromDataSource($row);
1953+
}
1954+
1955+
if ($returnType === 'object') {
1956+
return (object) $this->converter->fromDataSource($row);
1957+
}
1958+
1959+
return $this->converter->reconstruct($returnType, $row);
1960+
}
18861961
}

system/Commands/Generators/Views/model.tpl.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class {class} extends Model
2020
protected bool $allowEmptyInserts = false;
2121
protected bool $updateOnlyChanged = true;
2222

23+
protected array $casts = [];
24+
protected array $castHandlers = [];
25+
2326
// Dates
2427
protected $useTimestamps = false;
2528
protected $dateFormat = 'datetime';

system/DataCaster/Cast/ArrayCast.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
*/
2222
class ArrayCast extends BaseCast implements CastInterface
2323
{
24-
public static function get(mixed $value, array $params = []): array
25-
{
24+
public static function get(
25+
mixed $value,
26+
array $params = [],
27+
?object $helper = null
28+
): array {
2629
if (! is_string($value)) {
2730
self::invalidTypeValueError($value);
2831
}
@@ -34,8 +37,11 @@ public static function get(mixed $value, array $params = []): array
3437
return (array) $value;
3538
}
3639

37-
public static function set(mixed $value, array $params = []): string
38-
{
40+
public static function set(
41+
mixed $value,
42+
array $params = [],
43+
?object $helper = null
44+
): string {
3945
return serialize($value);
4046
}
4147
}

system/DataCaster/Cast/BaseCast.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,23 @@
1313

1414
namespace CodeIgniter\DataCaster\Cast;
1515

16-
use TypeError;
16+
use InvalidArgumentException;
1717

1818
abstract class BaseCast implements CastInterface
1919
{
20-
public static function get(mixed $value, array $params = []): mixed
21-
{
20+
public static function get(
21+
mixed $value,
22+
array $params = [],
23+
?object $helper = null
24+
): mixed {
2225
return $value;
2326
}
2427

25-
public static function set(mixed $value, array $params = []): mixed
26-
{
28+
public static function set(
29+
mixed $value,
30+
array $params = [],
31+
?object $helper = null
32+
): mixed {
2733
return $value;
2834
}
2935

@@ -34,6 +40,6 @@ protected static function invalidTypeValueError(mixed $value): never
3440
$message .= ', and its value: ' . var_export($value, true);
3541
}
3642

37-
throw new TypeError($message);
43+
throw new InvalidArgumentException($message);
3844
}
3945
}

system/DataCaster/Cast/BooleanCast.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
*/
2222
class BooleanCast extends BaseCast
2323
{
24-
public static function get(mixed $value, array $params = []): bool
25-
{
24+
public static function get(
25+
mixed $value,
26+
array $params = [],
27+
?object $helper = null
28+
): bool {
2629
// For PostgreSQL
2730
if ($value === 't') {
2831
return true;

system/DataCaster/Cast/CSVCast.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,23 @@
2121
*/
2222
class CSVCast extends BaseCast
2323
{
24-
public static function get(mixed $value, array $params = []): array
25-
{
24+
public static function get(
25+
mixed $value,
26+
array $params = [],
27+
?object $helper = null
28+
): array {
2629
if (! is_string($value)) {
2730
self::invalidTypeValueError($value);
2831
}
2932

3033
return explode(',', $value);
3134
}
3235

33-
public static function set(mixed $value, array $params = []): string
34-
{
36+
public static function set(
37+
mixed $value,
38+
array $params = [],
39+
?object $helper = null
40+
): string {
3541
if (! is_array($value)) {
3642
self::invalidTypeValueError($value);
3743
}

system/DataCaster/Cast/CastInterface.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,28 @@ interface CastInterface
2020
*
2121
* @param mixed $value Data from database driver
2222
* @param list<string> $params Additional param
23+
* @param object|null $helper Helper object. E.g., database connection
2324
*
2425
* @return mixed PHP native value
2526
*/
26-
public static function get(mixed $value, array $params = []): mixed;
27+
public static function get(
28+
mixed $value,
29+
array $params = [],
30+
?object $helper = null
31+
): mixed;
2732

2833
/**
2934
* Takes a PHP value, returns its value for DataSource.
3035
*
3136
* @param mixed $value PHP native value
3237
* @param list<string> $params Additional param
38+
* @param object|null $helper Helper object. E.g., database connection
3339
*
3440
* @return mixed Data to pass to database driver
3541
*/
36-
public static function set(mixed $value, array $params = []): mixed;
42+
public static function set(
43+
mixed $value,
44+
array $params = [],
45+
?object $helper = null
46+
): mixed;
3747
}

0 commit comments

Comments
 (0)