Skip to content

Commit bc3afe4

Browse files
committed
Support for adding relationship linkage meta
1 parent 10bcf57 commit bc3afe4

File tree

6 files changed

+161
-17
lines changed

6 files changed

+161
-17
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
### Added
12+
13+
- Add `linkageMeta()` method to relationship fields for adding meta to resource
14+
identifier objects in linkage
15+
16+
### Changed
17+
18+
- Various performance optimizations to improve serialization speed
19+
1120
## [1.0.0-beta.6] - 2025-10-02
1221

1322
### ⚠️ Breaking Changes

docs/relationships.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,26 @@ ToOne::make('user')->withoutLinkage();
5050
ToMany::make('roles')->withLinkage();
5151
```
5252

53-
::: danger
54-
Be careful when enabling linkage on to-many relationships as pagination is not
55-
supported.
56-
:::
53+
::: danger Be careful when enabling linkage on to-many relationships as
54+
pagination is not supported. :::
55+
56+
### Linkage Meta
57+
58+
You can add
59+
[meta information](https://jsonapi.org/format/#document-resource-object-linkage)
60+
to resource identifier objects in relationship linkage using the `linkageMeta`
61+
method. This is useful for including additional data about the relationship
62+
itself, such as pivot table attributes.
63+
64+
```php
65+
ToMany::make('roles')
66+
->withLinkage()
67+
->linkageMeta([
68+
Attribute::make('assignedAt')->get(
69+
fn($role) => $role->pivot->created_at,
70+
),
71+
]);
72+
```
5773

5874
## Relationship Mutations
5975

src/Schema/Field/Relationship.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ abstract class Relationship extends Field
1818
public array $collections;
1919
public bool $includable = false;
2020
public bool|Closure $linkage = false;
21+
public array $linkageMeta = [];
2122

2223
/**
2324
* Set the collection(s) that this relationship is to.
@@ -75,6 +76,16 @@ public function withoutLinkage(): static
7576
return $this->withLinkage(false);
7677
}
7778

79+
/**
80+
* Define meta fields for resource identifier objects in linkage.
81+
*/
82+
public function linkageMeta(array $fields): static
83+
{
84+
$this->linkageMeta = array_merge($this->linkageMeta, $fields);
85+
86+
return $this;
87+
}
88+
7889
public function getValue(Context $context): mixed
7990
{
8091
if ($context->include === null && !$this->hasLinkage($context)) {
@@ -111,6 +122,36 @@ public function serializeValue($value, Context $context): mixed
111122

112123
abstract protected function serializeData($value, Context $context): array;
113124

125+
protected function serializeIdentifier($model, Context $context): array
126+
{
127+
$context = $context->forModel($this->collections, $model);
128+
129+
$identifier = $context->serializer->addIncluded($context);
130+
131+
if ($meta = $this->serializeLinkageMeta($context)) {
132+
$identifier['meta'] = $meta;
133+
}
134+
135+
return $identifier;
136+
}
137+
138+
protected function serializeLinkageMeta(Context $context): array
139+
{
140+
$meta = [];
141+
142+
foreach ($this->linkageMeta as $field) {
143+
if (!$field->isVisible($context)) {
144+
continue;
145+
}
146+
147+
$value = $field->getValue($context);
148+
149+
$meta[$field->name] = $field->serializeValue($value, $context);
150+
}
151+
152+
return $meta;
153+
}
154+
114155
public function hasLinkage(Context $context): mixed
115156
{
116157
if ($this->linkage instanceof Closure) {

src/Schema/Field/ToMany.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,7 @@ protected function serializeData($value, Context $context): array
9696
$context = $context->withField($this);
9797

9898
return [
99-
'data' => array_map(
100-
fn($model) => $context->serializer->addIncluded(
101-
$context->forModel($this->collections, $model),
102-
),
103-
$value,
104-
),
99+
'data' => array_map(fn($model) => $this->serializeIdentifier($model, $context), $value),
105100
];
106101
}
107102

src/Schema/Field/ToOne.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,7 @@ protected function serializeData($value, Context $context): array
2929
return ['data' => null];
3030
}
3131

32-
$context = $context->withField($this);
33-
34-
return [
35-
'data' => $context->serializer->addIncluded(
36-
$context->forModel($this->collections, $value),
37-
),
38-
];
32+
return ['data' => $this->serializeIdentifier($value, $context)];
3933
}
4034

4135
public function deserializeValue(mixed $value, Context $context): mixed

tests/specification/MetaTest.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,95 @@ public function test_to_many_relationship_meta()
8787
);
8888
}
8989

90+
public function test_to_one_linkage_meta()
91+
{
92+
$role = (object) ['id' => '1', 'name' => 'admin'];
93+
94+
$this->api->resource(
95+
new MockResource(
96+
'users',
97+
models: [(object) ['id' => '1', 'role' => $role]],
98+
endpoints: [Show::make()],
99+
fields: [
100+
ToOne::make('role')
101+
->get(fn($user) => $user->role)
102+
->linkageMeta([Attribute::make('active')->get(fn() => true)]),
103+
],
104+
),
105+
);
106+
107+
$this->api->resource(new MockResource('roles', models: [$role]));
108+
109+
$response = $this->api->handle($this->buildRequest('GET', '/users/1'));
110+
111+
$this->assertJsonApiDocumentSubset(
112+
[
113+
'data' => [
114+
'relationships' => [
115+
'role' => [
116+
'data' => [
117+
'type' => 'roles',
118+
'id' => '1',
119+
'meta' => ['active' => true],
120+
],
121+
],
122+
],
123+
],
124+
],
125+
$response->getBody(),
126+
);
127+
}
128+
129+
public function test_to_many_linkage_meta()
130+
{
131+
$role1 = (object) ['id' => '1', 'name' => 'admin', 'assignedAt' => '2024-01-01'];
132+
$role2 = (object) ['id' => '2', 'name' => 'editor', 'assignedAt' => '2024-01-02'];
133+
134+
$this->api->resource(
135+
new MockResource(
136+
'users',
137+
models: [(object) ['id' => '1', 'roles' => [$role1, $role2]]],
138+
endpoints: [Show::make()],
139+
fields: [
140+
ToMany::make('roles')
141+
->get(fn($user) => $user->roles)
142+
->withLinkage()
143+
->linkageMeta([
144+
Attribute::make('assignedAt')->get(fn($role) => $role->assignedAt),
145+
]),
146+
],
147+
),
148+
);
149+
150+
$this->api->resource(new MockResource('roles', models: [$role1, $role2]));
151+
152+
$response = $this->api->handle($this->buildRequest('GET', '/users/1'));
153+
154+
$this->assertJsonApiDocumentSubset(
155+
[
156+
'data' => [
157+
'relationships' => [
158+
'roles' => [
159+
'data' => [
160+
[
161+
'type' => 'roles',
162+
'id' => '1',
163+
'meta' => ['assignedAt' => '2024-01-01'],
164+
],
165+
[
166+
'type' => 'roles',
167+
'id' => '2',
168+
'meta' => ['assignedAt' => '2024-01-02'],
169+
],
170+
],
171+
],
172+
],
173+
],
174+
],
175+
$response->getBody(),
176+
);
177+
}
178+
90179
public function test_show_endpoint_meta()
91180
{
92181
$this->api->resource(

0 commit comments

Comments
 (0)