Skip to content

Commit 978a8fd

Browse files
committed
Add support for mapping nested API relations in models
Introduces the $apiRelations property and castApiRelation method to ApiModelTrait, enabling flexible mapping and instantiation of nested relations from API responses. Adds comprehensive tests for recursive relation mapping using FolderModel and FileModel.
1 parent 4d21d35 commit 978a8fd

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed

src/Traits/ApiModelTrait.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,76 @@
44

55
use Illuminate\Pagination\LengthAwarePaginator;
66
use Illuminate\Support\Collection;
7+
use Kroderdev\LaravelMicroserviceCore\Interfaces\ApiModelContract;
78

89
trait ApiModelTrait
910
{
11+
/**
12+
* Relation mapping callbacks.
13+
*
14+
* Each key should be the relation name and the value either a class name,
15+
* an array with the class name for collections, or a closure that returns
16+
* the relation value.
17+
*/
18+
protected static array $apiRelations = [];
19+
1020
/**
1121
* Instantiate a model from raw API data.
1222
*/
1323
public static function fromApiResponse(array $data): static
1424
{
1525
$instance = new static();
26+
27+
foreach (static::$apiRelations as $relation => $cast) {
28+
if (array_key_exists($relation, $data)) {
29+
$instance->setRelation(
30+
$relation,
31+
static::castApiRelation($cast, $data[$relation])
32+
);
33+
unset($data[$relation]);
34+
}
35+
}
36+
1637
$instance->fill($data);
1738

1839
return $instance;
1940
}
2041

42+
/**
43+
* Cast the given relation value based on the provided definition.
44+
*/
45+
protected static function castApiRelation(mixed $cast, mixed $value): mixed
46+
{
47+
if (is_callable($cast)) {
48+
return $cast($value);
49+
}
50+
51+
if (is_string($cast) && method_exists(static::class, $cast)) {
52+
return static::$cast($value);
53+
}
54+
55+
if (is_array($cast)) {
56+
$class = $cast[0];
57+
58+
return collect($value)->map(fn ($item) => static::castApiRelation($class, $item));
59+
}
60+
61+
if (is_string($cast) && class_exists($cast)) {
62+
if (is_subclass_of($cast, ApiModelContract::class)) {
63+
return $cast::fromApiResponse($value);
64+
}
65+
66+
$model = new $cast();
67+
if (method_exists($model, 'fill')) {
68+
$model->fill($value);
69+
}
70+
71+
return $model;
72+
}
73+
74+
return $value;
75+
}
76+
2177
/**
2278
* Create a collection of models from an array of payloads.
2379
*/

tests/Models/ApiModelTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ class RemoteUser extends ApiModel
1616
protected $fillable = ['id', 'name'];
1717
}
1818

19+
class FileModel extends ApiModel
20+
{
21+
protected $fillable = ['id', 'type', 'name', 'size'];
22+
}
23+
24+
class FolderModel extends ApiModel
25+
{
26+
protected $fillable = ['id', 'name', 'children'];
27+
28+
protected static array $apiRelations = [
29+
'children' => 'mapChildren',
30+
];
31+
32+
protected static function mapChildren(array $items)
33+
{
34+
return collect($items)->map(function (array $data) {
35+
if ($data['type'] === 'file') {
36+
return FileModel::fromApiResponse($data);
37+
}
38+
39+
if ($data['type'] === 'folder') {
40+
return FolderModel::fromApiResponse($data);
41+
}
42+
43+
throw new \UnexpectedValueException('Unknown child type: '.$data['type']);
44+
});
45+
}
46+
}
47+
1948
class ApiModelTest extends TestCase
2049
{
2150
protected FakeGatewayClient $gateway;
@@ -179,4 +208,53 @@ public function delete_users_gateway()
179208
['method' => 'DELETE', 'uri' => '/users/4'],
180209
], $this->gateway->getCalls());
181210
}
211+
212+
213+
/** @test */
214+
public function from_api_response_maps_nested_relations()
215+
{
216+
$data = [
217+
'id' => 1,
218+
'name' => 'root',
219+
'type' => 'folder',
220+
'children' => [
221+
['id' => 2, 'name' => 'file1.txt', 'type' => 'file', 'size' => 123],
222+
[
223+
'id' => 3,
224+
'name' => 'docs',
225+
'type' => 'folder',
226+
'children' => [
227+
['id' => 4, 'name' => 'file2.txt', 'type' => 'file', 'size' => 456],
228+
[
229+
'id' => 5,
230+
'name' => 'deep',
231+
'type' => 'folder',
232+
'children' => [
233+
['id' => 6, 'name' => 'file3.txt', 'type' => 'file', 'size' => 789],
234+
],
235+
],
236+
],
237+
],
238+
],
239+
];
240+
241+
$folder = FolderModel::fromApiResponse($data);
242+
243+
$this->assertEquals(1, $folder->id);
244+
$this->assertCount(2, $folder->children);
245+
$this->assertInstanceOf(FileModel::class, $folder->children[0]);
246+
$this->assertEquals(123, $folder->children[0]->size);
247+
$this->assertInstanceOf(FolderModel::class, $folder->children[1]);
248+
249+
$subfolder = $folder->children[1];
250+
$this->assertCount(2, $subfolder->children);
251+
$this->assertInstanceOf(FileModel::class, $subfolder->children[0]);
252+
$this->assertInstanceOf(FolderModel::class, $subfolder->children[1]);
253+
254+
$deep = $subfolder->children[1];
255+
$this->assertCount(1, $deep->children);
256+
$this->assertInstanceOf(FileModel::class, $deep->children[0]);
257+
$this->assertEquals('file3.txt', $deep->children[0]->name);
258+
$this->assertEquals(789, $deep->children[0]->size);
259+
}
182260
}

0 commit comments

Comments
 (0)