Skip to content

Commit b33204f

Browse files
authored
feat(database): primary key improvements (#1517)
1 parent a28c943 commit b33204f

38 files changed

+309
-417
lines changed

docs/1-essentials/01-routing.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ final class Aircraft implements Bindable
103103
}
104104
```
105105

106+
By default, `Bindable` objects will be cast to strings when they are passed into the `uri()` function as a route parameter. You can override this default behaviour by tagging a public property on the object with the {b`\Tempest\Router\IsBindingValue`} attribute:
107+
108+
```php
109+
use Tempest\Router\Bindable;
110+
use Tempest\Router\IsBindingValue;
111+
112+
final class Aircraft implements Bindable
113+
{
114+
#[IsBindingValue]
115+
public string $callSign;
116+
117+
public function resolve(string $input): self
118+
{
119+
return self::find(id: $input);
120+
}
121+
}
122+
```
123+
106124
### Backed enum binding
107125

108126
You may inject string-backed enumerations to controller actions. Tempest will try to map the corresponding parameter from the URI to an instance of that enum using the [`tryFrom`](https://www.php.net/manual/en/backedenum.tryfrom.php) enum method.

packages/database/src/Builder/ModelInspector.php

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
use Tempest\Database\BelongsTo;
77
use Tempest\Database\Config\DatabaseConfig;
88
use Tempest\Database\Eager;
9-
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
10-
use Tempest\Database\Exceptions\ModelHadMultiplePrimaryColumns;
119
use Tempest\Database\HasMany;
1210
use Tempest\Database\HasOne;
1311
use Tempest\Database\PrimaryKey;
@@ -108,6 +106,14 @@ public function getPropertyValues(): array
108106
$values = [];
109107

110108
foreach ($this->reflector->getProperties() as $property) {
109+
if ($property->isVirtual()) {
110+
continue;
111+
}
112+
113+
if ($property->hasAttribute(Virtual::class)) {
114+
continue;
115+
}
116+
111117
if (! $property->isInitialized($this->instance)) {
112118
continue;
113119
}
@@ -247,6 +253,81 @@ public function getRelation(string|PropertyReflector $name): ?Relation
247253
return $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name);
248254
}
249255

256+
/**
257+
* @return \Tempest\Support\Arr\ImmutableArray<array-key, Relation>
258+
*/
259+
public function getRelations(): ImmutableArray
260+
{
261+
if (! $this->isObjectModel()) {
262+
return arr();
263+
}
264+
265+
$relationFields = arr();
266+
267+
foreach ($this->reflector->getPublicProperties() as $property) {
268+
if ($relation = $this->getRelation($property->getName())) {
269+
$relationFields[] = $relation;
270+
}
271+
}
272+
273+
return $relationFields;
274+
}
275+
276+
/**
277+
* @return \Tempest\Support\Arr\ImmutableArray<array-key, PropertyReflector>
278+
*/
279+
public function getValueFields(): ImmutableArray
280+
{
281+
if (! $this->isObjectModel()) {
282+
return arr();
283+
}
284+
285+
$valueFields = arr();
286+
287+
foreach ($this->reflector->getPublicProperties() as $property) {
288+
if ($property->isVirtual()) {
289+
continue;
290+
}
291+
292+
if ($property->hasAttribute(Virtual::class)) {
293+
continue;
294+
}
295+
296+
if ($this->isRelation($property->getName())) {
297+
continue;
298+
}
299+
300+
$valueFields[] = $property;
301+
}
302+
303+
return $valueFields;
304+
}
305+
306+
public function isRelationLoaded(string|PropertyReflector|Relation $relation): bool
307+
{
308+
if (! $this->isObjectModel()) {
309+
return false;
310+
}
311+
312+
if (! ($relation instanceof Relation)) {
313+
$relation = $this->getRelation($relation);
314+
}
315+
316+
if (! $relation) {
317+
return false;
318+
}
319+
320+
if (! $relation->property->isInitialized($this->instance)) {
321+
return false;
322+
}
323+
324+
if ($relation->property->getValue($this->instance) === null) {
325+
return false;
326+
}
327+
328+
return true;
329+
}
330+
250331
public function getSelectFields(): ImmutableArray
251332
{
252333
if (! $this->isObjectModel()) {
@@ -255,6 +336,10 @@ public function getSelectFields(): ImmutableArray
255336

256337
$selectFields = arr();
257338

339+
if ($primaryKey = $this->getPrimaryKeyProperty()) {
340+
$selectFields[] = $primaryKey->getName();
341+
}
342+
258343
foreach ($this->reflector->getPublicProperties() as $property) {
259344
$relation = $this->getRelation($property->getName());
260345

@@ -266,6 +351,10 @@ public function getSelectFields(): ImmutableArray
266351
continue;
267352
}
268353

354+
if ($property->getType()->equals(PrimaryKey::class)) {
355+
continue;
356+
}
357+
269358
if ($relation instanceof BelongsTo) {
270359
$selectFields[] = $relation->getOwnerFieldName();
271360
} else {
@@ -417,11 +506,7 @@ public function getPrimaryKeyProperty(): ?PropertyReflector
417506

418507
return match ($primaryKeys->count()) {
419508
0 => null,
420-
1 => $primaryKeys->first(),
421-
default => throw ModelHadMultiplePrimaryColumns::found(
422-
model: $this->model,
423-
properties: $primaryKeys->map(fn (PropertyReflector $property) => $property->getName())->toArray(),
424-
),
509+
default => $primaryKeys->first(),
425510
};
426511
}
427512

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ private function convertObjectToArray(object $object, array $excludeProperties =
149149
continue;
150150
}
151151

152+
if ($property->isVirtual()) {
153+
continue;
154+
}
155+
156+
if ($property->isUninitialized($object)) {
157+
continue;
158+
}
159+
152160
$propertyName = $property->getName();
153161

154162
if (! in_array($propertyName, $excludeProperties, strict: true)) {

packages/database/src/Exceptions/ModelHadMultiplePrimaryColumns.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/database/src/IsDatabaseModel.php

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
use Tempest\Database\Builder\QueryBuilders\SelectQueryBuilder;
1010
use Tempest\Database\Exceptions\RelationWasMissing;
1111
use Tempest\Database\Exceptions\ValueWasMissing;
12-
use Tempest\Database\Virtual;
1312
use Tempest\Reflection\ClassReflector;
1413
use Tempest\Reflection\PropertyReflector;
14+
use Tempest\Router\IsBindingValue;
15+
use Tempest\Validation\SkipValidation;
1516

16-
use function Tempest\Database\query;
17+
use function Tempest\Support\arr;
18+
use function Tempest\Support\str;
1719

1820
trait IsDatabaseModel
1921
{
22+
#[IsBindingValue, SkipValidation]
23+
public PrimaryKey $id;
24+
2025
/**
2126
* Returns a builder for selecting records using this model's table.
2227
*
@@ -58,15 +63,15 @@ public static function new(mixed ...$params): self
5863
/**
5964
* Finds a model instance by its ID.
6065
*/
61-
public static function findById(string|int|PrimaryKey $id): static
66+
public static function findById(string|int|PrimaryKey $id): self
6267
{
6368
return self::get($id);
6469
}
6570

6671
/**
6772
* Finds a model instance by its ID.
6873
*/
69-
public static function resolve(string|int|PrimaryKey $id): static
74+
public static function resolve(string|int|PrimaryKey $id): self
7075
{
7176
return query(self::class)->resolve($id);
7277
}
@@ -152,7 +157,6 @@ public static function findOrNew(array $find, array $update): self
152157
*
153158
* @param array<string,mixed> $find Properties to search for in the existing model.
154159
* @param array<string,mixed> $update Properties to update or set on the model if it is found or created.
155-
* @return TModel
156160
*/
157161
public static function updateOrCreate(array $find, array $update): self
158162
{
@@ -166,23 +170,30 @@ public function refresh(): self
166170
{
167171
$model = inspect($this);
168172

169-
if (! $model->hasPrimaryKey()) {
170-
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'refresh');
171-
}
173+
$loadedRelations = $model
174+
->getRelations()
175+
->filter(fn (Relation $relation) => $model->isRelationLoaded($relation));
172176

173-
$relations = [];
177+
$primaryKeyProperty = $model->getPrimaryKeyProperty();
178+
$primaryKeyValue = $primaryKeyProperty->getValue($this);
174179

175-
foreach (new ClassReflector($this)->getPublicProperties() as $property) {
176-
if (! $property->getValue($this)) {
177-
continue;
178-
}
180+
$new = self::select()
181+
->with(...$loadedRelations->map(fn (Relation $relation) => $relation->name))
182+
->get($primaryKeyValue);
179183

180-
if ($model->isRelation($property->getName())) {
181-
$relations[] = $property->getName();
182-
}
184+
foreach ($loadedRelations as $relation) {
185+
$relation->property->setValue(
186+
object: $this,
187+
value: $relation->property->getValue($new),
188+
);
183189
}
184190

185-
$this->load(...$relations);
191+
foreach ($model->getValueFields() as $property) {
192+
$property->setValue(
193+
object: $this,
194+
value: $property->getValue($new),
195+
);
196+
}
186197

187198
return $this;
188199
}
@@ -194,21 +205,17 @@ public function load(string ...$relations): self
194205
{
195206
$model = inspect($this);
196207

197-
if (! $model->hasPrimaryKey()) {
198-
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'load');
199-
}
200-
201208
$primaryKeyProperty = $model->getPrimaryKeyProperty();
202209
$primaryKeyValue = $primaryKeyProperty->getValue($this);
203210

204211
$new = self::get($primaryKeyValue, $relations);
205212

206-
foreach (new ClassReflector($new)->getPublicProperties() as $property) {
207-
if ($property->hasAttribute(Virtual::class)) {
208-
continue;
209-
}
213+
$fieldsToUpdate = arr($relations)
214+
->map(fn (string $relation) => str($relation)->before('.')->toString())
215+
->unique();
210216

211-
$property->setValue($this, $property->getValue($new));
217+
foreach ($fieldsToUpdate as $fieldToUpdate) {
218+
$this->{$fieldToUpdate} = $new->{$fieldToUpdate};
212219
}
213220

214221
return $this;
@@ -262,10 +269,6 @@ public function update(mixed ...$params): self
262269
{
263270
$model = inspect($this);
264271

265-
if (! $model->hasPrimaryKey()) {
266-
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'update');
267-
}
268-
269272
$model->validate(...$params);
270273

271274
query($this)

packages/router/src/GenericRouter.php

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

77
use BackedEnum;
88
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
9+
use ReflectionClass;
910
use Tempest\Container\Container;
1011
use Tempest\Core\AppConfig;
12+
use Tempest\Database\PrimaryKey;
1113
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
1214
use Tempest\Http\Request;
1315
use Tempest\Http\Response;
@@ -126,6 +128,15 @@ public function toUri(array|string $action, ...$params): string
126128

127129
if ($value instanceof BackedEnum) {
128130
$value = $value->value;
131+
} elseif ($value instanceof Bindable) {
132+
foreach (new ClassReflector($value)->getPublicProperties() as $property) {
133+
if (! $property->hasAttribute(IsBindingValue::class)) {
134+
continue;
135+
}
136+
137+
$value = $property->getValue($value);
138+
break;
139+
}
129140
}
130141

131142
$uri = $uri->replaceRegex(
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Attribute;
6+
7+
#[Attribute(Attribute::TARGET_PROPERTY)]
8+
final class IsBindingValue
9+
{
10+
}

packages/router/src/Routing/Construction/DiscoveredRoute.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static function fromRoute(Route $route, MethodReflector $methodReflector)
2424
self::getRouteParams($route->uri),
2525
$route->middleware,
2626
$methodReflector,
27-
$route->without,
27+
$route->without ?? [],
2828
);
2929
}
3030

tests/Fixtures/Models/A.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ final class A
1313
{
1414
use IsDatabaseModel;
1515

16-
public PrimaryKey $id;
17-
1816
public function __construct(
1917
public B $b,
2018
) {}

tests/Fixtures/Models/AWithEager.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ final class AWithEager
1414
{
1515
use IsDatabaseModel;
1616

17-
public PrimaryKey $id;
18-
1917
public function __construct(
2018
#[Eager]
2119
public BWithEager $b,

0 commit comments

Comments
 (0)