Skip to content

Commit 63db99a

Browse files
authored
feat(database): add support for hasmanythrough relation (#2052)
1 parent ff10111 commit 63db99a

5 files changed

Lines changed: 537 additions & 4 deletions

File tree

docs/1-essentials/03-database.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,30 @@ public ?Address $address = null;
293293
- `throughOwnerJoin`: FK on the target table pointing to the intermediate
294294
- `throughRelationJoin`: PK on the intermediate table
295295

296+
### Has many through
297+
298+
The {b`#[Tempest\Database\HasManyThrough]`} attribute defines a one-to-many relationship that traverses an intermediate model. This lets you access a collection of distant relations directly, resolved in a single SQL query with two JOINs.
299+
300+
```php
301+
use Tempest\Database\HasManyThrough;
302+
303+
final class Author
304+
{
305+
/** @var \App\Payment\Payment[] */
306+
#[HasManyThrough(Contract::class)]
307+
public array $payments = [];
308+
}
309+
```
310+
311+
The `through` parameter specifies the intermediate model class. The target model is inferred from the docblock's array type. This generates SQL like:
312+
313+
```sql
314+
LEFT JOIN contracts ON contracts.author_id = authors.id
315+
LEFT JOIN payments ON payments.contract_id = contracts.id
316+
```
317+
318+
The same optional parameters as `HasOneThrough` are available for custom join fields: `ownerJoin`, `relationJoin`, `throughOwnerJoin`, and `throughRelationJoin`.
319+
296320
### Using UUIDs as primary keys
297321

298322
By default, Tempest uses auto-incrementing integers as primary keys. UUIDs can be used as primary keys instead by annotating the {b`Tempest\Database\PrimaryKey`} property with the {b`#[Tempest\Database\Uuid]`} attribute. Tempest automatically generates a UUID v7 when a new model is created:

packages/database/src/Builder/ModelInspector.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tempest\Database\Config\DatabaseConfig;
88
use Tempest\Database\Eager;
99
use Tempest\Database\HasMany;
10+
use Tempest\Database\HasManyThrough;
1011
use Tempest\Database\HasOne;
1112
use Tempest\Database\HasOneThrough;
1213
use Tempest\Database\PrimaryKey;
@@ -54,6 +55,7 @@ public static function forModel(object|string $model): self
5455
$key = match (true) {
5556
is_string($model) => $model,
5657
$model instanceof HasMany => $model->property->getIterableType()->getName(),
58+
$model instanceof HasManyThrough => $model->property->getIterableType()->getName(),
5759
$model instanceof BelongsTo => $model->property->getType()->getName(),
5860
$model instanceof HasOne => $model->property->getType()->getName(),
5961
$model instanceof HasOneThrough => $model->property->getType()->getName(),
@@ -71,7 +73,7 @@ public static function forModel(object|string $model): self
7173
public function __construct(
7274
private(set) object|string $model,
7375
) {
74-
if ($model instanceof HasMany) {
76+
if ($model instanceof HasMany || $model instanceof HasManyThrough) {
7577
$model = $model->property->getIterableType()->asClass();
7678
$this->reflector = $model;
7779
} elseif ($model instanceof BelongsTo || $model instanceof HasOne || $model instanceof HasOneThrough) {
@@ -164,6 +166,10 @@ public function getPropertyValues(): array
164166
continue;
165167
}
166168

169+
if ($this->getHasManyThrough($property->getName()) instanceof HasManyThrough) {
170+
continue;
171+
}
172+
167173
$name = $property->getName();
168174

169175
$values[$name] = $property->getValue($this->instance);
@@ -222,6 +228,10 @@ public function getBelongsTo(string $name): ?BelongsTo
222228
return null;
223229
}
224230

231+
if ($property->hasAttribute(HasManyThrough::class)) {
232+
return null;
233+
}
234+
225235
$belongsTo = new BelongsTo();
226236
$belongsTo->property = $property;
227237

@@ -277,6 +287,10 @@ public function getHasMany(string $name): ?HasMany
277287
return $hasMany;
278288
}
279289

290+
if ($property->hasAttribute(HasManyThrough::class)) {
291+
return null;
292+
}
293+
280294
if ($property->hasAttribute(Virtual::class)) {
281295
return null;
282296
}
@@ -321,6 +335,29 @@ public function getHasOneThrough(string $name): ?HasOneThrough
321335
});
322336
}
323337

338+
public function getHasManyThrough(string $name): ?HasManyThrough
339+
{
340+
return $this->memoize('getHasManyThrough' . $name, function () use ($name) {
341+
if (! $this->isObjectModel()) {
342+
return null;
343+
}
344+
345+
$name = str($name)->camel();
346+
347+
if (! $this->reflector->hasProperty($name)) {
348+
return null;
349+
}
350+
351+
$property = $this->reflector->getProperty($name);
352+
353+
if (($hasManyThrough = $property->getAttribute(HasManyThrough::class)) instanceof HasManyThrough) {
354+
return $hasManyThrough;
355+
}
356+
357+
return null;
358+
});
359+
}
360+
324361
public function isRelation(string|PropertyReflector $name): bool
325362
{
326363
$name = $name instanceof PropertyReflector ? $name->getName() : $name;
@@ -332,6 +369,7 @@ public function isRelation(string|PropertyReflector $name): bool
332369
|| $this->getHasOne($name) instanceof HasOne
333370
|| $this->getHasMany($name) instanceof HasMany
334371
|| $this->getHasOneThrough($name) instanceof HasOneThrough
372+
|| $this->getHasManyThrough($name) instanceof HasManyThrough
335373
),
336374
);
337375
}
@@ -340,7 +378,10 @@ public function getRelation(string|PropertyReflector $name): ?Relation
340378
{
341379
$name = $name instanceof PropertyReflector ? $name->getName() : $name;
342380

343-
return $this->memoize('getRelation' . $name, fn () => $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name) ?? $this->getHasOneThrough($name));
381+
return $this->memoize(
382+
'getRelation' . $name,
383+
fn () => $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name) ?? $this->getHasOneThrough($name) ?? $this->getHasManyThrough($name),
384+
);
344385
}
345386

346387
/**
@@ -444,6 +485,10 @@ public function getSelectFields(): ImmutableArray
444485
continue;
445486
}
446487

488+
if ($relation instanceof HasManyThrough) {
489+
continue;
490+
}
491+
447492
if ($property->isVirtual()) {
448493
continue;
449494
}

0 commit comments

Comments
 (0)