Skip to content

Commit eb98dde

Browse files
committed
wip
1 parent 54c597c commit eb98dde

File tree

16 files changed

+428
-107
lines changed

16 files changed

+428
-107
lines changed

packages/database/src/BelongsTo.php

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,76 @@
55
namespace Tempest\Database;
66

77
use Attribute;
8-
use Tempest\Reflection\PropertyAttribute;
8+
use Tempest\Database\QueryStatements\FieldStatement;
9+
use Tempest\Database\QueryStatements\JoinStatement;
910
use Tempest\Reflection\PropertyReflector;
11+
use Tempest\Support\Arr\ImmutableArray;
12+
use function Tempest\Support\str;
1013

1114
#[Attribute(Attribute::TARGET_PROPERTY)]
12-
final class BelongsTo implements PropertyAttribute
15+
final class BelongsTo implements Relation
1316
{
1417
public PropertyReflector $property;
1518

1619
public function __construct(
17-
public string $localPropertyName,
18-
public string $inversePropertyName = 'id',
20+
public ?string $relationJoin = null,
21+
public ?string $ownerJoin = null,
1922
) {}
23+
24+
public function getOwnerFieldName(): string
25+
{
26+
if ($this->ownerJoin) {
27+
return explode('.', $this->ownerJoin)[1];
28+
}
29+
30+
$relationModel = model($this->property->getType()->asClass());
31+
32+
return str($relationModel->getTableName())->singularizeLastWord() . '_' . $relationModel->getPrimaryKey();
33+
}
34+
35+
public function getSelectFields(): ImmutableArray
36+
{
37+
$relationModel = model($this->property->getType()->asClass());
38+
39+
return $relationModel
40+
->getSelectFields()
41+
->map(fn ($field) => new FieldStatement($relationModel->getTableName() . '.' . $field)->withAlias());
42+
}
43+
44+
public function getJoinStatement(): JoinStatement
45+
{
46+
$relationModel = model($this->property->getType()->asClass());
47+
48+
// authors.id
49+
$relationJoin = $this->relationJoin;
50+
51+
if (! $relationJoin) {
52+
$relationJoin = sprintf(
53+
'%s.%s',
54+
$relationModel->getTableName(),
55+
$relationModel->getPrimaryKey(),
56+
);
57+
}
58+
59+
// books.author_id
60+
$ownerJoin = $this->ownerJoin;
61+
62+
if (! $ownerJoin) {
63+
$ownerModel = model($this->property->getClass());
64+
65+
$ownerJoin = sprintf(
66+
'%s.%s',
67+
$ownerModel->getTableName(),
68+
$this->getOwnerFieldName(),
69+
);
70+
}
71+
72+
// LEFT JOIN authors ON authors.id = books.author_id
73+
return new JoinStatement(sprintf(
74+
'LEFT JOIN %s ON %s = %s',
75+
$relationModel->getTableName(),
76+
$relationJoin,
77+
$ownerJoin,
78+
));
79+
}
2080
}

packages/database/src/Builder/ModelInspector.php

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55
use ReflectionException;
66
use Tempest\Database\BelongsTo;
77
use Tempest\Database\Config\DatabaseConfig;
8+
use Tempest\Database\Eager;
89
use Tempest\Database\HasMany;
910
use Tempest\Database\HasOne;
11+
use Tempest\Database\Relation;
1012
use Tempest\Database\Table;
13+
use Tempest\Database\Virtual;
1114
use Tempest\Reflection\ClassReflector;
15+
use Tempest\Reflection\PropertyReflector;
16+
use Tempest\Support\Arr\ImmutableArray;
1217
use Tempest\Validation\Exceptions\ValidationException;
1318
use Tempest\Validation\SkipValidation;
1419
use Tempest\Validation\Validator;
1520

1621
use function Tempest\get;
22+
use function Tempest\Support\arr;
1723
use function Tempest\Support\str;
1824

1925
final class ModelInspector
@@ -57,6 +63,11 @@ public function getTableDefinition(): TableDefinition
5763
return new TableDefinition($specificName ?? $conventionalName);
5864
}
5965

