Skip to content

Commit 0214ac3

Browse files
authored
fix(database): loading database relations or other objects (#884)
1 parent f880072 commit 0214ac3

19 files changed

+253
-12
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"illuminate/view": "~11.7.0",
4444
"jenssegers/blade": "^2.0",
4545
"mikey179/vfsstream": "^2.0@dev",
46+
"nesbot/carbon": "^3.8",
4647
"nyholm/psr7": "^1.8",
4748
"phpat/phpat": "^0.11.0",
4849
"phpbench/phpbench": "84.x-dev",

src/Tempest/Database/src/Builder/FieldName.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\Database\Builder;
66

77
use Stringable;
8+
use Tempest\Database\DatabaseModel;
89
use Tempest\Mapper\Casters\CasterFactory;
910
use Tempest\Reflection\ClassReflector;
1011

@@ -25,6 +26,11 @@ public static function make(ClassReflector $class, ?TableName $tableName = null)
2526
$tableName ??= $class->callStatic('table');
2627

2728
foreach ($class->getPublicProperties() as $property) {
29+
// Don't include the field if it's a relation
30+
if ($property->getType()->matches(DatabaseModel::class)) {
31+
continue;
32+
}
33+
2834
$caster = $casterFactory->forProperty($property);
2935

3036
if ($caster !== null) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Database\Casters;
6+
7+
use Tempest\Mapper\Caster;
8+
9+
final class RelationCaster implements Caster
10+
{
11+
public function cast(mixed $input): mixed
12+
{
13+
return $input;
14+
}
15+
}

src/Tempest/Database/src/DatabaseModel.php

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

77
use Tempest\Database\Builder\ModelQueryBuilder;
88
use Tempest\Database\Builder\TableName;
9+
use Tempest\Database\Casters\RelationCaster;
10+
use Tempest\Mapper\CastWith;
911

12+
#[CastWith(RelationCaster::class)]
1013
interface DatabaseModel
1114
{
1215
public static function table(): TableName;

src/Tempest/Database/src/Mappers/QueryToModelMapper.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ private function parse(ClassReflector $class, DatabaseModel $model, array $row):
122122

123123
private function parseProperty(PropertyReflector $property, DatabaseModel $model, mixed $value): DatabaseModel
124124
{
125-
if ($value && ($caster = $this->casterFactory->forProperty($property)) !== null) {
125+
$caster = $this->casterFactory->forProperty($property);
126+
127+
if ($value && $caster !== null) {
126128
$value = $caster->cast($value);
127129
}
128130

src/Tempest/Mapper/src/Casters/CasterFactory.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,51 @@
1818
{
1919
public function forProperty(PropertyReflector $property): ?Caster
2020
{
21+
$type = $property->getType();
22+
2123
// Get CastWith from the property
2224
$castWith = $property->getAttribute(CastWith::class);
2325

24-
$type = $property->getType();
25-
26-
// Get CastWith from the property's type
26+
// Get CastWith from the property's type if there's no property-defined CastWith
2727
if ($castWith === null) {
2828
try {
29-
$castWith = $type->asClass()->getAttribute(CastWith::class);
29+
$castWith = $type->asClass()->getAttribute(CastWith::class, recursive: true);
3030
} catch (ReflectionException) {
3131
// Could not resolve CastWith from the type
3232
}
3333
}
3434

35+
// Return the caster if defined with CastWith
3536
if ($castWith !== null) {
3637
// Resolve the caster from the container
3738
return get($castWith->className);
3839
}
3940

41+
$typeName = $type->getName();
42+
4043
// Check if backed enum
4144
if ($type->matches(BackedEnum::class)) {
42-
return new EnumCaster($type->getName());
45+
return new EnumCaster($typeName);
4346
}
4447

45-
// Get Caster from built-in casters
46-
return match ($type->getName()) {
48+
// Try a built-in caster
49+
$builtInCaster = match ($type->getName()) {
4750
'int' => new IntegerCaster(),
4851
'float' => new FloatCaster(),
4952
'bool' => new BooleanCaster(),
5053
DateTimeImmutable::class, DateTimeInterface::class, DateTime::class => DateTimeCaster::fromProperty($property),
5154
default => null,
5255
};
56+
57+
if ($builtInCaster !== null) {
58+
return $builtInCaster;
59+
}
60+
61+
// If the type's a class, we'll cast it with the generic object caster
62+
if ($type->isClass()) {
63+
return new ObjectCaster($type);
64+
}
65+
66+
return null;
5367
}
5468
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Mapper\Casters;
6+
7+
use Tempest\Mapper\Caster;
8+
use Tempest\Reflection\TypeReflector;
9+
use Throwable;
10+
11+
final readonly class ObjectCaster implements Caster
12+
{
13+
public function __construct(
14+
private TypeReflector $type,
15+
) {
16+
}
17+
18+
public function cast(mixed $input): mixed
19+
{
20+
try {
21+
return $this->type->asClass()->newInstanceArgs([$input]);
22+
} catch (Throwable) {
23+
return $input;
24+
}
25+
}
26+
}

src/Tempest/Reflection/src/ClassReflector.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ public function getReflection(): PHPReflectionClass
3737
return $this->reflectionClass;
3838
}
3939

40+
public function getParent(): ?ClassReflector
41+
{
42+
if ($parentClass = $this->reflectionClass->getParentClass()) {
43+
return new ClassReflector($parentClass);
44+
}
45+
46+
return null;
47+
}
48+
49+
/** @return Generator<\Tempest\Reflection\TypeReflector> */
50+
public function getInterfaces(): Generator
51+
{
52+
foreach ($this->reflectionClass->getInterfaces() as $interface) {
53+
yield new TypeReflector($interface);
54+
}
55+
}
56+
4057
/** @return Generator<PropertyReflector> */
4158
public function getPublicProperties(): Generator
4259
{

src/Tempest/Reflection/src/HasAttributes.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,31 @@ public function hasAttribute(string $name): bool
2424
* @param class-string<TAttributeClass> $attributeClass
2525
* @return TAttributeClass|null
2626
*/
27-
public function getAttribute(string $attributeClass): object|null
27+
public function getAttribute(string $attributeClass, bool $recursive = false): object|null
2828
{
2929
$attribute = $this->getReflection()->getAttributes($attributeClass, PHPReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
3030

31-
return $attribute?->newInstance();
31+
$attributeInstance = $attribute?->newInstance();
32+
33+
if (! $recursive) {
34+
return $attributeInstance;
35+
}
36+
37+
if ($this instanceof ClassReflector) {
38+
foreach ($this->getInterfaces() as $interface) {
39+
$attributeInstance = $interface->asClass()->getAttribute($attributeClass);
40+
41+
if ($attributeInstance !== null) {
42+
break;
43+
}
44+
}
45+
46+
if ($attributeInstance === null && $parent = $this->getParent()) {
47+
$attributeInstance = $parent->getAttribute($attributeClass, true);
48+
}
49+
}
50+
51+
return $attributeInstance;
3252
}
3353

3454
/**

src/Tempest/Reflection/tests/ClassReflectorTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use PHPUnit\Framework\TestCase;
88
use ReflectionClass;
99
use Tempest\Reflection\ClassReflector;
10+
use Tempest\Reflection\Tests\Fixtures\ChildWithRecursiveAttribute;
11+
use Tempest\Reflection\Tests\Fixtures\ClassWithInterfaceWithRecursiveAttribute;
12+
use Tempest\Reflection\Tests\Fixtures\RecursiveAttribute;
1013
use Tempest\Reflection\Tests\Fixtures\TestClassA;
1114
use Tempest\Reflection\Tests\Fixtures\TestClassB;
1215

@@ -43,4 +46,18 @@ public function test_nullable_property_type(): void
4346
$reflector = new ClassReflector(TestClassB::class);
4447
$this->assertTrue($reflector->getProperty('name')->isNullable());
4548
}
49+
50+
public function test_recursive_attribute_from_interface(): void
51+
{
52+
$reflector = new ClassReflector(ClassWithInterfaceWithRecursiveAttribute::class);
53+
$this->assertNull($reflector->getAttribute(RecursiveAttribute::class));
54+
$this->assertNotNull($reflector->getAttribute(RecursiveAttribute::class, recursive: true));
55+
}
56+
57+
public function test_recursive_attribute_from_parent(): void
58+
{
59+
$reflector = new ClassReflector(ChildWithRecursiveAttribute::class);
60+
$this->assertNull($reflector->getAttribute(RecursiveAttribute::class));
61+
$this->assertNotNull($reflector->getAttribute(RecursiveAttribute::class, recursive: true));
62+
}
4663
}

0 commit comments

Comments
 (0)