Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Model field casting #8243

Merged
merged 35 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c9cc31a
feat: add Model field casting for find*() methods
kenjis Nov 24, 2023
e37e5cd
feat: add Model field casting for insert()/update() methods
kenjis Nov 28, 2023
e3903eb
test: add tests for save() method
kenjis Nov 28, 2023
2345424
refactor: extract useCasts() method
kenjis Nov 28, 2023
c94aacc
refactor: add $type property
kenjis Nov 28, 2023
bf68d85
refactor: cast only type defined fields
kenjis Nov 28, 2023
d137bbe
feat: add reconstruct() and extract()
kenjis Nov 28, 2023
9954a2b
refactor: use DataConverter::reconstruct(), extract()
kenjis Dec 2, 2023
7d18cdb
docs: add @used-by
kenjis Nov 28, 2023
7a041b3
refactor: use $reconstructor parameter
kenjis Nov 29, 2023
3013029
feat: Model supports casting with Entity
kenjis Nov 29, 2023
5cf81ef
chore: add skip_violations
kenjis Nov 30, 2023
c5dbff3
refactor: add `declare(strict_types=1)`
kenjis Dec 7, 2023
6d43772
refactor: fix inherited method visibility
kenjis Dec 29, 2023
059b085
chore: add skip to use `$created_at`
kenjis Dec 29, 2023
20ef735
feat: add $casts to template for make:model
kenjis Feb 4, 2024
9a6d5da
docs: add docs
kenjis Feb 4, 2024
6a00193
docs: add changelog
kenjis Feb 4, 2024
405a249
docs: add about datetime format
kenjis Feb 4, 2024
303af1d
docs: add @internal to DataCaster and DataConverter
kenjis Feb 4, 2024
5c32e46
docs: update doc comments
kenjis Feb 4, 2024
e354b40
docs: remove @internal
kenjis Feb 4, 2024
0588960
docs: add @TODO
kenjis Feb 4, 2024
5aefb00
feat: add $castHandlers to BaseModel
kenjis Feb 5, 2024
2c576be
fix: replace TypeError with InvalidArgumentException
kenjis Feb 5, 2024
411bf80
docs: add about Custom Casting
kenjis Feb 5, 2024
464f627
refactor: add readonly
kenjis Feb 5, 2024
2043a51
test: add PHPDoc types
kenjis Feb 5, 2024
a8b1e4e
docs: fix rebase mistake
kenjis Feb 7, 2024
45f10a3
feat: use $db->dateFormat in DataCaster and DataConverter
kenjis Feb 12, 2024
7cf4b87
feat: use $db->dateFormat in DatetimeCast
kenjis Feb 12, 2024
8f0ac00
chore: DataCaster depends on Database
kenjis Feb 12, 2024
94c7f08
chore: skip rector bug
kenjis Feb 12, 2024
eb2eb0a
fix: take $updateOnlyChanged into account when using casts
kenjis Feb 13, 2024
4ea7da1
refactor: add readonly
kenjis Feb 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading