Skip to content

Commit 49e70e2

Browse files
committed
[Refactor] Properly extract existing values for update requests
Closes #2
1 parent 7ec1b64 commit 49e70e2

File tree

7 files changed

+57
-172
lines changed

7 files changed

+57
-172
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
All notable changes to this project will be documented in this file. This project adheres to
44
[Semantic Versioning](http://semver.org/) and [this changelog format](http://keepachangelog.com/).
55

6+
## Unreleased
7+
8+
### Changed
9+
- [#2](https://github.com/laravel-json-api/laravel/issues/2) **BREAKING** Improved the extraction of existing resource
10+
field values when constructing validation data for update requests:
11+
- The `existingAttributes()` and `existingRelationships()` methods on the resource request class has been removed.
12+
If you need to modify the existing values before the client values are merged, implement the `withExisting()`
13+
method instead. This receives the model its JSON representation (as an array).
14+
- The `mustValidate()` method must now be called on a schema relationship field. (Previously this was on the
15+
resource relation.) By default, belongs-to and morph-to relations will be included when extracting existing
16+
values; all other relations will not. Use the `mustValidate()` or `notValidated()` method on the schema relation
17+
to alter whether a relation is included in the extracted values.
18+
619
## [1.0.0-alpha.2] - 2021-02-02
720

821
### Added

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
"require": {
2626
"php": "^7.4|^8.0",
2727
"ext-json": "*",
28-
"laravel-json-api/core": "^1.0.0-alpha.2",
29-
"laravel-json-api/eloquent": "^1.0.0-alpha.2",
30-
"laravel-json-api/encoder-neomerx": "^1.0.0-alpha.2",
28+
"laravel-json-api/core": "^1.0.0-alpha.3",
29+
"laravel-json-api/eloquent": "^1.0.0-alpha.3",
30+
"laravel-json-api/encoder-neomerx": "^1.0.0-alpha.3",
3131
"laravel-json-api/exceptions": "^1.0.0-alpha.1",
3232
"laravel-json-api/spec": "^1.0.0-alpha.2",
3333
"laravel-json-api/validation": "^1.0.0-alpha.2",

src/Http/Requests/ResourceRequest.php

Lines changed: 34 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
use Illuminate\Database\Eloquent\Model;
2525
use Illuminate\Http\Response;
2626
use LaravelJsonApi\Contracts\Auth\Authorizer;
27-
use LaravelJsonApi\Contracts\Resources\Container as ResourceContainer;
27+
use LaravelJsonApi\Contracts\Schema\Relation;
2828
use LaravelJsonApi\Core\Document\ResourceObject;
2929
use LaravelJsonApi\Core\Exceptions\JsonApiException;
30-
use LaravelJsonApi\Core\Resources\JsonApiResource;
31-
use LaravelJsonApi\Core\Resources\Relation;
30+
use LaravelJsonApi\Core\Query\IncludePaths;
3231
use LaravelJsonApi\Core\Support\Str;
3332
use LaravelJsonApi\Spec\RelationBuilder;
3433
use LaravelJsonApi\Spec\ResourceBuilder;
@@ -41,11 +40,6 @@
4140
class ResourceRequest extends FormRequest
4241
{
4342

44-
/**
45-
* @var bool
46-
*/
47-
protected bool $validateExisting = true;
48-
4943
/**
5044
* @var callable|null
5145
*/
@@ -353,43 +347,37 @@ protected function dataForCreate(array $document): array
353347
* @param Model|object $model
354348
* the model being updated.
355349
* @param array $document
356-
* the JSON API document to validate.
350+
* the JSON:API document to validate.
357351
* @return array
358352
*/
359353
protected function dataForUpdate(object $model, array $document): array
360354
{
361-
$data = $document['data'] ?? [];
362-
363-
if ($this->mustValidateExisting($model, $document)) {
364-
$data['attributes'] = $this->extractAttributes(
365-
$model,
366-
$data['attributes'] ?? []
367-
);
355+
$existing = $this->extractForUpdate($model);
368356

369-
$data['relationships'] = $this->extractRelationships(
370-
$model,
371-
$data['relationships'] ?? []
372-
);
357+
if (method_exists($this, 'withExisting')) {
358+
$existing = $this->withExisting($model, $existing) ?? $existing;
373359
}
374360

375-
return $data;
361+
return ResourceObject::fromArray($existing)->merge(
362+
$document['data']
363+
)->jsonSerialize();
376364
}
377365

378366
/**
379367
* Get validation data for modifying a relationship.
380368
*
381-
* @param Model|object $record
369+
* @param Model|object $model
382370
* @param string $fieldName
383371
* @param array $document
384372
* @return array
385373
*/
386-
protected function dataForRelationship(object $record, string $fieldName, array $document): array
374+
protected function dataForRelationship(object $model, string $fieldName, array $document): array
387375
{
388-
$resource = $this->resources()->create($record);
376+
$route = $this->jsonApi()->route();
389377

390378
return [
391-
'type' => $resource->type(),
392-
'id' => $resource->id(),
379+
'type' => $route->resourceType(),
380+
'id' => $route->resourceId(),
393381
'relationships' => [
394382
$fieldName => [
395383
'data' => $document['data'],
@@ -406,14 +394,7 @@ protected function dataForRelationship(object $record, string $fieldName, array
406394
*/
407395
protected function dataForDelete(object $record): array
408396
{
409-
$route = $this->jsonApi()->route();
410-
411-
$data = $this->dataForUpdate($record, [
412-
'data' => [
413-
'type' => $route->resourceType(),
414-
'id' => $route->resourceId(),
415-
],
416-
]);
397+
$data = $this->extractForUpdate($record);
417398

418399
if (method_exists($this, 'metaForDelete')) {
419400
$data['meta'] = (array) $this->metaForDelete($record);
@@ -423,80 +404,36 @@ protected function dataForDelete(object $record): array
423404
}
424405

425406
/**
426-
* Should existing resource values be provided to the validator for an update request?
407+
* Extract the existing values for the provided model.
427408
*
428-
* Child classes can overload this method if they need to programmatically work out
429-
* if existing values must be provided to the validator instance for an update request.
430-
*
431-
* @param Model|object $model
432-
* the model being updated
433-
* @param array $document
434-
* the JSON API document provided by the client.
435-
* @return bool
436-
*/
437-
protected function mustValidateExisting(object $model, array $document): bool
438-
{
439-
return false !== $this->validateExisting;
440-
}
441-
442-
/**
443-
* Extract existing attributes for the provided model.
444-
*
445-
* @param Model|object $model
446-
* @return iterable
409+
* @param object $model
410+
* @return array
447411
*/
448-
protected function existingAttributes(object $model): iterable
412+
private function extractForUpdate(object $model): array
449413
{
450-
$resource = $this->resources()->create($model);
414+
$encoder = $this->jsonApi()->server()->encoder();
451415

452-
return $resource->attributes($this);
416+
return $encoder
417+
->withRequest($this)
418+
->withIncludePaths($this->includePathsToExtract($model))
419+
->withResource($model)
420+
->toArray()['data'];
453421
}
454422

455423
/**
456-
* Extract existing relationships for the provided model.
424+
* Get the relationship paths that must be included when extracting the current field values.
457425
*
458-
* @param Model|object $model
459-
* @return iterable
426+
* @param object $model
427+
* @return IncludePaths
460428
*/
461-
protected function existingRelationships(object $model): iterable
429+
private function includePathsToExtract(object $model): IncludePaths
462430
{
463-
$resource = $this->resources()->create($model);
464-
465-
/** @var Relation $relationship */
466-
foreach ($resource->relationships($this) as $relationship) {
467-
if ($relationship->isValidated()) {
468-
yield $relationship->fieldName() => $relationship->data();
469-
}
470-
}
471-
}
431+
$paths = collect($this->schema()->relationships())
432+
->filter(fn (Relation $relation) => $relation->isValidated())
433+
->map(fn (Relation $relation) => $relation->name())
434+
->values();
472435

473-
/**
474-
* Extract attributes for a resource update.
475-
*
476-
* @param Model|object $model
477-
* @param array $new
478-
* @return array
479-
*/
480-
private function extractAttributes(object $model, array $new): array
481-
{
482-
return collect($this->existingAttributes($model))
483-
->merge($new)
484-
->all();
485-
}
486-
487-
/**
488-
* Extract relationships for a resource update.
489-
*
490-
* @param Model|object $model
491-
* @param array $new
492-
* @return array
493-
*/
494-
private function extractRelationships(object $model, array $new): array
495-
{
496-
return collect($this->existingRelationships($model))
497-
->map(fn($value) => $this->convertExistingRelationships($value))
498-
->merge($new)
499-
->all();
436+
return IncludePaths::fromArray($paths);
500437
}
501438

502439
/**
@@ -514,64 +451,6 @@ private function relationshipRules(): array
514451
->all();
515452
}
516453

517-
/**
518-
* Convert relationships returned by the `existingRelationships()` method.
519-
*
520-
* We support the method returning JSON API formatted relationships, e.g.:
521-
*
522-
* ```
523-
* return [
524-
* 'author' => [
525-
* 'data' => [
526-
* 'type' => 'users',
527-
* 'id' => (string) $record->author->getRouteKey(),
528-
* ],
529-
* ],
530-
* ];
531-
* ```
532-
*
533-
* Or this shorthand:
534-
*
535-
* ```php
536-
* return [
537-
* 'author' => $record->author,
538-
* ];
539-
* ```
540-
*
541-
* This method converts the shorthand into the JSON API formatted relationships.
542-
*
543-
* @param $value
544-
* @return array
545-
*/
546-
private function convertExistingRelationships($value)
547-
{
548-
if (is_array($value) && array_key_exists('data', $value)) {
549-
return $value;
550-
}
551-
552-
if (is_null($value)) {
553-
return ['data' => null];
554-
}
555-
556-
$value = $this->resources()->resolve($value);
557-
558-
if ($value instanceof JsonApiResource) {
559-
return [
560-
'data' => [
561-
'type' => $value->type(),
562-
'id' => $value->id(),
563-
],
564-
];
565-
}
566-
567-
return [
568-
'data' => collect($value)->map(fn(JsonApiResource $resource) => [
569-
'type' => $resource->type(),
570-
'id' => $resource->id()
571-
])->all(),
572-
];
573-
}
574-
575454
/**
576455
* Validate the JSON API document for a resource request.
577456
*
@@ -618,12 +497,4 @@ private function validateRelationshipDocument(): void
618497
}
619498
}
620499

621-
/**
622-
* @return ResourceContainer
623-
*/
624-
final protected function resources(): ResourceContainer
625-
{
626-
return $this->jsonApi()->server()->resources();
627-
}
628-
629500
}

tests/dummy/app/JsonApi/V1/Posts/PostResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function relationships($request): iterable
5555
return [
5656
$this->relation('author')->showDataIfLoaded(),
5757
$this->relation('comments'),
58-
$this->relation('tags')->showDataIfLoaded()->mustValidate(),
58+
$this->relation('tags')->showDataIfLoaded(),
5959
];
6060
}
6161

tests/dummy/tests/Api/V1/Posts/DeleteTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ protected function setUp(): void
4444
public function test(): void
4545
{
4646
$response = $this
47+
->withoutExceptionHandling()
4748
->actingAs($this->post->author)
4849
->jsonApi()
4950
->delete(url('api/v1/posts', $this->post));

tests/dummy/tests/Api/V1/Posts/IndexTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,13 @@ public function testIncludeAuthor(): void
101101
{
102102
$posts = Post::factory()->count(2)->create();
103103

104-
$expected1 = $this->serializer->post($posts[0])->toArray();
104+
$expected1 = $this->serializer->post($posts[0])->jsonSerialize();
105105
$expected1['relationships']['author']['data'] = $user1 = [
106106
'type' => 'users',
107107
'id' => (string) $posts[0]->author->getRouteKey(),
108108
];
109109

110-
$expected2 = $this->serializer->post($posts[1])->toArray();
110+
$expected2 = $this->serializer->post($posts[1])->jsonSerialize();
111111
$expected2['relationships']['author']['data'] = $user2 = [
112112
'type' => 'users',
113113
'id' => (string) $posts[1]->author->getRouteKey(),
@@ -143,7 +143,7 @@ public function testIdFilter(): void
143143
public function testSlugFilter(): void
144144
{
145145
$posts = Post::factory()->count(2)->create();
146-
$expected = $this->serializer->post($posts[1])->toArray();
146+
$expected = $this->serializer->post($posts[1])->jsonSerialize();
147147

148148
$response = $this
149149
->withoutExceptionHandling()

tests/dummy/tests/Api/V1/Posts/UpdateTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public function testIndividualField(string $fieldName): void
9797
->replace($fieldName, $data[$fieldName]);
9898

9999
$response = $this
100+
->withoutExceptionHandling()
100101
->actingAs($this->post->author)
101102
->jsonApi('posts')
102103
->withData($data)
@@ -165,8 +166,7 @@ public function testNotAcceptableMediaType(): void
165166

166167
$response = $this
167168
->actingAs($this->post->author)
168-
->jsonApi()
169-
->expects('posts')
169+
->jsonApi('posts')
170170
->accept('text/html')
171171
->withData($data)
172172
->patch(url('/api/v1/posts', $this->post));

0 commit comments

Comments
 (0)