66+
public function getTableName(): string
67+
{
68+
return $this->getTableDefinition()->name;
69+
}
70+
6071
public function getPropertyValues(): array
6172
{
6273
if (! $this->isObjectModel()) {
@@ -116,7 +127,7 @@ public function getBelongsTo(string $name): ?BelongsTo
116127
return null;
117128
}
118129

119-
$belongsTo = new BelongsTo($property->getName());
130+
$belongsTo = new BelongsTo();
120131
$belongsTo->property = $property;
121132

122133
return $belongsTo;
@@ -167,12 +178,67 @@ public function getHasMany(string $name): ?HasMany
167178
return null;
168179
}
169180

170-
$hasMany = new HasMany(inversePropertyName: $property->getName());
181+
$hasMany = new HasMany();
171182
$hasMany->property = $property;
172183

173184
return $hasMany;
174185
}
175186

187+
public function getSelectFields(): ImmutableArray
188+
{
189+
if (! $this->isObjectModel()) {
190+
return arr();
191+
}
192+
193+
$selectFields = arr();
194+
195+
foreach ($this->modelClass->getPublicProperties() as $property) {
196+
$relation = $this->getRelation($property->getName());
197+
198+
if ($relation instanceof HasMany || $relation instanceof HasOne) {
199+
continue;
200+
}
201+
202+
if ($property->hasAttribute(Virtual::class)) {
203+
continue;
204+
}
205+
206+
if ($relation instanceof BelongsTo) {
207+
$selectFields[] = $relation->getOwnerFieldName();
208+
} else {
209+
$selectFields[] = $property->getName();
210+
}
211+
}
212+
213+
return $selectFields;
214+
}
215+
216+
public function getRelation(string|PropertyReflector $name): ?Relation
217+
{
218+
$name = $name instanceof PropertyReflector ? $name->getName() : $name;
219+
220+
return $this->getBelongsTo($name)
221+
?? $this->getHasOne($name)
222+
?? $this->getHasMany($name);
223+
}
224+
225+
public function getEagerRelations(): array
226+
{
227+
if (! $this->isObjectModel()) {
228+
return [];
229+
}
230+
231+
$relations = [];
232+
233+
foreach ($this->modelClass->getPublicProperties() as $property) {
234+
if ($property->hasAttribute(Eager::class)) {
235+
$relations[$property->getName()] = $this->getRelation($property);
236+
}
237+
}
238+
239+
return array_filter($relations);
240+
}
241+
176242
public function validate(mixed ...$data): void
177243
{
178244
if (! $this->isObjectModel()) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function select(string ...$columns): SelectQueryBuilder
1717
{
1818
return new SelectQueryBuilder(
1919
model: $this->model,
20-
columns: $columns !== [] ? arr($columns) : null,
20+
fields: $columns !== [] ? arr($columns) : null,
2121
);
2222
}
2323

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

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
use Tempest\Database\Builder\TableDefinition;
1111
use Tempest\Database\Id;
1212
use Tempest\Database\Mappers\QueryToModelMapper;
13+
use Tempest\Database\Mappers\SelectModelMapper;
1314
use Tempest\Database\Query;
15+
use Tempest\Database\QueryStatements\FieldStatement;
1416
use Tempest\Database\QueryStatements\JoinStatement;
1517
use Tempest\Database\QueryStatements\OrderByStatement;
1618
use Tempest\Database\QueryStatements\RawStatement;
@@ -20,6 +22,7 @@
2022
use Tempest\Support\Arr\ImmutableArray;
2123
use Tempest\Support\Conditions\HasConditions;
2224

25+
use function Tempest\Database\model;
2326
use function Tempest\map;
2427
use function Tempest\reflect;
2528
use function Tempest\Support\arr;
@@ -44,14 +47,17 @@ final class SelectQueryBuilder implements BuildsQuery
4447

4548
private array $bindings = [];
4649

47-
public function __construct(string|object $model, ?ImmutableArray $columns = null)
50+
public function __construct(string|object $model, ?ImmutableArray $fields = null)
4851
{
4952
$this->modelDefinition = ModelDefinition::tryFrom($model);
5053
$this->modelClass = is_object($model) ? $model::class : $model;
54+
$model = model($this->modelClass);
5155

5256
$this->select = new SelectStatement(
5357
table: $this->resolveTable($model),
54-
columns: $columns ?? $this->resolveColumns(),
58+
fields: $fields ?? $model
59+
->getSelectFields()
60+
->map(fn (string $fieldName) => new FieldStatement("{$model->getTableName()}.{$fieldName}")->withAlias()),
5561
);
5662
}
5763

@@ -66,9 +72,9 @@ public function first(mixed ...$bindings): mixed
6672
return $query->fetchFirst();
6773
}
6874

