Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
136 changes: 123 additions & 13 deletions src/Resources/JsonApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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),
];

Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand All @@ -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);

}

/**
Expand All @@ -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(),
];
}

Expand Down
11 changes: 1 addition & 10 deletions tests/Resources/AccountResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,4 +26,4 @@ protected function register(): array

return $data;
}
}
}
2 changes: 1 addition & 1 deletion tests/Resources/CommentResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ protected function register(): array

return $data;
}
}
}
2 changes: 1 addition & 1 deletion tests/Resources/PostResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ protected function register(): array

return $data;
}
}
}
4 changes: 2 additions & 2 deletions tests/Unit/JsonApiResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function testRelatedResource()

$response->assertExactJson([
'data' => $this->createJsonResource($post, [ 'author' => $author]),
'included' => [$this->createJsonResource($author)],
'included' => [ $this->createJsonResource($author)],
]);
}

Expand Down Expand Up @@ -262,4 +262,4 @@ public function testResourceIncludedSparseFieldset()



}
}