Skip to content

Commit 84ba448

Browse files
committed
Add Create/Update::saved() hook
1 parent dabe37b commit 84ba448

File tree

8 files changed

+140
-0
lines changed

8 files changed

+140
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ and this project adheres to
7777
`Header` schema class
7878
- Add `Endpoint::response()` method for defining custom response callbacks
7979
- Add `Endpoint::schema()` method for defining custom schema
80+
- Add `Create::saved()` and `Update::saved()` to register callbacks after
81+
the model/fields are saved, but before the response is serialized
8082
- Add ability to customize error objects
8183
- Add specific exception classes for all errors
8284
- Add `JsonApi::errors(array $overrides)` method to register error object

docs/create.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,14 @@ For Laravel applications with Eloquent-backed resources, you can extend the
9595
interface for you. Learn more on the
9696
[Laravel Integration](laravel.md#eloquent-resources) page.
9797
:::
98+
99+
## Post-processing
100+
101+
Run logic after every field has been persisted but before the response document
102+
is built by chaining `saved`:
103+
104+
```php
105+
Create::make()->saved(function ($model, Context $context) {
106+
// e.g. dispatch an event or tweak the model before serialization
107+
});
108+
```

docs/update.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ interface for you. Learn more on the
9797
[Laravel Integration](laravel.md#eloquent-resources) page.
9898
:::
9999

100+
## Post-processing
101+
102+
If you need to perform work after every field has been saved but before the
103+
response document is produced, register a `saved` callback:
104+
105+
```php
106+
Update::make()->saved(function ($model, Context $context) {
107+
// e.g. refresh aggregates or mutate the context prior to serialization
108+
});
109+
```
110+
100111
## Modifying To-Many Relationships
101112

102113
By default, the JSON:API `POST` and `DELETE` relationship routes behave like a
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Tobyz\JsonApiServer\Endpoint\Concerns;
4+
5+
use Closure;
6+
use Tobyz\JsonApiServer\Context;
7+
8+
trait HasSavedCallbacks
9+
{
10+
/**
11+
* @var Closure[]
12+
*/
13+
protected array $savedCallbacks = [];
14+
15+
/**
16+
* Register a callback to run after the resource has been persisted but before the response is built.
17+
*/
18+
public function saved(Closure $callback): static
19+
{
20+
$this->savedCallbacks[] = $callback;
21+
22+
return $this;
23+
}
24+
25+
protected function runSavedCallbacks(Context $context): Context
26+
{
27+
foreach ($this->savedCallbacks as $callback) {
28+
$result = isset($context->model)
29+
? $callback($context->model, $context)
30+
: $callback($context);
31+
32+
if ($result instanceof Context) {
33+
$context = $result;
34+
}
35+
}
36+
37+
return $context;
38+
}
39+
}

src/Endpoint/Create.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Psr\Http\Message\ResponseInterface;
77
use RuntimeException;
88
use Tobyz\JsonApiServer\Context;
9+
use Tobyz\JsonApiServer\Endpoint\Concerns\HasSavedCallbacks;
910
use Tobyz\JsonApiServer\Endpoint\Concerns\HasParameters;
1011
use Tobyz\JsonApiServer\Endpoint\Concerns\HasResponse;
1112
use Tobyz\JsonApiServer\Endpoint\Concerns\MutatesResource;
@@ -27,6 +28,7 @@ class Create implements Endpoint, ProvidesRootSchema
2728
{
2829
use HasVisibility;
2930
use HasParameters;
31+
use HasSavedCallbacks;
3032
use HasResponse;
3133
use HasSchema;
3234
use MutatesResource;
@@ -111,6 +113,8 @@ public function handle(Context $context): ?ResponseInterface
111113

112114
$this->saveFields($context, true);
113115

116+
$context = $this->runSavedCallbacks($context);
117+
114118
$response = $this->createResponse(
115119
$document = $this->serializeResourceDocument($model, $context),
116120
$context,

src/Endpoint/Update.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ public function response(Closure $callback): static
7171
return $this;
7272
}
7373

74+
public function saved(Closure $callback): static
75+
{
76+
$this->updateResource->saved($callback);
77+
78+
return $this;
79+
}
80+
7481
public function headers(array $headers): static
7582
{
7683
$this->updateResource->headers($headers);

src/Endpoint/UpdateResource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Psr\Http\Message\ResponseInterface;
66
use RuntimeException;
77
use Tobyz\JsonApiServer\Context;
8+
use Tobyz\JsonApiServer\Endpoint\Concerns\HasSavedCallbacks;
89
use Tobyz\JsonApiServer\Endpoint\Concerns\HasParameters;
910
use Tobyz\JsonApiServer\Endpoint\Concerns\HasResponse;
1011
use Tobyz\JsonApiServer\Endpoint\Concerns\MutatesResource;
@@ -22,6 +23,7 @@
2223
class UpdateResource implements Endpoint, ProvidesRootSchema, ProvidesResourceLinks
2324
{
2425
use HasParameters;
26+
use HasSavedCallbacks;
2527
use HasResponse;
2628
use HasSchema;
2729
use ResolvesModel;
@@ -68,6 +70,8 @@ public function handle(Context $context): ?ResponseInterface
6870

6971
$this->saveFields($context);
7072

73+
$context = $this->runSavedCallbacks($context);
74+
7175
return $this->createResponse(
7276
$this->serializeResourceDocument($context->model, $context),
7377
$context,

tests/feature/EndpointResponseTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Tobyz\JsonApiServer\Endpoint\Show;
1212
use Tobyz\JsonApiServer\Endpoint\Update;
1313
use Tobyz\JsonApiServer\JsonApi;
14+
use Tobyz\JsonApiServer\Schema\Field\Attribute;
1415
use Tobyz\JsonApiServer\Schema\Header;
1516
use Tobyz\JsonApiServer\Schema\Type\Integer;
1617
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
@@ -91,4 +92,65 @@ public function test_response_callback(
9192

9293
$this->assertEquals('executed', $response->getHeaderLine('X-Callback'));
9394
}
95+
96+
public function test_create_after_callback_runs_before_response()
97+
{
98+
$api = new JsonApi();
99+
100+
$endpoint = Create::make()->saved(function ($model): void {
101+
$model->after = 'executed';
102+
});
103+
104+
$api->resource(
105+
new MockResource(
106+
'users',
107+
endpoints: [$endpoint],
108+
fields: [
109+
Attribute::make('after')->get(fn($model, $context) => $model->after ?? null),
110+
],
111+
),
112+
);
113+
114+
$response = $api->handle(
115+
$this->buildRequest('POST', '/users')->withParsedBody([
116+
'data' => ['type' => 'users'],
117+
]),
118+
);
119+
120+
$this->assertJsonApiDocumentSubset(
121+
['data' => ['attributes' => ['after' => 'executed']]],
122+
$response->getBody(),
123+
);
124+
}
125+
126+
public function test_update_after_callback_runs_before_response()
127+
{
128+
$api = new JsonApi();
129+
130+
$endpoint = Update::make()->saved(function ($model): void {
131+
$model->after = 'executed';
132+
});
133+
134+
$api->resource(
135+
new MockResource(
136+
'users',
137+
models: [(object) ['id' => '1']],
138+
endpoints: [$endpoint],
139+
fields: [
140+
Attribute::make('after')->get(fn($model, $context) => $model->after ?? null),
141+
],
142+
),
143+
);
144+
145+
$response = $api->handle(
146+
$this->buildRequest('PATCH', '/users/1')->withParsedBody([
147+
'data' => ['type' => 'users', 'id' => '1'],
148+
]),
149+
);
150+
151+
$this->assertJsonApiDocumentSubset(
152+
['data' => ['attributes' => ['after' => 'executed']]],
153+
$response->getBody(),
154+
);
155+
}
94156
}

0 commit comments

Comments
 (0)