Skip to content

Commit 2af1788

Browse files
authored
feat: transforming relation IDs (#10)
1 parent acbe29a commit 2af1788

File tree

11 files changed

+1021
-342
lines changed

11 files changed

+1021
-342
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2022-2024 Michal Sniatala
3+
Copyright (c) 2022-2025 Michal Sniatala
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

composer.lock

Lines changed: 448 additions & 328 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/basic_usage.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [Eager loading](#eager-loading)
44
- [Lazy loading](#lazy-loading)
5+
- [Transforming relation IDs](#transforming-relation-ids)
56

67
## Eager loading
78

@@ -141,3 +142,53 @@ foreach ($users as $user) {
141142
```
142143

143144
This will perform `n+1` queries. First one to get all the users and then one for each profile we want to access.
145+
146+
## Transforming relation IDs
147+
148+
Sometimes you may need to transform the IDs before they are used in the relation queries. This is particularly useful when working with different ID formats, such as UUIDs, that need to be converted to binary format. An ideal example is the [codeigniter4-uuid](https://github.com/michalsn/codeigniter4-uuid) package.
149+
150+
You can define transformation methods in your model to automatically transform IDs when loading relations:
151+
152+
```php
153+
use Michalsn\CodeIgniterNestedModel\Relation;
154+
use Michalsn\CodeIgniterNestedModel\Traits\HasRelations;
155+
156+
class UserModel extends Model
157+
{
158+
use HasRelations;
159+
160+
// ...
161+
162+
protected function initialize()
163+
{
164+
$this->initRelations();
165+
}
166+
167+
public function profile(): Relation
168+
{
169+
return $this->hasOne(ProfileModel::class);
170+
}
171+
172+
// Transform IDs specifically for the 'profile' relation
173+
protected function transformProfileRelationIds(array $ids): array
174+
{
175+
return array_map(fn ($id) => $this->uuid->fromValue($id)->getBytes(), $ids);
176+
}
177+
}
178+
```
179+
180+
!!! note
181+
This will be needed only if you store your UUIDs in a byte format.
182+
183+
### Method naming
184+
185+
The transformation methods follow a specific naming convention:
186+
187+
- `transform{RelationName}RelationIds()` for specific relations
188+
- `transformAllRelationIds()` for a general fallback
189+
190+
### Priority order
191+
192+
1. Specific method (e.g., `transformProfileRelationIds()`) - highest priority
193+
2. General method (`transformAllRelationIds()`) - fallback
194+
3. No transformation - if neither method exists

phpstan-baseline.neon

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
5+
identifier: method.notFound
6+
count: 8
7+
path: src/Relation.php
8+
9+
-
10+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:with\(\)\.$#'
11+
identifier: method.notFound
12+
count: 1
13+
path: src/Relation.php
14+
15+
-
16+
message: '#^Cannot access property \$country on array\|bool\|float\|int\|object\|string\.$#'
17+
identifier: property.nonObject
18+
count: 1
19+
path: tests/EntityTest.php
20+
21+
-
22+
message: '#^Cannot access property \$user_id on array\|bool\|float\|int\|object\|string\.$#'
23+
identifier: property.nonObject
24+
count: 1
25+
path: tests/EntityTest.php
26+
27+
-
28+
message: '#^Cannot access property \$country on array\|bool\|float\|int\|object\|string\.$#'
29+
identifier: property.nonObject
30+
count: 1
31+
path: tests/ModelTest.php
32+
33+
-
34+
message: '#^PHPDoc tag @var with type CodeIgniter\\Entity\\Entity is not subtype of native type Tests\\Support\\Entities\\User\.$#'
35+
identifier: varTag.nativeType
36+
count: 1
37+
path: tests/ModelTest.php
38+
39+
-
40+
message: '#^Parameter \#1 \$row of method Tests\\Support\\Models\\UserModel\:\:insert\(\) expects array\<int\|string, bool\|float\|int\|object\|string\|null\>\|object\|null, array\<string, array\<string, string\>\|string\> given\.$#'
41+
identifier: argument.type
42+
count: 2
43+
path: tests/ModelTest.php
44+
45+
-
46+
message: '#^Parameter \#2 \$row of method Tests\\Support\\Models\\UserModel\:\:update\(\) expects array\<int\|string, bool\|float\|int\|object\|string\|null\>\|object\|null, array\<string, array\<string, string\>\|string\> given\.$#'
47+
identifier: argument.type
48+
count: 2
49+
path: tests/ModelTest.php
50+
51+
-
52+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
53+
identifier: method.notFound
54+
count: 5
55+
path: tests/TransformRelationIdsTest.php
56+
57+
-
58+
message: '#^Call to function method_exists\(\) with \$this\(CodeIgniter\\Model@anonymous/tests/TransformRelationIdsTest\.php\:52\) and ''transformAllRelatio…'' will always evaluate to false\.$#'
59+
identifier: function.impossibleType
60+
count: 1
61+
path: tests/TransformRelationIdsTest.php
62+
63+
-
64+
message: '#^Parameter \#1 \$row of method Tests\\Support\\Models\\UserModel\:\:insert\(\) expects array\<int\|string, bool\|float\|int\|object\|string\|null\>\|object\|null, array\<string, array\<int\|string, array\<string, int\|list\<array\<string, int\|string\>\>\|string\>\|string\>\|string\> given\.$#'
65+
identifier: argument.type
66+
count: 1
67+
path: tests/_support/Database/Seeds/SeedTests.php
68+
69+
-
70+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
71+
identifier: method.notFound
72+
count: 5
73+
path: tests/_support/Models/AddressModel.php
74+
75+
-
76+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
77+
identifier: method.notFound
78+
count: 5
79+
path: tests/_support/Models/CommentModel.php
80+
81+
-
82+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
83+
identifier: method.notFound
84+
count: 5
85+
path: tests/_support/Models/CompanyModel.php
86+
87+
-
88+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
89+
identifier: method.notFound
90+
count: 5
91+
path: tests/_support/Models/CountryModel.php
92+
93+
-
94+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
95+
identifier: method.notFound
96+
count: 5
97+
path: tests/_support/Models/CourseModel.php
98+
99+
-
100+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
101+
identifier: method.notFound
102+
count: 5
103+
path: tests/_support/Models/PostModel.php
104+
105+
-
106+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
107+
identifier: method.notFound
108+
count: 5
109+
path: tests/_support/Models/ProfileModel.php
110+
111+
-
112+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
113+
identifier: method.notFound
114+
count: 5
115+
path: tests/_support/Models/StudentModel.php
116+
117+
-
118+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
119+
identifier: method.notFound
120+
count: 5
121+
path: tests/_support/Models/UserModel.php
122+
123+
-
124+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
125+
identifier: method.notFound
126+
count: 5
127+
path: tests/_support/Models/UuidCommentModel.php
128+
129+
-
130+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
131+
identifier: method.notFound
132+
count: 5
133+
path: tests/_support/Models/UuidPostModel.php
134+
135+
-
136+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
137+
identifier: method.notFound
138+
count: 5
139+
path: tests/_support/Models/UuidProfileModel.php
140+
141+
-
142+
message: '#^Call to an undefined method CodeIgniter\\Model\:\:getTable\(\)\.$#'
143+
identifier: method.notFound
144+
count: 5
145+
path: tests/_support/Models/UuidUserModel.php

phpstan.neon.dist

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
includes:
2+
- phpstan-baseline.neon
13
parameters:
24
tmpDir: build/phpstan
35
level: 5
@@ -8,13 +10,6 @@ parameters:
810
- vendor/codeigniter4/framework/system/Test/bootstrap.php
911
excludePaths:
1012
- src/Views/*
11-
ignoreErrors:
12-
- '#Call to an undefined method CodeIgniter\\Model::with\(\).#'
13-
- '#Call to an undefined method CodeIgniter\\Model::getTable\(\).#'
14-
- '#Cannot access property \$[a-zA-Z0-9\\_]+ on array\|bool\|float\|int\|object\|string.#'
15-
- '#^.*expects array\<int\|string, bool\|float\|int\|object\|string\|null\>\|object\|null, array\<string, array\<string, string\>\|string\> given\.$#'
16-
- '#^.*expects array\<int\|string, bool\|float\|int\|object\|string\|null\>\|object\|null, array\<string, array\<int\|string, array\<string, int\|list\<array\<string, int\|string\>\>\|string\>\|string\>\|string\> given\.$#'
17-
- '#^PHPDoc tag @var with type CodeIgniter\\Entity\\Entity is not subtype of native type.*#'
1813
universalObjectCratesClasses:
1914
- CodeIgniter\Entity
2015
- CodeIgniter\Entity\Entity

src/Traits/HasRelations.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ public function with(string $relation, ?Closure $closure = null): static
6767
return $this;
6868
}
6969

70+
/**
71+
* Transform relation IDs before using them in whereIn queries
72+
* This method checks for relation-specific transform methods
73+
*/
74+
private function transformRelationIds(array $ids, string $relationName): array
75+
{
76+
// Check if there's a specific transform method for this relation
77+
// e.g., transformProfileRelationIds() for 'profile' relation
78+
$transformMethod = 'transform' . ucfirst($relationName) . 'RelationIds';
79+
80+
if (method_exists($this, $transformMethod)) {
81+
return $this->{$transformMethod}($ids);
82+
}
83+
84+
// Check for a general relation transform method
85+
if (method_exists($this, 'transformAllRelationIds')) {
86+
return $this->transformAllRelationIds($ids);
87+
}
88+
89+
return $ids;
90+
}
91+
7092
/**
7193
* Validate relation definition.
7294
*/
@@ -365,17 +387,17 @@ protected function relationsAfterFind(array $eventData): array
365387
if ($eventData['singleton']) {
366388
if ($this->tempReturnType === 'array') {
367389
foreach ($this->relations as $relationName => $relationObject) {
368-
$eventData['data'][$relationName] = $this->getDataForRelationById($eventData['data'][$relationObject->primaryKey], $relationObject);
390+
$eventData['data'][$relationName] = $this->getDataForRelationById($eventData['data'][$relationObject->primaryKey], $relationObject, $relationName);
369391
}
370392
} else {
371393
foreach ($this->relations as $relationName => $relationObject) {
372-
$eventData['data']->{$relationName} = $this->getDataForRelationById($eventData['data']->{$relationObject->primaryKey}, $relationObject);
394+
$eventData['data']->{$relationName} = $this->getDataForRelationById($eventData['data']->{$relationObject->primaryKey}, $relationObject, $relationName);
373395
}
374396
}
375397
} else {
376398
foreach ($this->relations as $relationName => $relationObject) {
377399
$ids = array_column($eventData['data'], $relationObject->primaryKey);
378-
$relationData = $this->getDataForRelationByIds($ids, $relationObject);
400+
$relationData = $this->getDataForRelationByIds($ids, $relationObject, $relationName);
379401

380402
foreach ($eventData['data'] as &$data) {
381403
if ($this->tempReturnType === 'array') {
@@ -395,9 +417,11 @@ protected function relationsAfterFind(array $eventData): array
395417
/**
396418
* Get relation data for a single item.
397419
*/
398-
protected function getDataForRelationById(int|string $id, Relation $relation)
420+
protected function getDataForRelationById(int|string $id, Relation $relation, string $relationName)
399421
{
400-
$relation->applyWith()->applyRelation([$id], $this->primaryKey)->applyConditions();
422+
$id = $this->transformRelationIds([$id], $relationName);
423+
424+
$relation->applyWith()->applyRelation($id, $this->primaryKey)->applyConditions();
401425

402426
$results = in_array($relation->type, [RelationTypes::hasOne, RelationTypes::belongsTo], true) ?
403427
$relation->model->first() :
@@ -409,8 +433,11 @@ protected function getDataForRelationById(int|string $id, Relation $relation)
409433
/**
410434
* Get relation data for many items.
411435
*/
412-
protected function getDataForRelationByIds(array $id, Relation $relation): array
436+
protected function getDataForRelationByIds(array $id, Relation $relation, string $relationName): array
413437
{
438+
// Transform the ID before applying relation
439+
$id = $this->transformRelationIds($id, $relationName);
440+
414441
$relation->applyWith()->applyRelation($id, $this->primaryKey)->applyConditions();
415442

416443
if ($relation->type === RelationTypes::hasOne && ($ofMany = $relation->getOfMany()) !== null) {

0 commit comments

Comments
 (0)