Skip to content

Commit 54c597c

Browse files
committed
wip
1 parent 900a87d commit 54c597c

21 files changed

+467
-51
lines changed

packages/database/src/BelongsTo.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
namespace Tempest\Database;
66

77
use Attribute;
8+
use Tempest\Reflection\PropertyAttribute;
9+
use Tempest\Reflection\PropertyReflector;
810

911
#[Attribute(Attribute::TARGET_PROPERTY)]
10-
final readonly class BelongsTo
12+
final class BelongsTo implements PropertyAttribute
1113
{
14+
public PropertyReflector $property;
15+
1216
public function __construct(
1317
public string $localPropertyName,
1418
public string $inversePropertyName = 'id',

packages/database/src/Builder/ModelInspector.php

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Tempest\Database\Builder;
44

55
use ReflectionException;
6+
use Tempest\Database\BelongsTo;
67
use Tempest\Database\Config\DatabaseConfig;
8+
use Tempest\Database\HasMany;
79
use Tempest\Database\HasOne;
810
use Tempest\Database\Table;
911
use Tempest\Reflection\ClassReflector;
@@ -12,14 +14,16 @@
1214
use Tempest\Validation\Validator;
1315

1416
use function Tempest\get;
17+
use function Tempest\Support\str;
1518

1619
final class ModelInspector
1720
{
1821
private ?ClassReflector $modelClass;
1922

2023
public function __construct(
2124
private object|string $model,
22-
) {
25+
)
26+
{
2327
if ($this->model instanceof ClassReflector) {
2428
$this->modelClass = $this->model;
2529
} else {
@@ -70,7 +74,7 @@ public function getPropertyValues(): array
7074
continue;
7175
}
7276

73-
if ($this->isHasManyRelation($property->getName()) || $this->isHasOneRelation($property->getName())) {
77+
if ($this->getHasMany($property->getName()) || $this->getHasOne($property->getName())) {
7478
continue;
7579
}
7680

@@ -82,42 +86,91 @@ public function getPropertyValues(): array
8286
return $values;
8387
}
8488

85-
public function isHasManyRelation(string $name): bool
89+
public function getBelongsTo(string $name): ?BelongsTo
8690
{
8791
if (! $this->isObjectModel()) {
88-
return false;
92+
return null;
93+
}
94+
95+
$singularizedName = str($name)->singularizeLastWord();
96+
97+
if (! $singularizedName->equals($name)) {
98+
return $this->getBelongsTo($singularizedName);
8999
}
90100

91101
if (! $this->modelClass->hasProperty($name)) {
92-
return false;
102+
return null;
93103
}
94104

95105
$property = $this->modelClass->getProperty($name);
96106

97-
if ($property->getIterableType()?->isRelation()) {
98-
return true;
107+
if ($belongsTo = $property->getAttribute(BelongsTo::class)) {
108+
return $belongsTo;
109+
}
110+
111+
if (! $property->getType()->isRelation()) {
112+
return null;
113+
}
114+
115+
if ($property->hasAttribute(HasOne::class)) {
116+
return null;
99117
}
100118

101-
return false;
119+
$belongsTo = new BelongsTo($property->getName());
120+
$belongsTo->property = $property;
121+
122+
return $belongsTo;
102123
}
103124

104-
public function isHasOneRelation(string $name): bool
125+
public function getHasOne(string $name): ?HasOne
105126
{
106127
if (! $this->isObjectModel()) {
107-
return false;
128+
return null;
129+
}
130+
131+
$singularizedName = str($name)->singularizeLastWord();
132+
133+
if (! $singularizedName->equals($name)) {
134+
return $this->getHasOne($singularizedName);
108135
}
109136

110137
if (! $this->modelClass->hasProperty($name)) {
111-
return false;
138+
return null;
112139
}
113140

114141
$property = $this->modelClass->getProperty($name);
115142

116-
if ($property->hasAttribute(HasOne::class)) {
117-
return true;
143+
if ($hasOne = $property->getAttribute(HasOne::class)) {
144+
return $hasOne;
145+
}
146+
147+
return null;
148+
}
149+
150+
public function getHasMany(string $name): ?HasMany
151+
{
152+
if (! $this->isObjectModel()) {
153+
return null;
154+
}
155+
156+
if (! $this->modelClass->hasProperty($name)) {
157+
return null;
158+
}
159+
160+
$property = $this->modelClass->getProperty($name);
161+
162+
if ($hasMany = $property->getAttribute(HasMany::class)) {
163+
return $hasMany;
118164
}
119165

120-
return false;
166+
if (! $property->getIterableType()?->isRelation()) {
167+
return null;
168+
}
169+
170+
$hasMany = new HasMany(inversePropertyName: $property->getName());
171+
$hasMany->property = $property;
172+
173+
return $hasMany;
121174
}
122175

123176
public function validate(mixed ...$data): void
@@ -159,4 +212,14 @@ public function getName(): string
159212

160213
return $this->modelClass;
161214
}
215+
216+
public function getPrimaryKey(): string
217+
{
218+
return 'id';
219+
}
220+
221+
public function getPrimaryField(): string
222+
{
223+
return $this->getTableDefinition()->name . '.' . $this->getPrimaryKey();
224+
}
162225
}

packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ public function build(mixed ...$bindings): Query
5151

5252
foreach ($this->resolveData() as $data) {
5353
foreach ($data as $key => $value) {
54-
if ($definition->isHasManyRelation($key)) {
54+
if ($definition->getHasMany($key)) {
5555
throw new CannotInsertHasManyRelation($definition->getName(), $key);
5656
}
5757

58-
if ($definition->isHasOneRelation($key)) {
58+
if ($definition->getHasOne($key)) {
5959
throw new CannotInsertHasOneRelation($definition->getName(), $key);
6060
}
6161

@@ -104,7 +104,7 @@ private function resolveData(): array
104104
}
105105

106106
// HasMany and HasOne relations are skipped
107-
if ($definition->isHasManyRelation($property->getName()) || $definition->isHasOneRelation($property->getName())) {
107+
if ($definition->getHasMany($property->getName()) || $definition->getHasOne($property->getName())) {
108108
continue;
109109
}
110110

packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Tempest\Database\Builder\ModelDefinition;
1010
use Tempest\Database\Builder\TableDefinition;
1111
use Tempest\Database\Id;
12-
use Tempest\Database\Mappers\DatabaseModelMapper;
12+
use Tempest\Database\Mappers\QueryToModelMapper;
1313
use Tempest\Database\Query;
1414
use Tempest\Database\QueryStatements\JoinStatement;
1515
use Tempest\Database\QueryStatements\OrderByStatement;
@@ -68,7 +68,7 @@ public function first(mixed ...$bindings): mixed
6868

6969
$result = map($query)
7070
->collection()
71-
->with(DatabaseModelMapper::class)
71+
->with(QueryToModelMapper::class)
7272
->to($this->modelClass);
7373

7474
if ($result === []) {

packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ private function resolveValues(): ImmutableArray
100100
foreach ($this->values as $column => $value) {
101101
$property = $modelClass->getProperty($column);
102102

103-
if ($modelDefinition->isHasManyRelation($property->getName())) {
103+
if ($modelDefinition->getHasMany($property->getName())) {
104104
throw new CannotUpdateHasManyRelation($modelClass->getName(), $property->getName());
105105
}
106106

107-
if ($modelDefinition->isHasOneRelation($property->getName())) {
107+
if ($modelDefinition->getHasOne($property->getName())) {
108108
throw new CannotUpdateHasOneRelation($modelClass->getName(), $property->getName());
109109
}
110110

packages/database/src/HasMany.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@
55
namespace Tempest\Database;
66

77
use Attribute;
8+
use Tempest\Reflection\PropertyAttribute;
9+
use Tempest\Reflection\PropertyReflector;
810

911
#[Attribute(Attribute::TARGET_PROPERTY)]
10-
final readonly class HasMany
12+
final class HasMany implements PropertyAttribute
1113
{
14+
public PropertyReflector $property;
15+
16+
public string $fieldName {
17+
get => $this->property->getName() . '.' . $this->localPropertyName;
18+
}
19+
1220
/** @param null|class-string $inverseClassName */
1321
public function __construct(
1422
public string $inversePropertyName,

packages/database/src/HasOne.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
namespace Tempest\Database;
66

77
use Attribute;
8+
use Tempest\Reflection\PropertyAttribute;
9+
use Tempest\Reflection\PropertyReflector;
810

911
#[Attribute(Attribute::TARGET_PROPERTY)]
10-
final readonly class HasOne
12+
final class HasOne implements PropertyAttribute
1113
{
14+
public PropertyReflector $property;
15+
1216
public function __construct(
1317
public ?string $inversePropertyName = null,
1418
) {}

packages/database/src/Mappers/DatabaseModelMapper.php renamed to packages/database/src/Mappers/QueryToModelMapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Tempest\Reflection\PropertyReflector;
1212

1313
// TODO: refactor
14-
final readonly class DatabaseModelMapper implements Mapper
14+
final readonly class QueryToModelMapper implements Mapper
1515
{
1616
public function __construct(
1717
private CasterFactory $casterFactory,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Tempest\Database\Mappers;
4+
5+
use Tempest\Database\Builder\ModelInspector;
6+
use Tempest\Discovery\SkipDiscovery;
7+
use Tempest\Mapper\Mapper;
8+
use function Tempest\Database\model;
9+
use function Tempest\map;
10+
use function Tempest\Support\arr;
11+
12+
#[SkipDiscovery]
13+
final class SelectModelMapper implements Mapper
14+
{
15+
public function canMap(mixed $from, mixed $to): bool
16+
{
17+
return false;
18+
}
19+
20+
public function map(mixed $from, mixed $to): array
21+
{
22+
$model = model($to);
23+
24+
$idField = $model->getPrimaryField();
25+
26+
$parsed = arr($from)
27+
->groupBy(function (array $data) use ($idField) {
28+
return $data[$idField];
29+
})
30+
->map(fn (array $rows) => $this->normalizeFields($model, $rows));
31+
32+
return map($parsed->values()->toArray())->collection()->to($to);
33+
}
34+
35+
private function normalizeFields(ModelInspector $model, array $rows): array
36+
{
37+
$data = [];
38+
39+
$mainTable = $model->getTableDefinition()->name;
40+
41+
$hasManyRelations = [];
42+
43+
foreach ($rows as $row) {
44+
foreach ($row as $field => $value) {
45+
$mainField = explode('.', $field)[0];
46+
47+
// Main fields
48+
if ($mainField === $mainTable) {
49+
$data[substr($field, strlen($mainTable) + 1)] = $value;
50+
continue;
51+
}
52+
53+
// BelongsTo
54+
if ($belongsTo = $model->getBelongsTo($mainField)) {
55+
$data[$belongsTo->property->getName()][str_replace($mainField . '.', '', $field)] = $value;
56+
}
57+
58+
// HasOne
59+
if ($hasOne = $model->getHasOne($mainField)) {
60+
$data[$hasOne->property->getName()][str_replace($mainField . '.', '', $field)] = $value;
61+
}
62+
63+
// HasMany
64+
if ($hasMany = $model->getHasMany($mainField)) {
65+
$hasManyRelations[$mainField] ??= $hasMany;
66+
67+
$hasManyId = $row[$hasMany->fieldName];
68+
69+
$data[$hasMany->property->getName()][$hasManyId][str_replace($mainField . '.', '', $field)] = $value;
70+
}
71+
}
72+
}
73+
74+
foreach ($hasManyRelations as $name => $hasMany) {
75+
$data[$name] = array_values($data[$name]);
76+
}
77+
78+
return $data;
79+
}
80+
}

packages/database/src/Query.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function execute(mixed ...$bindings): Id
3636

3737
public function fetch(mixed ...$bindings): array
3838
{
39-
lw($this->getSql());
4039
return $this->getDatabase()->fetch($this->withBindings($bindings));
4140
}
4241

0 commit comments

Comments
 (0)