Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/1-essentials/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ final class Aircraft implements Bindable
}
```

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:

```php
use Tempest\Router\Bindable;
use Tempest\Router\IsBindingValue;

final class Aircraft implements Bindable
{
#[IsBindingValue]
public string $callSign;

public function resolve(string $input): self
{
return self::find(id: $input);
}
}
```

### Backed enum binding

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.
Expand Down
99 changes: 92 additions & 7 deletions packages/database/src/Builder/ModelInspector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
use Tempest\Database\BelongsTo;
use Tempest\Database\Config\DatabaseConfig;
use Tempest\Database\Eager;
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
use Tempest\Database\Exceptions\ModelHadMultiplePrimaryColumns;
use Tempest\Database\HasMany;
use Tempest\Database\HasOne;
use Tempest\Database\PrimaryKey;
Expand Down Expand Up @@ -108,6 +106,14 @@ public function getPropertyValues(): array
$values = [];

foreach ($this->reflector->getProperties() as $property) {
if ($property->isVirtual()) {
continue;
}

if ($property->hasAttribute(Virtual::class)) {
continue;
}

if (! $property->isInitialized($this->instance)) {
continue;
}
Expand Down Expand Up @@ -247,6 +253,81 @@ public function getRelation(string|PropertyReflector $name): ?Relation
return $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name);
}

/**
* @return \Tempest\Support\Arr\ImmutableArray<array-key, Relation>
*/
public function getRelations(): ImmutableArray
{
if (! $this->isObjectModel()) {
return arr();
}

$relationFields = arr();

foreach ($this->reflector->getPublicProperties() as $property) {
if ($relation = $this->getRelation($property->getName())) {
$relationFields[] = $relation;
}
}

return $relationFields;
}

/**
* @return \Tempest\Support\Arr\ImmutableArray<array-key, PropertyReflector>
*/
public function getValueFields(): ImmutableArray
{
if (! $this->isObjectModel()) {
return arr();
}

$valueFields = arr();

foreach ($this->reflector->getPublicProperties() as $property) {
if ($property->isVirtual()) {
continue;
}

if ($property->hasAttribute(Virtual::class)) {
continue;
}

if ($this->isRelation($property->getName())) {
continue;
}

$valueFields[] = $property;
}

return $valueFields;
}

public function isRelationLoaded(string|PropertyReflector|Relation $relation): bool
{
if (! $this->isObjectModel()) {
return false;
}

if (! ($relation instanceof Relation)) {
$relation = $this->getRelation($relation);
}

if (! $relation) {
return false;
}

if (! $relation->property->isInitialized($this->instance)) {
return false;
}

if ($relation->property->getValue($this->instance) === null) {
return false;
}

return true;
}

public function getSelectFields(): ImmutableArray
{
if (! $this->isObjectModel()) {
Expand All @@ -255,6 +336,10 @@ public function getSelectFields(): ImmutableArray

$selectFields = arr();

if ($primaryKey = $this->getPrimaryKeyProperty()) {
$selectFields[] = $primaryKey->getName();
}

foreach ($this->reflector->getPublicProperties() as $property) {
$relation = $this->getRelation($property->getName());

Expand All @@ -266,6 +351,10 @@ public function getSelectFields(): ImmutableArray
continue;
}

if ($property->getType()->equals(PrimaryKey::class)) {
continue;
}

if ($relation instanceof BelongsTo) {
$selectFields[] = $relation->getOwnerFieldName();
} else {
Expand Down Expand Up @@ -417,11 +506,7 @@ public function getPrimaryKeyProperty(): ?PropertyReflector

return match ($primaryKeys->count()) {
0 => null,
1 => $primaryKeys->first(),
default => throw ModelHadMultiplePrimaryColumns::found(
model: $this->model,
properties: $primaryKeys->map(fn (PropertyReflector $property) => $property->getName())->toArray(),
),
default => $primaryKeys->first(),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ private function convertObjectToArray(object $object, array $excludeProperties =
continue;
}

if ($property->isVirtual()) {
continue;
}

if ($property->isUninitialized($object)) {
continue;
}

$propertyName = $property->getName();

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

This file was deleted.

63 changes: 33 additions & 30 deletions packages/database/src/IsDatabaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@
use Tempest\Database\Builder\QueryBuilders\SelectQueryBuilder;
use Tempest\Database\Exceptions\RelationWasMissing;
use Tempest\Database\Exceptions\ValueWasMissing;
use Tempest\Database\Virtual;
use Tempest\Reflection\ClassReflector;
use Tempest\Reflection\PropertyReflector;
use Tempest\Router\IsBindingValue;
use Tempest\Validation\SkipValidation;

use function Tempest\Database\query;
use function Tempest\Support\arr;
use function Tempest\Support\str;

trait IsDatabaseModel
{
#[IsBindingValue, SkipValidation]
public PrimaryKey $id;

/**
* Returns a builder for selecting records using this model's table.
*
Expand Down Expand Up @@ -58,15 +63,15 @@ public static function new(mixed ...$params): self
/**
* Finds a model instance by its ID.
*/
public static function findById(string|int|PrimaryKey $id): static
public static function findById(string|int|PrimaryKey $id): self
{
return self::get($id);
}

/**
* Finds a model instance by its ID.
*/
public static function resolve(string|int|PrimaryKey $id): static
public static function resolve(string|int|PrimaryKey $id): self
{
return query(self::class)->resolve($id);
}
Expand Down Expand Up @@ -152,7 +157,6 @@ public static function findOrNew(array $find, array $update): self
*
* @param array<string,mixed> $find Properties to search for in the existing model.
* @param array<string,mixed> $update Properties to update or set on the model if it is found or created.
* @return TModel
*/
public static function updateOrCreate(array $find, array $update): self
{
Expand All @@ -166,23 +170,30 @@ public function refresh(): self
{
$model = inspect($this);

if (! $model->hasPrimaryKey()) {
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'refresh');
}
$loadedRelations = $model
->getRelations()
->filter(fn (Relation $relation) => $model->isRelationLoaded($relation));

$relations = [];
$primaryKeyProperty = $model->getPrimaryKeyProperty();
$primaryKeyValue = $primaryKeyProperty->getValue($this);

foreach (new ClassReflector($this)->getPublicProperties() as $property) {
if (! $property->getValue($this)) {
continue;
}
$new = self::select()
->with(...$loadedRelations->map(fn (Relation $relation) => $relation->name))
->get($primaryKeyValue);

if ($model->isRelation($property->getName())) {
$relations[] = $property->getName();
}
foreach ($loadedRelations as $relation) {
$relation->property->setValue(
object: $this,
value: $relation->property->getValue($new),
);
}

$this->load(...$relations);
foreach ($model->getValueFields() as $property) {
$property->setValue(
object: $this,
value: $property->getValue($new),
);
}

return $this;
}
Expand All @@ -194,21 +205,17 @@ public function load(string ...$relations): self
{
$model = inspect($this);

if (! $model->hasPrimaryKey()) {
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'load');
}

$primaryKeyProperty = $model->getPrimaryKeyProperty();
$primaryKeyValue = $primaryKeyProperty->getValue($this);

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

foreach (new ClassReflector($new)->getPublicProperties() as $property) {
if ($property->hasAttribute(Virtual::class)) {
continue;
}
$fieldsToUpdate = arr($relations)
->map(fn (string $relation) => str($relation)->before('.')->toString())
->unique();

$property->setValue($this, $property->getValue($new));
foreach ($fieldsToUpdate as $fieldToUpdate) {
$this->{$fieldToUpdate} = $new->{$fieldToUpdate};
}

return $this;
Expand Down Expand Up @@ -262,10 +269,6 @@ public function update(mixed ...$params): self
{
$model = inspect($this);

if (! $model->hasPrimaryKey()) {
throw Exceptions\ModelDidNotHavePrimaryColumn::neededForMethod($this, 'update');
}

$model->validate(...$params);

query($this)
Expand Down
11 changes: 11 additions & 0 deletions packages/router/src/GenericRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

use BackedEnum;
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use ReflectionClass;
use Tempest\Container\Container;
use Tempest\Core\AppConfig;
use Tempest\Database\PrimaryKey;
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
use Tempest\Http\Request;
use Tempest\Http\Response;
Expand Down Expand Up @@ -126,6 +128,15 @@ public function toUri(array|string $action, ...$params): string

if ($value instanceof BackedEnum) {
$value = $value->value;
} elseif ($value instanceof Bindable) {
foreach (new ClassReflector($value)->getPublicProperties() as $property) {
if (! $property->hasAttribute(IsBindingValue::class)) {
continue;
}

$value = $property->getValue($value);
break;
}
}

$uri = $uri->replaceRegex(
Expand Down
10 changes: 10 additions & 0 deletions packages/router/src/IsBindingValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Tempest\Router;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class IsBindingValue
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static function fromRoute(Route $route, MethodReflector $methodReflector)
self::getRouteParams($route->uri),
$route->middleware,
$methodReflector,
$route->without,
$route->without ?? [],
);
}

Expand Down
2 changes: 0 additions & 2 deletions tests/Fixtures/Models/A.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ final class A
{
use IsDatabaseModel;

public PrimaryKey $id;

public function __construct(
public B $b,
) {}
Expand Down
2 changes: 0 additions & 2 deletions tests/Fixtures/Models/AWithEager.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ final class AWithEager
{
use IsDatabaseModel;

public PrimaryKey $id;

public function __construct(
#[Eager]
public BWithEager $b,
Expand Down
Loading
Loading