69-
$result = map($query)
75+
$result = map($query->fetch())
7076
->collection()
71-
->with(QueryToModelMapper::class)
77+
->with(SelectModelMapper::class)
7278
->to($this->modelClass);
7379

7480
if ($result === []) {
@@ -95,7 +101,10 @@ public function all(mixed ...$bindings): array
95101
return $query->fetch();
96102
}
97103

98-
return map($query)->collection()->to($this->modelClass);
104+
return map($query->fetch())
105+
->collection()
106+
->with(SelectModelMapper::class)
107+
->to($this->modelClass);
99108
}
100109

101110
/**
@@ -215,15 +224,16 @@ public function toSql(): string
215224

216225
public function build(mixed ...$bindings): Query
217226
{
218-
$resolvedRelations = $this->resolveRelations();
219-
220227
foreach ($this->joins as $join) {
221228
$this->select->join[] = new JoinStatement($join);
222229
}
223230

224-
foreach ($resolvedRelations as $relation) {
225-
$this->select->columns = $this->select->columns->append(...$relation->getFieldDefinitions()->map(fn (FieldDefinition $field) => (string) $field->withAlias()));
226-
$this->select->join[] = new JoinStatement($relation->getStatement());
231+
foreach ($this->getIncludedRelations() as $relation) {
232+
$this->select->fields = $this->select->fields->append(
233+
...$relation->getSelectFields()
234+
);
235+
236+
$this->select->join[] = $relation->getJoinStatement();
227237
}
228238

229239
return new Query($this->select, [...$this->bindings, ...$bindings]);
@@ -243,32 +253,27 @@ private function resolveTable(string|object $model): TableDefinition
243253
return $this->modelDefinition->getTableDefinition();
244254
}
245255

246-
private function resolveColumns(): ImmutableArray
256+
/** @return \Tempest\Database\Relation[] */
257+
private function getIncludedRelations(): array
247258
{
248-
if ($this->modelDefinition === null) {
249-
return arr();
250-
}
259+
$definition = model($this->modelClass);
251260

252-
return $this->modelDefinition
253-
->getFieldDefinitions()
254-
->filter(fn (FieldDefinition $field) => ! reflect($this->modelClass, $field->name)->hasAttribute(Virtual::class))
255-
->map(fn (FieldDefinition $field) => (string) $field->withAlias());
256-
}
257-
258-
private function resolveRelations(): ImmutableArray
259-
{
260-
if ($this->modelDefinition === null) {
261-
return arr();
261+
if (! $definition->isObjectModel()) {
262+
return [];
262263
}
263264

264-
$relations = $this->modelDefinition->getEagerRelations();
265+
$relations = $definition->getEagerRelations();
266+
267+
foreach ($this->relations as $relation) {
268+
$relation = $definition->getRelation($relation);
265269

266-
foreach ($this->relations as $relationName) {
267-
foreach ($this->modelDefinition->getRelations($relationName) as $relation) {
268-
$relations[$relation->getRelationName()] = $relation;
270+
if (! $relation) {
271+
continue;
269272
}
273+
274+
$relations[$relation->property->getName()] = $relation;
270275
}
271276

272-
return arr($relations);
277+
return $relations;
273278
}
274279
}

packages/database/src/Builder/Relations/BelongsToRelation.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ public static function fromAttribute(BelongsTo $belongsTo, PropertyReflector $pr
3737
$relationModelClass = $property->getType()->asClass();
3838

3939
$localTable = TableDefinition::for($property->getClass(), $alias);
40-
$localField = new FieldDefinition($localTable, $belongsTo->localPropertyName);
40+
$localField = new FieldDefinition($localTable, $belongsTo->ownerJoin);
4141

4242
$joinTable = TableDefinition::for($property->getType()->asClass(), "{$alias}.{$property->getName()}");
43-
$joinField = new FieldDefinition($joinTable, $belongsTo->inversePropertyName);
43+
$joinField = new FieldDefinition($joinTable, $belongsTo->relationJoin);
4444

4545
return new self($relationModelClass, $localField, $joinField);
4646
}

packages/database/src/Builder/Relations/Relation.php

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

77
use Tempest\Support\Arr\ImmutableArray;
88

9+
// TODO: remove
910
interface Relation
1011
{
1112
public function getRelationName(): string;

0 commit comments

Comments
 (0)