Skip to content

Commit 0748aa9

Browse files
authored
feat(mapper): add two-way casters (#920)
1 parent f93fb3d commit 0748aa9

30 files changed

+482
-44
lines changed

src/Tempest/Database/src/Builder/FieldName.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ public static function make(ClassReflector $class, ?TableName $tableName = null)
2626
$tableName ??= $class->callStatic('table');
2727

2828
foreach ($class->getPublicProperties() as $property) {
29-
// Don't include the field if it's a relation
29+
// Don't include the field if it's a 1:1 or n:1 relation
3030
if ($property->getType()->matches(DatabaseModel::class)) {
3131
continue;
3232
}
3333

34+
// Don't include the field if it's a 1:n relation
35+
if ($property->getIterableType()?->matches(DatabaseModel::class)) {
36+
continue;
37+
}
38+
3439
$caster = $casterFactory->forProperty($property);
3540

3641
if ($caster !== null) {
@@ -39,13 +44,7 @@ public static function make(ClassReflector $class, ?TableName $tableName = null)
3944
continue;
4045
}
4146

42-
$type = $property->getType();
43-
44-
if ($type->isIterable()) {
45-
continue;
46-
}
47-
48-
if (! $type->isBuiltIn()) {
47+
if (! $property->getType()->isBuiltIn()) {
4948
continue;
5049
}
5150

src/Tempest/Database/src/Casters/IdCaster.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Tempest\Database\Id;
88
use Tempest\Mapper\Caster;
9+
use Tempest\Mapper\Exceptions\CannotSerializeValue;
910

1011
final readonly class IdCaster implements Caster
1112
{
@@ -17,4 +18,13 @@ public function cast(mixed $input): Id
1718

1819
return new Id($input);
1920
}
21+
22+
public function serialize(mixed $input): string
23+
{
24+
if (! $input instanceof Id) {
25+
throw new CannotSerializeValue(Id::class);
26+
}
27+
28+
return $input->id;
29+
}
2030
}

src/Tempest/Database/src/Casters/RelationCaster.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ public function cast(mixed $input): mixed
1212
{
1313
return $input;
1414
}
15+
16+
public function serialize(mixed $input): string
17+
{
18+
return $input;
19+
}
1520
}

src/Tempest/Database/src/Mappers/ModelToQueryMapper.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
use Tempest\Database\DatabaseModel;
88
use Tempest\Database\Query;
9+
use Tempest\Mapper\Casters\CasterFactory;
910
use Tempest\Mapper\Mapper;
1011
use Tempest\Reflection\ClassReflector;
1112
use function Tempest\map;
1213

1314
final readonly class ModelToQueryMapper implements Mapper
1415
{
16+
public function __construct(private CasterFactory $casterFactory) {}
17+
1518
public function canMap(mixed $from, mixed $to): bool
1619
{
1720
return $to === Query::class && $from instanceof DatabaseModel;
@@ -120,18 +123,23 @@ private function fields(DatabaseModel $model): array
120123
}
121124

122125
// 1:1 or n:1 relations
123-
$type = $property->getType();
124-
125-
if ($type->matches(DatabaseModel::class)) {
126+
if ($property->getType()->matches(DatabaseModel::class)) {
126127
continue;
127128
}
128129

129130
// 1:n relations
130-
if ($type->isIterable()) {
131+
if ($property->getIterableType()?->matches(DatabaseModel::class)) {
131132
continue;
132133
}
133134

134-
$fields[$property->getName()] = $property->getValue($model);
135+
$value = $property->getValue($model);
136+
137+
// Check if caster is available for value serialization
138+
if ($value !== null && $caster = $this->casterFactory->forProperty($property)) {
139+
$value = $caster->serialize($value);
140+
}
141+
142+
$fields[$property->getName()] = $value;
135143
}
136144

137145
return $fields;

src/Tempest/Database/src/Mappers/QueryToModelMapper.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public function canMap(mixed $from, mixed $to): bool
2424

2525
public function map(mixed $from, mixed $to): array
2626
{
27+
/** @var \Tempest\Database\Query $from */
28+
/** @var class-string<DatabaseModel> $to */
2729
$class = new ClassReflector($to);
2830
$table = $class->callStatic('table');
2931

@@ -55,7 +57,7 @@ private function parse(ClassReflector $class, DatabaseModel $model, array $row):
5557
if ($count > 3) {
5658
$property = $class->getProperty(rtrim($propertyName, '[]'));
5759

58-
if ($property->isIterable()) {
60+
if ($property->getIterableType()?->matches(DatabaseModel::class)) {
5961
$collection = $property->get($model, []);
6062
$childId = $row[$keyParts[0] . '.' . $keyParts[1] . '.id'];
6163

@@ -217,7 +219,7 @@ private function makeLazyModel(DatabaseModel $model): DatabaseModel
217219
continue;
218220
}
219221

220-
if ($property->isIterable()) {
222+
if ($property->getIterableType()?->matches(DatabaseModel::class)) {
221223
foreach ($property->get($model) as $childModel) {
222224
$this->makeLazyModel($childModel);
223225
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Database\QueryStatements;
6+
7+
use BackedEnum;
8+
use Tempest\Database\DatabaseDialect;
9+
use Tempest\Database\QueryStatement;
10+
use Tempest\Database\UnsupportedDialect;
11+
use UnitEnum;
12+
use function Tempest\Support\arr;
13+
use function Tempest\Support\str;
14+
15+
final readonly class CreateEnumTypeStatement implements QueryStatement
16+
{
17+
public function __construct(
18+
/** @var class-string<UnitEnum|BackedEnum> */
19+
private string $enumClass,
20+
) {}
21+
22+
public function compile(DatabaseDialect $dialect): string
23+
{
24+
$cases = arr($this->enumClass::cases())
25+
->map(fn (UnitEnum|BackedEnum $case) => $case instanceof BackedEnum ? $case->value : $case->name)
26+
->map(fn (string $value) => "'{$value}'");
27+
28+
return match ($dialect) {
29+
DatabaseDialect::MYSQL, DatabaseDialect::SQLITE => '',
30+
DatabaseDialect::POSTGRESQL => sprintf(
31+
<<<PSQL
32+
DO $$ BEGIN
33+
CREATE TYPE %s AS (%s);
34+
EXCEPTION
35+
WHEN duplicate_object THEN null;
36+
END $$;
37+
PSQL,
38+
str($this->enumClass)->replace('\\\\', '_'),
39+
$cases->implode(', ')
40+
),
41+
};
42+
}
43+
}

src/Tempest/Database/src/QueryStatements/CreateTableStatement.php

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
namespace Tempest\Database\QueryStatements;
66

7+
use BackedEnum;
78
use Tempest\Database\Builder\TableName;
89
use Tempest\Database\DatabaseDialect;
910
use Tempest\Database\QueryStatement;
1011
use Tempest\Support\StringHelper;
12+
use UnitEnum;
1113
use function Tempest\Support\arr;
1214
use function Tempest\Support\str;
1315

@@ -17,8 +19,7 @@ public function __construct(
1719
private readonly string $tableName,
1820
private array $statements = [],
1921
private array $indexStatements = [],
20-
) {
21-
}
22+
) {}
2223

2324
/** @param class-string<\Tempest\Database\DatabaseModel> $modelClass */
2425
public static function forModel(string $modelClass): self
@@ -39,7 +40,8 @@ public function belongsTo(
3940
OnDelete $onDelete = OnDelete::RESTRICT,
4041
OnUpdate $onUpdate = OnUpdate::NO_ACTION,
4142
bool $nullable = false,
42-
): self {
43+
): self
44+
{
4345
[$localTable, $localKey] = explode('.', $local);
4446

4547
$this->integer($localKey, nullable: $nullable);
@@ -58,7 +60,8 @@ public function text(
5860
string $name,
5961
bool $nullable = false,
6062
?string $default = null,
61-
): self {
63+
): self
64+
{
6265
$this->statements[] = new TextStatement(
6366
name: $name,
6467
nullable: $nullable,
@@ -73,7 +76,8 @@ public function varchar(
7376
int $length = 255,
7477
bool $nullable = false,
7578
?string $default = null,
76-
): self {
79+
): self
80+
{
7781
$this->statements[] = new VarcharStatement(
7882
name: $name,
7983
size: $length,
@@ -88,7 +92,8 @@ public function char(
8892
string $name,
8993
bool $nullable = false,
9094
?string $default = null,
91-
): self {
95+
): self
96+
{
9297
$this->statements[] = new CharStatement(
9398
name: $name,
9499
nullable: $nullable,
@@ -103,7 +108,8 @@ public function integer(
103108
bool $unsigned = false,
104109
bool $nullable = false,
105110
?int $default = null,
106-
): self {
111+
): self
112+
{
107113
$this->statements[] = new IntegerStatement(
108114
name: $name,
109115
unsigned: $unsigned,
@@ -118,7 +124,8 @@ public function float(
118124
string $name,
119125
bool $nullable = false,
120126
?float $default = null,
121-
): self {
127+
): self
128+
{
122129
$this->statements[] = new FloatStatement(
123130
name: $name,
124131
nullable: $nullable,
@@ -132,7 +139,8 @@ public function datetime(
132139
string $name,
133140
bool $nullable = false,
134141
?string $default = null,
135-
): self {
142+
): self
143+
{
136144
$this->statements[] = new DatetimeStatement(
137145
name: $name,
138146
nullable: $nullable,
@@ -146,7 +154,8 @@ public function date(
146154
string $name,
147155
bool $nullable = false,
148156
?string $default = null,
149-
): self {
157+
): self
158+
{
150159
$this->statements[] = new DateStatement(
151160
name: $name,
152161
nullable: $nullable,
@@ -160,7 +169,8 @@ public function boolean(
160169
string $name,
161170
bool $nullable = false,
162171
?bool $default = null,
163-
): self {
172+
): self
173+
{
164174
$this->statements[] = new BooleanStatement(
165175
name: $name,
166176
nullable: $nullable,
@@ -174,10 +184,45 @@ public function json(
174184
string $name,
175185
bool $nullable = false,
176186
?string $default = null,
177-
): self {
187+
): self
188+
{
189+
$this->statements[] = new JsonStatement(
190+
name: $name,
191+
nullable: $nullable,
192+
default: $default,
193+
);
194+
195+
return $this;
196+
}
197+
198+
public function array(
199+
string $name,
200+
bool $nullable = false,
201+
array $default = [],
202+
): self
203+
{
178204
$this->statements[] = new JsonStatement(
179205
name: $name,
180206
nullable: $nullable,
207+
default: json_encode($default),
208+
);
209+
210+
return $this;
211+
}
212+
213+
public function enum(
214+
string $name,
215+
string $enumClass,
216+
bool $nullable = false,
217+
null|UnitEnum|BackedEnum $default = null,
218+
): self
219+
{
220+
$this->statements[] = new CreateEnumTypeStatement($enumClass);
221+
222+
$this->statements[] = new EnumStatement(
223+
name: $name,
224+
enumClass: $enumClass,
225+
nullable: $nullable,
181226
default: $default,
182227
);
183228

@@ -189,7 +234,8 @@ public function set(
189234
array $values,
190235
bool $nullable = false,
191236
?string $default = null,
192-
): self {
237+
): self
238+
{
193239
$this->statements[] = new SetStatement(
194240
name: $name,
195241
values: $values,
@@ -242,9 +288,9 @@ public function compile(DatabaseDialect $dialect): string
242288

243289
if ($this->indexStatements !== []) {
244290
$createIndices = PHP_EOL . arr($this->indexStatements)
245-
->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' '))
246-
->implode(';' . PHP_EOL)
247-
->append(';');
291+
->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' '))
292+
->implode(';' . PHP_EOL)
293+
->append(';');
248294
} else {
249295
$createIndices = '';
250296
}

0 commit comments

Comments
 (0)