diff --git a/README.md b/README.md index c9c4373..804becf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,84 @@ # JSON:API Resource for Laravel + Make your Laravel API [JSON:API](https://jsonapi.org/) compliant with the `Brainstud\JsonApi` package. + +## Usage + +Install the package with Composer: + +```bash +composer require brainstud/json-api-resource +``` + +After that, you can create new resources that extend the `JsonApiResource` of this package. + +```php +// Post.php +class Post extends Model { + protected $fillable = [ + 'title', + 'content', + ]; + + + public function comments(): HasMany + { + return $this->hasMany(Comment::class); + } + +} + + +// PostResource.php +class PostResource extends JsonApiResource { + + protected function register() { + $data = [ + 'id' => $this->resourceObject->identifier, + 'type' => 'posts', + 'attributes' => [ + 'title' => $this->resourceObject->title, + 'content' => $this->resourceObject->content, + ], + 'relationships' => ['comments', CommentCollectionResource::class] + ]; + + return $data; + } +} + +// PostCollectionResource.php +class PostCollectionResource extends JsonApiCollectionResource { + + public $collects = PostResource::class; + +} + +// PostController.php +class PostController extends Controller { + + public function index(){ + $posts = Post::all()->load('comments'); + return new PostCollectionResource($posts); + } + + public function show (Post $post) { + return new PostResource($post->load('comments')); + } +} +``` + + + + + + + + + + + ## Example usage ```php // Course.php diff --git a/src/Resources/JsonApiResource.php b/src/Resources/JsonApiResource.php index 4e55aad..0f916e2 100644 --- a/src/Resources/JsonApiResource.php +++ b/src/Resources/JsonApiResource.php @@ -15,6 +15,25 @@ abstract class JsonApiResource extends JsonResource */ protected mixed $resourceObject; + + /** + * Prevent specific properties from the model to be published in the resource. + * @var $except string[] + */ + protected array $except; + + /** + * Select specific attributes from the model of the resource. + * @var $only string[] + */ + protected array $only; + + /** + * Overwrite model key where the model is identified by within the resource (e.g. `identifier` or `id`). + * @var string $identifiedBy + */ + protected string $identifiedBy; + /** * The registered resource data * @var array @@ -66,7 +85,7 @@ public function __construct($jsonApiResourceData) $this->resourceDepth = $resourceDepth ?? 0; $this->resourceObject = $resource; $this->resourceRegistrationData = $this->register(); - $this->resourceKey = "{$this->resourceRegistrationData['type']}.{$this->resourceRegistrationData['id']}"; + $this->resourceKey = "{$this->getType()}.{$this->getId()}"; if ($this->resourceDepth < $this->maxResourceDepth) { $this->mapRelationships(); @@ -83,6 +102,54 @@ public function __construct($jsonApiResourceData) */ abstract protected function register(): array; + /** + * toId. + * + * When string is returned, it will be set as the JSON:API `id` property. + * @return string|null + */ + protected function toId(): null | string { + return null; + } + + /** + * getId. + * + * Returns the id of the resource. + * @return string + */ + protected function getId(): string { + return ( + $this->toId() + ?? $this->resourceRegistrationData['id'] + ?? $this->resourceObject->{$this->identifiedBy ?? $this->resourceObject->getRouteKeyName()} + ); + } + + /** + * toType. + * + * When string is returned, it will be set as the JSON:API `type` property. + * @return string|null + */ + protected function toType(): null | string { + return null; + } + + /** + * getType. + * + * Returns the type of the resource. + * @return string + */ + protected function getType(){ + return ( + $this->toType() ?? + $this->resourceRegistrationData['type'] ?? + Str::snake(Str::plural(class_basename($this->resourceObject))) + ); + } + /** * Build the response @@ -96,8 +163,8 @@ public function toArray($request): array } $response = [ - 'id' => $this->resourceRegistrationData['id'], - 'type' => $this->resourceRegistrationData['type'], + 'id' => $this->resourceObject->{$this->identifiedBy ?? $this->resourceObject->getRouteKeyName()}, + 'type' => $this->getType(), 'attributes' => $this->getAttributes($request), ]; @@ -277,6 +344,8 @@ private function addInclude(JsonApiResource $includedResource): self { * * Merges two similar resources together. * + * TODO: Make sure this combine method works with dynamic attributes (`only`, `except`, `toAttributes`). + * * @param JsonApiResource|null $second * @return JsonApiResource */ @@ -301,6 +370,18 @@ public function getIncludedResources(): Collection { return collect(array_values($this->included)); } + /** + * toAttributes. + * + * When an array is returned, it will set the attributes of the resource to that array. + * + * @param Request $request + * @param $model + * @return array|null + */ + protected function toAttributes(Request $request, $model): ?array { + return null; + } /** * getAttributes. @@ -312,17 +393,46 @@ public function getIncludedResources(): Collection { */ private function getAttributes(Request $request) { - $attributes = $this->resourceRegistrationData['attributes']; - $type = $this->resourceRegistrationData['type']; - - if (!($fieldSet = $request->query('fields')) - || !array_key_exists($type, $fieldSet) - || !($fields = explode(',', $fieldSet[$type])) + if( + array_key_exists('attributes', $this->resourceRegistrationData) + || $this->toAttributes($request, $this->resourceObject) !== null + ){ + $attributes = array_merge( + $this->resourceRegistrationData['attributes'] ?? [], + $this->toAttributes($request, $this->resourceObject) ?? [], + ); + }else{ + $keys = $this->only ?? ( + array_filter( + $this->resourceObject->getFillable(), + fn($key) => !in_array($key, ['identifier', ...($this->except ?? [])]) + ) + ); + + $attributes = array_filter( + $this->resourceObject->getAttributes(), + fn($key) => ( + !!$this->resourceObject->{$key} + && in_array($key, $keys) + ), + ARRAY_FILTER_USE_KEY + ); + } + $type = $this->getType(); + if ( + ($fieldSet = $request->query('fields')) + && array_key_exists($type, $fieldSet) + && ($fields = explode(',', $fieldSet[$type])) ) { - return $attributes; + $attributes = array_filter($attributes, fn($key) => in_array($key, $fields), ARRAY_FILTER_USE_KEY); } - return array_filter($attributes, fn ($key) => in_array($key, $fields), ARRAY_FILTER_USE_KEY); + return array_map(fn ($attribute) => ( + is_callable($attribute) + ? $attribute($request, $this->resourceObject) + : $attribute + ), $attributes); + } /** @@ -332,8 +442,8 @@ private function getAttributes(Request $request) public function toRelationshipReferenceArray(): array { return [ - 'id' => $this->resourceRegistrationData['id'], - 'type' => $this->resourceRegistrationData['type'], + 'id' => $this->getId(), + 'type' => $this->getType(), ]; } diff --git a/tests/Resources/AccountResource.php b/tests/Resources/AccountResource.php index 18029a3..55b8ff7 100644 --- a/tests/Resources/AccountResource.php +++ b/tests/Resources/AccountResource.php @@ -11,21 +11,12 @@ protected function register(): array { $data = [ - 'id' => $this->resourceObject->identifier, - 'type' => 'accounts', - 'attributes' => [ - 'name' => $this->resourceObject->name, - ], 'relationships' => [ 'posts' => ['posts', PostCollectionResource::class], 'comments' => ['comments', CommentCollectionResource::class], ], ]; - if($this->resourceObject->email){ - $data['attributes']['email'] = $this->resourceObject->email; - } - if($this->resourceObject->posts()->count() >= 10){ $data['meta'] = [ 'experienced_author' => true, @@ -35,4 +26,4 @@ protected function register(): array return $data; } -} \ No newline at end of file +} diff --git a/tests/Resources/CommentResource.php b/tests/Resources/CommentResource.php index 8bd8dc2..2c75d04 100644 --- a/tests/Resources/CommentResource.php +++ b/tests/Resources/CommentResource.php @@ -22,4 +22,4 @@ protected function register(): array return $data; } -} \ No newline at end of file +} diff --git a/tests/Resources/PostResource.php b/tests/Resources/PostResource.php index 0b50472..2fa1da4 100644 --- a/tests/Resources/PostResource.php +++ b/tests/Resources/PostResource.php @@ -31,4 +31,4 @@ protected function register(): array return $data; } -} \ No newline at end of file +} diff --git a/tests/Unit/JsonApiResourceTest.php b/tests/Unit/JsonApiResourceTest.php index 7c53998..21de796 100644 --- a/tests/Unit/JsonApiResourceTest.php +++ b/tests/Unit/JsonApiResourceTest.php @@ -70,7 +70,7 @@ public function testRelatedResource() $response->assertExactJson([ 'data' => $this->createJsonResource($post, [ 'author' => $author]), - 'included' => [$this->createJsonResource($author)], + 'included' => [ $this->createJsonResource($author)], ]); } @@ -262,4 +262,4 @@ public function testResourceIncludedSparseFieldset() -} \ No newline at end of file +}