Skip to content

Commit

Permalink
Merge pull request #8243 from kenjis/feat-model-type-casting
Browse files Browse the repository at this point in the history
feat: add Model field casting
  • Loading branch information
kenjis authored Feb 17, 2024
2 parents 6614567 + 4ea7da1 commit eac87d1
Show file tree
Hide file tree
Showing 32 changed files with 1,416 additions and 95 deletions.
4 changes: 4 additions & 0 deletions deptrac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ parameters:
DataCaster:
- I18n
- URI
- Database
DataConverter:
- DataCaster
Email:
Expand All @@ -203,6 +204,7 @@ parameters:
- I18n
Model:
- Database
- DataConverter
- Entity
- I18n
- Pager
Expand Down Expand Up @@ -248,6 +250,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:
Expand Down
9 changes: 8 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand All @@ -116,9 +122,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 => [
Expand Down
105 changes: 90 additions & 15 deletions system/BaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,13 +101,30 @@ 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.
* Array of column names and the type of value to cast.
*
* @var array<string, string> [column => type]
*/
protected array $casts = [];

/**
* Custom convert handlers.
*
* @var array<string, class-string> [type => classname]
*/
protected array $castHandlers = [];

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
*/
Expand Down Expand Up @@ -346,6 +365,29 @@ public function __construct(?ValidationInterface $validation = null)
$this->validation = $validation;

$this->initialize();
$this->createDataConverter();
}

/**
* Creates DataConverter instance.
*/
protected function createDataConverter(): void
{
if ($this->useCasts()) {
$this->converter = new DataConverter(
$this->casts,
$this->castHandlers,
$this->db
);
}
}

/**
* Are casts used?
*/
protected function useCasts(): bool
{
return $this->casts !== [];
}

/**
Expand Down Expand Up @@ -1684,7 +1726,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
*/
Expand Down Expand Up @@ -1784,6 +1826,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
{
Expand All @@ -1795,20 +1840,31 @@ protected function transformDataToArray($row, string $type): array
throw DataException::forEmptyDataset($type);
}

// 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);
}

if ($this->useCasts()) {
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, $onlyChanged);
}
}
// 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;
}
// If it validates with entire rules, all fields are needed.
elseif ($this->skipValidation === false && $this->cleanValidationRules === false) {
$onlyChanged = false;
} else {
$onlyChanged = ($type === 'update');
}

elseif (is_object($row) && ! $row instanceof stdClass) {
$row = $this->objectToArray($row, $onlyChanged, true);
}

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

return $this;
}

/**
* Converts database data array to return type value.
*
* @param array<string, mixed> $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->converter->reconstruct($returnType, $row);
}
}
3 changes: 3 additions & 0 deletions system/Commands/Generators/Views/model.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class {class} extends Model
protected bool $allowEmptyInserts = false;
protected bool $updateOnlyChanged = true;

protected array $casts = [];
protected array $castHandlers = [];

// Dates
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
Expand Down
14 changes: 10 additions & 4 deletions system/DataCaster/Cast/ArrayCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
}
18 changes: 12 additions & 6 deletions system/DataCaster/Cast/BaseCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@

namespace CodeIgniter\DataCaster\Cast;

use TypeError;
use InvalidArgumentException;

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;
}

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

throw new TypeError($message);
throw new InvalidArgumentException($message);
}
}
7 changes: 5 additions & 2 deletions system/DataCaster/Cast/BooleanCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions system/DataCaster/Cast/CSVCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@
*/
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);
}

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);
}
Expand Down
14 changes: 12 additions & 2 deletions system/DataCaster/Cast/CastInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,28 @@ interface CastInterface
*
* @param mixed $value Data from database driver
* @param list<string> $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<string> $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;
}
Loading

0 comments on commit eac87d1

Please sign in to comment.