Skip to content

Commit e6c2b8c

Browse files
committed
Separate FK fields from relation properties in hydration
wireRelationships() now sets related entities on a dedicated relation property (e.g. $comment->post) instead of overwriting the FK field ($comment->post_id stays as the int). This is driven by two new Style methods: relationProperty() derives the property name from an FK field, and isRelationProperty() identifies relation properties for filtering. Flat hydrator gains an isEntityBoundary() hook so FlatNum can detect table transitions via column metadata, fixing hydration of join tables without a standalone id column.
1 parent 84ccafb commit e6c2b8c

25 files changed

Lines changed: 149 additions & 15 deletions

src/Hydrators/Base.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Respect\Data\Hydrator;
1010
use SplObjectStorage;
1111

12+
use function is_object;
13+
1214
/** Base hydrator providing FK-to-entity wiring shared by all strategies */
1315
abstract class Base implements Hydrator
1416
{
@@ -25,6 +27,10 @@ protected function wireRelationships(SplObjectStorage $entities, EntityFactory $
2527
}
2628

2729
foreach ($entitiesClone as $sub) {
30+
if ($sub === $instance) {
31+
continue;
32+
}
33+
2834
$tableName = (string) $entities[$sub]->name;
2935
$primaryName = $style->identifier($tableName);
3036

@@ -38,6 +44,15 @@ protected function wireRelationships(SplObjectStorage $entities, EntityFactory $
3844
$v = $sub;
3945
}
4046

47+
if (is_object($v)) {
48+
$relationName = $style->relationProperty($field);
49+
if ($relationName !== null) {
50+
$entityFactory->set($instance, $relationName, $v);
51+
52+
continue;
53+
}
54+
}
55+
4156
$entityFactory->set($instance, $field, $v);
4257
}
4358
}

src/Hydrators/Flat.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function hydrate(
5656
$value,
5757
);
5858

59-
if ($primaryName != $columnName) {
59+
if ($primaryName != $columnName && !$this->isEntityBoundary($col, $raw)) {
6060
continue;
6161
}
6262

@@ -75,6 +75,12 @@ public function hydrate(
7575
/** Resolve the column name for a given reference (numeric index, namespaced key, etc.) */
7676
abstract protected function resolveColumnName(mixed $reference, mixed $raw): string;
7777

78+
/** Check if this column is the last one for the current entity (table boundary without PK) */
79+
protected function isEntityBoundary(mixed $col, mixed $raw): bool
80+
{
81+
return false;
82+
}
83+
7884
/**
7985
* @param SplObjectStorage<object, Collection> $entities
8086
*

src/Styles/NorthWind.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ public function remoteFromIdentifier(string $name): string|null
4444
{
4545
return $this->isRemoteIdentifier($name) ? $this->singularToPlural(substr($name, 0, -2)) : null;
4646
}
47+
48+
public function relationProperty(string $field): string|null
49+
{
50+
return $this->isRemoteIdentifier($field) ? substr($field, 0, -2) : null;
51+
}
52+
53+
public function isRelationProperty(string $name): bool
54+
{
55+
return !$this->isRemoteIdentifier($name) && $this->isRemoteIdentifier($name . 'ID');
56+
}
4757
}

src/Styles/Standard.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,14 @@ public function remoteFromIdentifier(string $name): string|null
5656
{
5757
return $this->isRemoteIdentifier($name) ? substr($name, 0, -3) : null;
5858
}
59+
60+
public function relationProperty(string $field): string|null
61+
{
62+
return $this->isRemoteIdentifier($field) ? substr($field, 0, -3) : null;
63+
}
64+
65+
public function isRelationProperty(string $name): bool
66+
{
67+
return !$this->isRemoteIdentifier($name) && $this->isRemoteIdentifier($name . '_id');
68+
}
5969
}

src/Styles/Stylable.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@ public function remoteFromIdentifier(string $name): string|null;
2222

2323
public function isRemoteIdentifier(string $name): bool;
2424

25+
public function relationProperty(string $remoteIdentifierField): string|null;
26+
27+
public function isRelationProperty(string $name): bool;
28+
2529
public function composed(string $left, string $right): string;
2630
}

tests/AbstractMapperTest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ public function hydrationWiresFkWithMatchingEntity(): void
243243

244244
$comment = $mapper->comment->post->fetch();
245245
$this->assertIsObject($comment);
246-
$post = $mapper->entityFactory->get($comment, 'post_id');
246+
// FK stays as int, relation goes to derived property
247+
$this->assertEquals(5, $mapper->entityFactory->get($comment, 'post_id'));
248+
$post = $mapper->entityFactory->get($comment, 'post');
247249
$this->assertIsObject($post);
248250
$this->assertEquals(5, $mapper->entityFactory->get($post, 'id'));
249251
$this->assertEquals('Post', $mapper->entityFactory->get($post, 'title'));
@@ -278,7 +280,9 @@ public function hydrationMatchesIntFkToStringPk(): void
278280

279281
$comment = $mapper->comment->post->fetch();
280282
$this->assertIsObject($comment);
281-
$post = $mapper->entityFactory->get($comment, 'post_id');
283+
// FK stays as int, relation goes to derived property
284+
$this->assertEquals(5, $mapper->entityFactory->get($comment, 'post_id'));
285+
$post = $mapper->entityFactory->get($comment, 'post');
282286
$this->assertIsObject($post);
283287
$this->assertEquals('5', $mapper->entityFactory->get($post, 'id'));
284288
}

tests/Styles/AbstractStyleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ public function remoteFromIdentifier(string $name): string|null
6161
{
6262
return null;
6363
}
64+
65+
public function relationProperty(string $field): string|null
66+
{
67+
return null;
68+
}
69+
70+
public function isRelationProperty(string $name): bool
71+
{
72+
return false;
73+
}
6474
};
6575
}
6676

tests/Styles/CakePHP/CakePHPIntegrationTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,16 @@ public function testFetchingAllEntityTyped(): void
5959

6060
$categories = $mapper->post_categories->categories->fetch();
6161
$this->assertInstanceOf(PostCategory::class, $categories);
62-
$this->assertInstanceOf(Category::class, $categories->category_id);
62+
$this->assertInstanceOf(Category::class, $categories->category);
6363
}
6464

6565
public function testFetchingAllEntityTypedNested(): void
6666
{
6767
$mapper = $this->mapper;
6868
$comment = $mapper->comments->posts->authors->fetchAll();
6969
$this->assertInstanceOf(Comment::class, $comment[0]);
70-
$this->assertInstanceOf(Post::class, $comment[0]->post_id);
71-
$this->assertInstanceOf(Author::class, $comment[0]->post_id->author_id);
70+
$this->assertInstanceOf(Post::class, $comment[0]->post);
71+
$this->assertInstanceOf(Author::class, $comment[0]->post->author);
7272
}
7373

7474
public function testPersistingEntityTyped(): void

tests/Styles/CakePHP/Comment.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ class Comment
1010

1111
public mixed $post_id = null;
1212

13+
public mixed $post = null;
14+
1315
public string|null $text = null;
1416
}

tests/Styles/CakePHP/Post.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ class Post
1313
public string|null $text = null;
1414

1515
public mixed $author_id = null;
16+
17+
public mixed $author = null;
1618
}

0 commit comments

Comments
 (0)