From de658963e88712bb3f5ff87ecf467f09d776933b Mon Sep 17 00:00:00 2001 From: brdv Date: Thu, 13 Apr 2023 09:55:36 +0200 Subject: [PATCH 1/2] move CreateJsonResource from testcase to custom trait --- src/Traits/JsonResourceHelper.php | 93 +++++++++++++++++++++++++++++++ tests/TestCase.php | 84 +--------------------------- 2 files changed, 95 insertions(+), 82 deletions(-) create mode 100644 src/Traits/JsonResourceHelper.php diff --git a/src/Traits/JsonResourceHelper.php b/src/Traits/JsonResourceHelper.php new file mode 100644 index 0000000..492aa72 --- /dev/null +++ b/src/Traits/JsonResourceHelper.php @@ -0,0 +1,93 @@ +map(fn ($model) => ( + $this->createJsonResource($model, $relationships, type: $type, isReferenceObject: $isReferenceObject) + ))->toArray(); + } elseif ($isReferenceObject) { + $data = [ + 'id' => $modelOrCollection->identifier, + 'type' => $type, + ]; + } else { + $fillableAttributes = $modelOrCollection->getFillable(); + $data = [ + 'id' => $modelOrCollection->identifier, + 'type' => $type, + 'attributes' => ( + array_filter( + $modelOrCollection->getAttributes(), + fn($key) => ( + !!$modelOrCollection->{$key} + && in_array($key, $fillableAttributes) + && (empty($onlyAttributes) || in_array($key, $onlyAttributes)) + && !in_array($key, ['identifier', ...$exceptAttributes]) + ), + ARRAY_FILTER_USE_KEY + ) + ), + ]; + } + + if(!$isReferenceObject && $relationships) { + $data['relationships'] = collect(array_keys($relationships))->reduce(function ($rels, $relationKey) use ($relationships) { + $relation = $relationships[$relationKey]; + if ($relation instanceof Model){ + $rels[$relationKey] = [ + 'data' => $this->createJsonResource($relation, isReferenceObject: true) + ]; + }else{ + $rels[$relationKey] = [ + 'data' => collect($relation)->map(fn ($relatedModel) => ( + $this->createJsonResource($relatedModel, isReferenceObject: true) + )), + ]; + } + return $rels; + }, []); + } + + if($meta){ + $data['meta'] = $meta; + } + + if($links){ + $data['links'] = $links; + } + + return $data; + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index d996a5e..f77ff95 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,11 @@ namespace Brainstud\JsonApi\Tests; -use Illuminate\Database\Eloquent\Collection; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; +use Brainstud\JsonApi\Traits\JsonResourceHelper; class TestCase extends \Orchestra\Testbench\TestCase { + use JsonResourceHelper; protected function getEnvironmentSetUp($app) { $app['config']->set('database.default', 'testing'); @@ -26,83 +25,4 @@ public function setUp(): void '--path' => realpath(__DIR__ . '/database/migrations') ]); } - - - /** - * createJsonResource. - * - * Makes it easier to create an array structure of an API resource to prevent being repetitive. - */ - protected function createJsonResource( - Collection|Model $modelOrCollection, - array $relationships = null, - array $meta = null, - array $links = null, - string $type = null, - array $exceptAttributes = [], - array $onlyAttributes = [], - bool $isReferenceObject = false, - ): array { - - $type ??= ($modelOrCollection instanceof Collection) - ? Str::snake(Str::plural(class_basename($modelOrCollection[0]))) - : Str::snake(Str::plural(class_basename($modelOrCollection))); - - if( $modelOrCollection instanceof Collection ) { - return $modelOrCollection->map(fn ($model) => ( - $this->createJsonResource($model, $relationships, type: $type, isReferenceObject: $isReferenceObject) - ))->toArray(); - } elseif ($isReferenceObject) { - $data = [ - 'id' => $modelOrCollection->identifier, - 'type' => $type, - ]; - } else { - $fillableAttributes = $modelOrCollection->getFillable(); - $data = [ - 'id' => $modelOrCollection->identifier, - 'type' => $type, - 'attributes' => ( - array_filter( - $modelOrCollection->getAttributes(), - fn($key) => ( - !!$modelOrCollection->{$key} - && in_array($key, $fillableAttributes) - && (empty($onlyAttributes) || in_array($key, $onlyAttributes)) - && !in_array($key, ['identifier', ...$exceptAttributes]) - ), - ARRAY_FILTER_USE_KEY - ) - ), - ]; - } - - if(!$isReferenceObject && $relationships) { - $data['relationships'] = collect(array_keys($relationships))->reduce(function ($rels, $relationKey) use ($relationships) { - $relation = $relationships[$relationKey]; - if ($relation instanceof Model){ - $rels[$relationKey] = [ - 'data' => $this->createJsonResource($relation, isReferenceObject: true) - ]; - }else{ - $rels[$relationKey] = [ - 'data' => collect($relation)->map(fn ($relatedModel) => ( - $this->createJsonResource($relatedModel, isReferenceObject: true) - )), - ]; - } - return $rels; - }, []); - } - - if($meta){ - $data['meta'] = $meta; - } - - if($links){ - $data['links'] = $links; - } - - return $data; - } } \ No newline at end of file From 6c0dd6e5369f848cb629dc8070a0371581f2221b Mon Sep 17 00:00:00 2001 From: brdv Date: Thu, 13 Apr 2023 10:57:32 +0200 Subject: [PATCH 2/2] Add function to trait to include columns with value null to attibutes --- src/Traits/JsonResourceHelper.php | 54 +++++++++++++++----- tests/Unit/JsonApiCollectionResourceTest.php | 16 +++--- tests/Unit/JsonApiResourceTest.php | 22 ++++---- 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/Traits/JsonResourceHelper.php b/src/Traits/JsonResourceHelper.php index 492aa72..d7fefaf 100644 --- a/src/Traits/JsonResourceHelper.php +++ b/src/Traits/JsonResourceHelper.php @@ -28,14 +28,19 @@ protected function createJsonResource( array $onlyAttributes = [], bool $isReferenceObject = false, ): array { - $type ??= ($modelOrCollection instanceof Collection) ? Str::snake(Str::plural(class_basename($modelOrCollection[0]))) : Str::snake(Str::plural(class_basename($modelOrCollection))); - if( $modelOrCollection instanceof Collection ) { + if ($modelOrCollection instanceof Collection) { return $modelOrCollection->map(fn ($model) => ( - $this->createJsonResource($model, $relationships, type: $type, isReferenceObject: $isReferenceObject) + $this->createJsonResource( + $model, + $relationships, + type: $type, + exceptAttributes: $exceptAttributes, + isReferenceObject: $isReferenceObject, + ) ))->toArray(); } elseif ($isReferenceObject) { $data = [ @@ -49,10 +54,9 @@ protected function createJsonResource( 'type' => $type, 'attributes' => ( array_filter( - $modelOrCollection->getAttributes(), - fn($key) => ( - !!$modelOrCollection->{$key} - && in_array($key, $fillableAttributes) + $this->getAllAttributes($modelOrCollection), + fn ($key) => ( + in_array($key, $fillableAttributes) && (empty($onlyAttributes) || in_array($key, $onlyAttributes)) && !in_array($key, ['identifier', ...$exceptAttributes]) ), @@ -62,32 +66,56 @@ protected function createJsonResource( ]; } - if(!$isReferenceObject && $relationships) { + if (!$isReferenceObject && $relationships) { $data['relationships'] = collect(array_keys($relationships))->reduce(function ($rels, $relationKey) use ($relationships) { $relation = $relationships[$relationKey]; - if ($relation instanceof Model){ + if ($relation instanceof Model) { $rels[$relationKey] = [ - 'data' => $this->createJsonResource($relation, isReferenceObject: true) + 'data' => $this->createJsonResource($relation, isReferenceObject: true), ]; - }else{ + } else { $rels[$relationKey] = [ 'data' => collect($relation)->map(fn ($relatedModel) => ( $this->createJsonResource($relatedModel, isReferenceObject: true) )), ]; } + return $rels; }, []); } - if($meta){ + if ($meta) { $data['meta'] = $meta; } - if($links){ + if ($links) { $data['links'] = $links; } return $data; } + + /** + * By default, laravel does not add columns where the value is null + * to the retrieved model. Therefore, we combine the received and + * fillable fields on the model. If a field is fillable, but not + * on the given model, it adds it to the attributes array with + * a value of null. + * + */ + private function getAllAttributes(Model $model): array + { + $columns = $model->getFillable(); + + $attributes = $model->getAttributes(); + + foreach ($columns as $column) { + if (!array_key_exists($column, $attributes)) { + $attributes[$column] = null; + } + } + + return $attributes; + } } \ No newline at end of file diff --git a/tests/Unit/JsonApiCollectionResourceTest.php b/tests/Unit/JsonApiCollectionResourceTest.php index 2dadce0..03d7df6 100644 --- a/tests/Unit/JsonApiCollectionResourceTest.php +++ b/tests/Unit/JsonApiCollectionResourceTest.php @@ -20,9 +20,9 @@ public function testBasicResourceCollectionResource() $response->assertExactJson([ 'data' => [ - $this->createJsonResource($accounts[0]), - $this->createJsonResource($accounts[1]), - $this->createJsonResource($accounts[2]), + $this->createJsonResource($accounts[0], exceptAttributes: ['email']), + $this->createJsonResource($accounts[1], exceptAttributes: ['email']), + $this->createJsonResource($accounts[2], exceptAttributes: ['email']), ], ]); } @@ -38,8 +38,8 @@ public function testCollectionResourceWithRelations() $response->assertExactJson([ 'data' => [ - ...$this->createJsonResource($others), - $this->createJsonResource($author, [ 'posts' => $author->posts ]), + ...$this->createJsonResource($others, exceptAttributes: ['email']), + $this->createJsonResource($author, [ 'posts' => $author->posts ], exceptAttributes: ['email']), ], 'included' => [ $this->createJsonResource($author->posts->first()) @@ -89,15 +89,15 @@ public function testCollectionResourceEnlargeResourceDepth1() $response->assertExactJson([ 'data' => [ - $this->createJsonResource($authorClaire, [ 'posts' => $postsClaire ]), + $this->createJsonResource($authorClaire, [ 'posts' => $postsClaire ], exceptAttributes: ['email']), ], 'included' => [ $this->createJsonResource($postClaire, [ 'comments' => $postClaire->comments ]), $this->createJsonResource($postClaire->comments[0], [ 'commenter' => $postClaire->comments[0]->commenter ]), $this->createJsonResource($postClaire->comments[1], [ 'commenter' => $postClaire->comments[1]->commenter ]), $this->createJsonResource($postClaire->comments[2], [ 'commenter' => $postClaire->comments[2]->commenter ]), - $this->createJsonResource($postClaire->comments[0]->commenter), - $this->createJsonResource($postClaire->comments[2]->commenter), + $this->createJsonResource($postClaire->comments[0]->commenter, exceptAttributes: ['email']), + $this->createJsonResource($postClaire->comments[2]->commenter, exceptAttributes: ['email']), ] ]); } diff --git a/tests/Unit/JsonApiResourceTest.php b/tests/Unit/JsonApiResourceTest.php index be4d1ab..363c160 100644 --- a/tests/Unit/JsonApiResourceTest.php +++ b/tests/Unit/JsonApiResourceTest.php @@ -39,7 +39,7 @@ public function testBasicResourceCollection() $response = $this->getJson('test-route'); $response->assertExactJson([ - 'data' => $this->createJsonResource($accounts), + 'data' => $this->createJsonResource($accounts, exceptAttributes: ['email']), ]); } @@ -54,7 +54,7 @@ public function testResourceWithEmptyRelationLoaded() $response = $this->getJson('test-route'); $response->assertExactJson([ - 'data' => $this->createJsonResource($account), + 'data' => $this->createJsonResource($account, exceptAttributes: ['email']), ]); } @@ -70,11 +70,10 @@ public function testRelatedResource() $response->assertExactJson([ 'data' => $this->createJsonResource($post, [ 'author' => $author]), - 'included' => [$this->createJsonResource($author)], + 'included' => [$this->createJsonResource($author, exceptAttributes: ['email'])], ]); } - public function testRelatedResources() { $post = Post::factory() @@ -111,7 +110,7 @@ public function testDuplicatedRelatedResources() $response->assertExactJson([ 'data' => $this->createJsonResource($post, ['author' => $author, 'comments' => [ $comment ]]), 'included' => [ - $this->createJsonResource($author), + $this->createJsonResource($author, exceptAttributes: ['email']), $this->createJsonResource($comment, [ 'commenter' => $author ]), ] ]); @@ -140,13 +139,13 @@ public function testDeepRelatedResource() $response->assertExactJson([ - 'data' => $this->createJsonResource($author, [ 'posts' => [ $post ]]), + 'data' => $this->createJsonResource($author, [ 'posts' => [ $post ]], exceptAttributes: ['email']), 'included' => [ $this->createJsonResource($post, [ 'author' => $author, 'comments' => $comments ]), - $this->createJsonResource($author), + $this->createJsonResource($author, exceptAttributes: ['email']), $this->createJsonResource($comments[0], [ 'commenter' => $commenter ]), $this->createJsonResource($comments[1], [ 'commenter' => $authorAsCommenter ]), - $this->createJsonResource($commenter), + $this->createJsonResource($commenter, exceptAttributes: ['email']), ] ]); } @@ -171,10 +170,10 @@ public function testTooDeepRelatedResource() $response = $this->getJson('test-route?includes=posts,posts.author,posts.comments,posts.comments.commenter'); $response->assertExactJson([ - 'data' => $this->createJsonResource($postAuthor, [ 'posts' => [ $post ]]), + 'data' => $this->createJsonResource($postAuthor, [ 'posts' => [ $post ]], exceptAttributes: ['email']), 'included' => [ $this->createJsonResource($post, [ 'author' => $postAuthor, 'comments' => $comments ]), - $this->createJsonResource($postAuthor), + $this->createJsonResource($postAuthor, exceptAttributes: ['email']), $this->createJsonResource($comments[0]), $this->createJsonResource($comments[1]), ] @@ -196,7 +195,8 @@ public function testResourceWithMetaData() 'data' => $this->createJsonResource( modelOrCollection: $account, relationships: [ 'posts' => $account->posts ], - meta: [ 'experienced_author' => true ] + meta: [ 'experienced_author' => true ], + exceptAttributes: ['email'], ), 'included' => $this->createJsonResource($account->posts) ]);