wip
This commit is contained in:
parent
5d76c0f45a
commit
e5f9a6212a
203
README.md
203
README.md
|
|
@ -71,7 +71,7 @@ try {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Tobyz\JsonApiServer\JsonApi` is a [PSR-15 Request Handler](https://www.php-fig.org/psr/psr-15/). Instantiate it with your API's base URL. Convert your framework's request object into a [PSR-7 Request](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) implementation, then let the `JsonApi` handler take it from there. Catch any exceptions and give them back to `JsonApi` if you want a JSON:API error response.
|
`Tobyz\JsonApiServer\JsonApi` is a [PSR-15 Request Handler](https://www.php-fig.org/psr/psr-15/). Instantiate it with your API's base URL. Convert your framework's request object into a [PSR-7 Request](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) implementation, then let the `JsonApi` handler take it from there. Catch any exceptions and give them back to `JsonApi` to generate a JSON:API error response.
|
||||||
|
|
||||||
### Defining Resources
|
### Defining Resources
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ Define your API's resources using the `resource` method. The first argument is t
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Schema\Type;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
|
||||||
$api->resource('comments', $adapter, function (Schema $schema) {
|
$api->resource('comments', $adapter, function (Type $type) {
|
||||||
// define your schema
|
// define your schema
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -98,13 +98,14 @@ $adapter = new EloquentAdapter(User::class);
|
||||||
Define an [attribute field](https://jsonapi.org/format/#document-resource-object-attributes) on your resource using the `attribute` method:
|
Define an [attribute field](https://jsonapi.org/format/#document-resource-object-attributes) on your resource using the `attribute` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName');
|
$type->attribute('firstName');
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the attribute will correspond to the property on your model with the same name. (`EloquentAdapter` will `snake_case` it automatically for you.) If you'd like it to correspond to a different property, use the `property` method:
|
By default the attribute will correspond to the property on your model with the same name. (`EloquentAdapter` will `snake_case` it automatically for you.) If you'd like it to correspond to a different property, use the `property` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')->property('fname');
|
$type->attribute('firstName')
|
||||||
|
->property('fname');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Relationships
|
### Relationships
|
||||||
|
|
@ -112,14 +113,15 @@ $schema->attribute('firstName')->property('fname');
|
||||||
Define [relationship fields](https://jsonapi.org/format/#document-resource-object-relationships) on your resource using the `hasOne` and `hasMany` methods:
|
Define [relationship fields](https://jsonapi.org/format/#document-resource-object-relationships) on your resource using the `hasOne` and `hasMany` methods:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('user');
|
$type->hasOne('user');
|
||||||
$schema->hasMany('comments');
|
$type->hasMany('comments');
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the [resource type](https://jsonapi.org/format/#document-resource-object-identification) that the relationship corresponds to will be derived from the relationship name. In the example above, the `user` relationship would correspond to the `users` resource type, while `comments` would correspond to `comments`. If you'd like to use a different resource type, call the `type` method:
|
By default the [resource type](https://jsonapi.org/format/#document-resource-object-identification) that the relationship corresponds to will be derived from the relationship name. In the example above, the `user` relationship would correspond to the `users` resource type, while `comments` would correspond to `comments`. If you'd like to use a different resource type, call the `type` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('author')->type('people');
|
$type->hasOne('author')
|
||||||
|
->type('people');
|
||||||
```
|
```
|
||||||
|
|
||||||
Like attributes, the relationship will automatically read and write to the relation on your model with the same name. If you'd like it to correspond to a different relation, use the `property` method.
|
Like attributes, the relationship will automatically read and write to the relation on your model with the same name. If you'd like it to correspond to a different relation, use the `property` method.
|
||||||
|
|
@ -129,7 +131,7 @@ Like attributes, the relationship will automatically read and write to the relat
|
||||||
Relationships include [`self`](https://jsonapi.org/format/#fetching-relationships) and [`related`](https://jsonapi.org/format/#document-resource-object-related-resource-links) links automatically. For some relationships it may not make sense to have them accessible via their own URL; you may disable these links by calling the `noLinks` method:
|
Relationships include [`self`](https://jsonapi.org/format/#fetching-relationships) and [`related`](https://jsonapi.org/format/#document-resource-object-related-resource-links) links automatically. For some relationships it may not make sense to have them accessible via their own URL; you may disable these links by calling the `noLinks` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('mostRelevantPost')
|
$type->hasOne('mostRelevantPost')
|
||||||
->noLinks();
|
->noLinks();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -137,10 +139,10 @@ $schema->hasOne('mostRelevantPost')
|
||||||
|
|
||||||
#### Relationship Linkage
|
#### Relationship Linkage
|
||||||
|
|
||||||
By default relationships include no [resource linkage](https://jsonapi.org/format/#document-resource-object-linkage). You can toggle this (without forcing the related resources to be included) by calling the `linkage` or `noLinkage` methods.
|
By default relationships include no [resource linkage](https://jsonapi.org/format/#document-resource-object-linkage). You can toggle this by calling the `linkage` or `noLinkage` methods.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('user')
|
$type->hasOne('user')
|
||||||
->linkage();
|
->linkage();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -151,22 +153,22 @@ $schema->hasOne('user')
|
||||||
To make a relationship available for [inclusion](https://jsonapi.org/format/#fetching-includes) via the `include` query parameter, call the `includable` method.
|
To make a relationship available for [inclusion](https://jsonapi.org/format/#fetching-includes) via the `include` query parameter, call the `includable` method.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('user')
|
$type->hasOne('user')
|
||||||
->includable();
|
->includable();
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Warning:** Be careful when making to-many relationships includable as pagination is not supported.
|
> **Warning:** Be careful when making to-many relationships includable as pagination is not supported.
|
||||||
|
|
||||||
Relationships included via the `include` query parameter are automatically eager-loaded. However, you may wish to define your own eager-loading logic, or prevent a relationship from being eager-loaded. You can do so using the `loadable` and `notLoadable` methods:
|
Relationships included via the `include` query parameter are automatically [eager-loaded](https://laravel.com/docs/5.8/eloquent-relationships#eager-loading) by the adapter. However, you may wish to define your own eager-loading logic, or prevent a relationship from being eager-loaded. You can do so using the `loadable` and `notLoadable` methods:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('user')
|
$type->hasOne('user')
|
||||||
->includable()
|
->includable()
|
||||||
->loadable(function ($models, $request) {
|
->loadable(function ($models, ServerRequestInterface $request) {
|
||||||
collect($models)->load(['user' => function () { /* constraints */ }]);
|
collect($models)->load(['user' => function () { /* constraints */ }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
$schema->hasOne('user')
|
$type->hasOne('user')
|
||||||
->includable()
|
->includable()
|
||||||
->notLoadable();
|
->notLoadable();
|
||||||
```
|
```
|
||||||
|
|
@ -176,19 +178,22 @@ $schema->hasOne('user')
|
||||||
Define a relationship as polymorphic using the `polymorphic` method:
|
Define a relationship as polymorphic using the `polymorphic` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasOne('commentable')->polymorphic();
|
$type->hasOne('commentable')
|
||||||
$schema->hasMany('taggable')->polymorphic();
|
->polymorphic();
|
||||||
|
|
||||||
|
$type->hasMany('taggable')
|
||||||
|
->polymorphic();
|
||||||
```
|
```
|
||||||
|
|
||||||
This will mean that the resource type associated with the relationship will be derived from the model of each related resource. Consequently, nested includes cannot be requested on these relationships.
|
This will mean that the resource type associated with the relationship will be derived from the model of each related resource. Consequently, nested includes cannot be requested on these relationships.
|
||||||
|
|
||||||
### Getters
|
### Getters
|
||||||
|
|
||||||
Use the `get` method to define custom retrieval logic for your field, instead of just reading the value straight from the model property. (If you're using Eloquent, you could also define [casts](https://laravel.com/docs/5.8/eloquent-mutators#attribute-casting) or [accessors](https://laravel.com/docs/5.8/eloquent-mutators#defining-an-accessor) on your model to achieve a similar thing.)
|
Use the `get` method to define custom retrieval logic for your field, instead of just reading the value straight from the model property. (If you're using Eloquent, you could also define attribute [casts](https://laravel.com/docs/5.8/eloquent-mutators#attribute-casting) or [accessors](https://laravel.com/docs/5.8/eloquent-mutators#defining-an-accessor) on your model to achieve a similar thing.)
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')
|
$type->attribute('firstName')
|
||||||
->get(function ($model, $request) {
|
->get(function ($model, ServerRequestInterface $request) {
|
||||||
return ucfirst($model->first_name);
|
return ucfirst($model->first_name);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -200,24 +205,30 @@ $schema->attribute('firstName')
|
||||||
You can restrict the visibility of the whole resource using the `scope` method. This will allow you to modify the query builder object provided by your adapter:
|
You can restrict the visibility of the whole resource using the `scope` method. This will allow you to modify the query builder object provided by your adapter:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->scope(function ($query, $request, $id = null) {
|
$type->scope(function ($query, ServerRequestInterface $request, string $id = null) {
|
||||||
$query->where('user_id', $request->getAttribute('userId'));
|
$query->where('user_id', $request->getAttribute('userId'));
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The third argument to this callback (`$id`) is only populated if the request is to access a single resource. If the request is to a resource index, it will be `null`.
|
The third argument to this callback (`$id`) is only populated if the request is to access a single resource. If the request is to a resource listing, it will be `null`.
|
||||||
|
|
||||||
|
If you want to prevent listing the resource altogether (ie. return `403 Forbidden` from `GET /articles`), you can use the `notListable` method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$type->notListable();
|
||||||
|
```
|
||||||
|
|
||||||
#### Field Visibility
|
#### Field Visibility
|
||||||
|
|
||||||
You can specify logic to restrict the visibility of a field using the `visible` and `hidden` methods:
|
You can specify logic to restrict the visibility of a field using the `visible` and `hidden` methods:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('email')
|
$type->attribute('email')
|
||||||
// Make a field always visible (default)
|
// Make a field always visible (default)
|
||||||
->visible()
|
->visible()
|
||||||
|
|
||||||
// Make a field visible only if certain logic is met
|
// Make a field visible only if certain logic is met
|
||||||
->visible(function ($model, $request) {
|
->visible(function ($model, ServerRequestInterface $request) {
|
||||||
return $model->id == $request->getAttribute('userId');
|
return $model->id == $request->getAttribute('userId');
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -225,22 +236,31 @@ $schema->attribute('email')
|
||||||
->hidden()
|
->hidden()
|
||||||
|
|
||||||
// Hide a field only if certain logic is met
|
// Hide a field only if certain logic is met
|
||||||
->hidden(function ($model, $request) {
|
->hidden(function ($model, ServerRequestInterface $request) {
|
||||||
return $request->getAttribute('userIsSuspended');
|
return $request->getAttribute('userIsSuspended');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Expensive Fields
|
||||||
|
|
||||||
|
If a field is particularly expensive to calculate (for example, if you define a custom getter which runs a query), you can opt to only show the field when a single resource has been requested (ie. the field will not be included on resource listings). Use the `single` method to do this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$type->attribute('expensive')
|
||||||
|
->single();
|
||||||
|
```
|
||||||
|
|
||||||
### Writability
|
### Writability
|
||||||
|
|
||||||
By default, fields are read-only. You can allow a field to be written to via `PATCH` and `POST` requests using the `writable` and `readonly` methods:
|
By default, fields are read-only. You can allow a field to be written to via `PATCH` and `POST` requests using the `writable` and `readonly` methods:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('email')
|
$type->attribute('email')
|
||||||
// Make an attribute writable
|
// Make an attribute writable
|
||||||
->writable()
|
->writable()
|
||||||
|
|
||||||
// Make an attribute writable only if certain logic is met
|
// Make an attribute writable only if certain logic is met
|
||||||
->writable(function ($model, $request) {
|
->writable(function ($model, ServerRequestInterface $request) {
|
||||||
return $model->id == $request->getAttribute('userId');
|
return $model->id == $request->getAttribute('userId');
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -248,7 +268,7 @@ $schema->attribute('email')
|
||||||
->readonly()
|
->readonly()
|
||||||
|
|
||||||
// Make an attribute writable *unless* certain logic is met
|
// Make an attribute writable *unless* certain logic is met
|
||||||
->readonly(function ($model, $request) {
|
->readonly(function ($model, ServerRequestInterface $request) {
|
||||||
return $request->getAttribute('userIsSuspended');
|
return $request->getAttribute('userIsSuspended');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -258,24 +278,24 @@ $schema->attribute('email')
|
||||||
You can provide a default value for a field to be used when creating a new resource if there is no value provided by the consumer. Pass a value or a closure to the `default` method:
|
You can provide a default value for a field to be used when creating a new resource if there is no value provided by the consumer. Pass a value or a closure to the `default` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('joinedAt')
|
$type->attribute('joinedAt')
|
||||||
->default(new DateTime);
|
->default(new DateTime);
|
||||||
|
|
||||||
$schema->attribute('ipAddress')
|
$type->attribute('ipAddress')
|
||||||
->default(function ($request) {
|
->default(function (ServerRequestInterface $request) {
|
||||||
return $request->getServerParams()['REMOTE_ADDR'] ?? null;
|
return $request->getServerParams()['REMOTE_ADDR'] ?? null;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're using Eloquent, you could also define [default attribute values](https://laravel.com/docs/5.8/eloquent#default-attribute-values) to achieve a similar thing, although you wouldn't have access to the request object.
|
If you're using Eloquent, you could also define [default attribute values](https://laravel.com/docs/5.8/eloquent#default-attribute-values) to achieve a similar thing (although you wouldn't have access to the request object).
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
|
|
||||||
You can ensure that data provided for a field is valid before it is saved. Provide a closure to the `validate` method, and call the first argument if validation fails:
|
You can ensure that data provided for a field is valid before it is saved. Provide a closure to the `validate` method, and call the first argument if validation fails:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('email')
|
$type->attribute('email')
|
||||||
->validate(function ($fail, $email, $model, $request, $field) {
|
->validate(function (callable $fail, $email, $model, ServerRequestInterface $request) {
|
||||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
$fail('Invalid email');
|
$fail('Invalid email');
|
||||||
}
|
}
|
||||||
|
|
@ -285,8 +305,8 @@ $schema->attribute('email')
|
||||||
This works for relationships too – the related models will be retrieved via your adapter and passed into your validation function.
|
This works for relationships too – the related models will be retrieved via your adapter and passed into your validation function.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->hasMany('groups')
|
$type->hasMany('groups')
|
||||||
->validate(function ($fail, $groups, $model, $request, $field) {
|
->validate(function (callable $fail, array $groups, $model, ServerRequestInterface $request) {
|
||||||
foreach ($groups as $group) {
|
foreach ($groups as $group) {
|
||||||
if ($group->id === 1) {
|
if ($group->id === 1) {
|
||||||
$fail('You cannot assign this group');
|
$fail('You cannot assign this group');
|
||||||
|
|
@ -300,17 +320,17 @@ You can easily use Laravel's [Validation](https://laravel.com/docs/5.8/validatio
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Laravel\rules;
|
use Tobyz\JsonApiServer\Laravel\rules;
|
||||||
|
|
||||||
$schema->attribute('username')
|
$type->attribute('username')
|
||||||
->validate(rules('required', 'min:3', 'max:30'));
|
->validate(rules('required', 'min:3', 'max:30'));
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setters & Savers
|
### Setters & Savers
|
||||||
|
|
||||||
Use the `set` method to define custom mutation logic for your field, instead of just setting the value straight on the model property. (Of course, if you're using Eloquent, you could also define [casts](https://laravel.com/docs/5.8/eloquent-mutators#attribute-casting) or [mutators](https://laravel.com/docs/5.8/eloquent-mutators#defining-a-mutator) on your model to achieve a similar thing.)
|
Use the `set` method to define custom mutation logic for your field, instead of just setting the value straight on the model property. (If you're using Eloquent, you could also define attribute [casts](https://laravel.com/docs/5.8/eloquent-mutators#attribute-casting) or [mutators](https://laravel.com/docs/5.8/eloquent-mutators#defining-a-mutator) on your model to achieve a similar thing.)
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')
|
$type->attribute('firstName')
|
||||||
->set(function ($model, $value, $request) {
|
->set(function ($model, $value, ServerRequestInterface $request) {
|
||||||
$model->first_name = strtolower($value);
|
$model->first_name = strtolower($value);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -318,11 +338,11 @@ $schema->attribute('firstName')
|
||||||
If your field corresponds to some other form of data storage rather than a simple property on your model, you can use the `save` method to provide a closure to be run _after_ your model is saved:
|
If your field corresponds to some other form of data storage rather than a simple property on your model, you can use the `save` method to provide a closure to be run _after_ your model is saved:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('locale')
|
$type->attribute('locale')
|
||||||
->save(function ($model, $value, $request) {
|
->save(function ($model, $value, ServerRequestInterface $request) {
|
||||||
$model->preferences()
|
$model->preferences()
|
||||||
->update(['value' => $value])
|
->where('key', 'locale')
|
||||||
->where('key', 'locale');
|
->update(['value' => $value]);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -331,13 +351,13 @@ $schema->attribute('locale')
|
||||||
You can define a field as `filterable` to allow the resource index to be [filtered](https://jsonapi.org/recommendations/#filtering) by the field's value. This works for both attributes and relationships:
|
You can define a field as `filterable` to allow the resource index to be [filtered](https://jsonapi.org/recommendations/#filtering) by the field's value. This works for both attributes and relationships:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')
|
$type->attribute('firstName')
|
||||||
->filterable();
|
->filterable();
|
||||||
|
|
||||||
$schema->hasMany('groups')
|
$type->hasMany('groups')
|
||||||
->filterable();
|
->filterable();
|
||||||
|
|
||||||
// e.g. GET /api/users?filter[firstName]=Toby&filter[groups]=1,2,3
|
// eg. GET /api/users?filter[firstName]=Toby&filter[groups]=1,2,3
|
||||||
```
|
```
|
||||||
|
|
||||||
The `EloquentAdapter` automatically parses and applies `>`, `>=`, `<`, `<=`, and `..` operators on attribute filter values, so you can do:
|
The `EloquentAdapter` automatically parses and applies `>`, `>=`, `<`, `<=`, and `..` operators on attribute filter values, so you can do:
|
||||||
|
|
@ -347,20 +367,10 @@ GET /api/users?filter[postCount]=>=10
|
||||||
GET /api/users?filter[postCount]=5..15
|
GET /api/users?filter[postCount]=5..15
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also pass a closure to customize how the filter is applied to the query builder object:
|
To define filters with custom logic, or ones that do not correspond to an attribute, use the `filter` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('name')
|
$type->filter('minPosts', function ($query, $value, ServerRequestInterface $request) {
|
||||||
->filterable(function ($query, $value, $request) {
|
|
||||||
$query->where('first_name', $value)
|
|
||||||
->orWhere('last_name', $value);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
To define filters that do not correspond to an attribute, use the `filter` method:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$schema->filter('minPosts', function ($query, $value, $request) {
|
|
||||||
$query->where('postCount', '>=', $value);
|
$query->where('postCount', '>=', $value);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -370,93 +380,82 @@ $schema->filter('minPosts', function ($query, $value, $request) {
|
||||||
You can define an attribute as `sortable` to allow the resource index to be [sorted](https://jsonapi.org/format/#fetching-sorting) by the attribute's value:
|
You can define an attribute as `sortable` to allow the resource index to be [sorted](https://jsonapi.org/format/#fetching-sorting) by the attribute's value:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')
|
$type->attribute('firstName')
|
||||||
->sortable();
|
->sortable();
|
||||||
|
|
||||||
$schema->attribute('lastName')
|
$type->attribute('lastName')
|
||||||
->sortable();
|
->sortable();
|
||||||
|
|
||||||
// e.g. GET /api/users?sort=lastName,firstName
|
// e.g. GET /api/users?sort=lastName,firstName
|
||||||
```
|
```
|
||||||
|
|
||||||
You can pass a closure to customize how the sort is applied to the query builder object:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$schema->attribute('name')
|
|
||||||
->sortable(function ($query, $direction, $request) {
|
|
||||||
$query->orderBy('last_name', $direction)
|
|
||||||
->orderBy('first_name', $direction);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
You can set a default sort string to be used when the consumer has not supplied one using the `defaultSort` method on the schema builder:
|
You can set a default sort string to be used when the consumer has not supplied one using the `defaultSort` method on the schema builder:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->defaultSort('-updatedAt,-createdAt');
|
$type->defaultSort('-updatedAt,-createdAt');
|
||||||
```
|
```
|
||||||
|
|
||||||
To define sortable criteria that does not correspond to an attribute, use the `sort` method:
|
To define sort fields with custom logic, or ones that do not correspond to an attribute, use the `sort` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->sort('relevance', function ($query, $direction, $request) {
|
$type->sort('relevance', function ($query, $direction, ServerRequestInterface $request) {
|
||||||
$query->orderBy('relevance', $direction);
|
$query->orderBy('relevance', $direction);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pagination
|
### Pagination
|
||||||
|
|
||||||
By default, resource listings are automatically [paginated](https://jsonapi.org/format/#fetching-pagination) with 20 records per page. You can change this limit using the `paginate` method on the schema builder, or you can remove it by passing `null`:
|
By default, resource listings are automatically [paginated](https://jsonapi.org/format/#fetching-pagination) with 20 records per page. You can change this amount using the `paginate` method on the schema builder, or you can remove it by calling the `dontPaginate` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->paginate(50); // default to listing 50 resources per page
|
$type->paginate(50); // default to listing 50 resources per page
|
||||||
$schema->paginate(null); // default to listing all resources
|
$type->dontPaginate(); // default to listing all resources
|
||||||
```
|
```
|
||||||
|
|
||||||
Consumers may request a different limit using the `page[limit]` query parameter. By default the maximum possible limit is capped at 50; you can change this cap using the `limit` method, or you can remove it by passing `null`:
|
Consumers may request a different limit using the `page[limit]` query parameter. By default the maximum possible limit is capped at 50; you can change this cap using the `limit` method, or you can remove it by calling the `noLimit` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->limit(100); // set the maximum limit for resources per page to 100
|
$type->limit(100); // set the maximum limit for resources per page to 100
|
||||||
$schema->limit(null); // remove the maximum limit for resources per page
|
$type->noLimit(); // remove the maximum limit for resources per page
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Countability
|
#### Countability
|
||||||
|
|
||||||
By default a query will be performed to count the total number of resources in a collection. This will be used to populate a `count` attribute in the document's `meta` object, as well as the `last` pagination link. For some types of resources, or when a query is resource-intensive (especially when certain filters or sorting is applied), it may be undesirable to have this happen. So it can be toggled using the `countable` and `uncountable` methods:
|
By default a query will be performed to count the total number of resources in a collection. This will be used to populate a `total` attribute in the document's `meta` object, as well as the `last` pagination link. For some types of resources, or when a query is resource-intensive (especially when certain filters or sorting is applied), it may be undesirable to have this happen. So it can be toggled using the `countable` and `uncountable` methods:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->countable();
|
$type->countable();
|
||||||
$schema->uncountable();
|
$type->uncountable();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Meta Information
|
### Meta Information
|
||||||
|
|
||||||
You can add meta information to the document or any relationship field using the `meta` method. Pass a value or a closure:
|
You can add meta information to any resource or relationship field using the `meta` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->meta('author', 'Toby Zerner');
|
$type->meta('requestTime', function (ServerRequestInterface $request) {
|
||||||
$schema->meta('requestTime', function ($request) {
|
|
||||||
return new DateTime;
|
return new DateTime;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Creating Resources
|
### Creating Resources
|
||||||
|
|
||||||
By default, resources are not [creatable](https://jsonapi.org/format/#crud-creating) (i.e. `POST` requests will return `403 Forbidden`). You can allow them to be created using the `creatable` and `notCreatable` methods on the schema builder. Pass a closure that returns `true` if the resource should be creatable, or no value to have it always creatable.
|
By default, resources are not [creatable](https://jsonapi.org/format/#crud-creating) (ie. `POST` requests will return `403 Forbidden`). You can allow them to be created using the `creatable` and `notCreatable` methods on the schema builder. Pass a closure that returns `true` if the resource should be creatable, or no value to have it always creatable.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->creatable();
|
$type->creatable();
|
||||||
|
|
||||||
$schema->creatable(function ($request) {
|
$type->creatable(function (ServerRequestInterface $request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Customizing the Model
|
#### Customizing the Model
|
||||||
|
|
||||||
When creating a resource, an empty model is supplied by the adapter. You may wish to override this and provide a custom model in special circumstances. You can do so using the `create` method:
|
When creating a resource, an empty model is supplied by the adapter. You may wish to override this and provide a custom model in special circumstances. You can do so using the `createModel` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->create(function ($request) {
|
$type->createModel(function (ServerRequestInterface $request) {
|
||||||
return new CustomModel;
|
return new CustomModel;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -466,9 +465,9 @@ $schema->create(function ($request) {
|
||||||
By default, resources are not [updatable](https://jsonapi.org/format/#crud-updating) (i.e. `PATCH` requests will return `403 Forbidden`). You can allow them to be updated using the `updatable` and `notUpdatable` methods on the schema builder:
|
By default, resources are not [updatable](https://jsonapi.org/format/#crud-updating) (i.e. `PATCH` requests will return `403 Forbidden`). You can allow them to be updated using the `updatable` and `notUpdatable` methods on the schema builder:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->updatable();
|
$type->updatable();
|
||||||
|
|
||||||
$schema->updatable(function ($request) {
|
$type->updatable(function (ServerRequestInterface $request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -478,21 +477,21 @@ $schema->updatable(function ($request) {
|
||||||
By default, resources are not [deletable](https://jsonapi.org/format/#crud-deleting) (i.e. `DELETE` requests will return `403 Forbidden`). You can allow them to be deleted using the `deletable` and `notDeletable` methods on the schema builder:
|
By default, resources are not [deletable](https://jsonapi.org/format/#crud-deleting) (i.e. `DELETE` requests will return `403 Forbidden`). You can allow them to be deleted using the `deletable` and `notDeletable` methods on the schema builder:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->deletable();
|
$type->deletable();
|
||||||
|
|
||||||
$schema->deletable(function ($request) {
|
$type->deletable(function (ServerRequestInterface $request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
The server will fire several events, allowing you to hook into the following points in a resource's lifecycle: `creating`, `created`, `updating`, `updated`, `saving`, `saved`, `deleting`, `deleted`. (Of course, if you're using Eloquent, you could also use [model events](https://laravel.com/docs/5.8/eloquent#events) to achieve a similar thing, although you wouldn't have access to the request object.)
|
The server will fire several events, allowing you to hook into the following points in a resource's lifecycle: `listing`, `listed`, `showing`, `shown`, `creating`, `created`, `updating`, `updated`, `deleting`, `deleted`. (If you're using Eloquent, you could also use [model events](https://laravel.com/docs/5.8/eloquent#events) to achieve a similar thing, although you wouldn't have access to the request object.)
|
||||||
|
|
||||||
To listen for an event, simply call the matching method name on the schema builder and pass a closure to be executed, which will receive the model and the request:
|
To listen for an event, simply call the matching method name on the schema and pass a closure to be executed, which will receive the model and the request:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->creating(function ($model, $request) {
|
$type->onCreating(function ($model, ServerRequestInterface $request) {
|
||||||
// do something before a new model is saved
|
// do something before a new model is saved
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -509,13 +508,7 @@ $api->authenticated();
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to send pull requests or create issues if you come across problems or have great ideas. See the [Contributing Guide](https://github.com/tobyz/json-api-server/blob/master/CONTRIBUTING.md) for more information.
|
Feel free to send pull requests or create issues if you come across problems or have great ideas.
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ vendor/bin/phpunit
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tobyz/json-api-server",
|
"name": "tobyz/json-api-server",
|
||||||
|
"description": "A fully automated framework-agnostic JSON:API server implementation in PHP.",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.2",
|
"php": "^7.2",
|
||||||
"doctrine/inflector": "^1.3",
|
"doctrine/inflector": "^1.3",
|
||||||
|
|
@ -19,7 +20,10 @@
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Tobyz\\JsonApiServer\\": "src/"
|
"Tobyz\\JsonApiServer\\": "src/"
|
||||||
},
|
},
|
||||||
"files": ["src/functions.php"]
|
"files": [
|
||||||
|
"src/functions.php",
|
||||||
|
"src/functions_laravel.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
@ -31,6 +35,9 @@
|
||||||
"helmich/phpunit-json-assert": "^3.0",
|
"helmich/phpunit-json-assert": "^3.0",
|
||||||
"phpunit/phpunit": "^8.0"
|
"phpunit/phpunit": "^8.0"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit"
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,12 +208,12 @@ trait SavesData
|
||||||
|
|
||||||
$value = get_value($data, $field);
|
$value = get_value($data, $field);
|
||||||
|
|
||||||
if ($setter = $field->getSetter()) {
|
if ($setCallback = $field->getSetCallback()) {
|
||||||
$setter($model, $value, $request);
|
$setCallback($model, $value, $request);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field->getSaver()) {
|
if ($field->getSaveCallback()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@ class Create implements RequestHandlerInterface
|
||||||
private function fillDefaultValues(array &$data, Request $request)
|
private function fillDefaultValues(array &$data, Request $request)
|
||||||
{
|
{
|
||||||
foreach ($this->resource->getSchema()->getFields() as $field) {
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
if (! has_value($data, $field) && ($default = $field->getDefault())) {
|
if (! has_value($data, $field) && ($defaultCallback = $field->getDefaultCallback())) {
|
||||||
set_value($data, $field, $default($request));
|
set_value($data, $field, $defaultCallback($request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class Show implements RequestHandlerInterface
|
||||||
run_callbacks($this->resource->getSchema()->getListeners('show'), [$this->model, $request]);
|
run_callbacks($this->resource->getSchema()->getListeners('show'), [$this->model, $request]);
|
||||||
|
|
||||||
$serializer = new Serializer($this->api, $request);
|
$serializer = new Serializer($this->api, $request);
|
||||||
$serializer->addSingle($this->resource, $this->model, $include);
|
$serializer->add($this->resource, $this->model, $include, true);
|
||||||
|
|
||||||
return new JsonApiResponse(
|
return new JsonApiResponse(
|
||||||
new CompoundDocument(
|
new CompoundDocument(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\Exception\InternalServerErrorException;
|
use Tobyz\JsonApiServer\Exception\InternalServerErrorException;
|
||||||
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
||||||
use Tobyz\JsonApiServer\Exception\NotAcceptableException;
|
use Tobyz\JsonApiServer\Exception\NotAcceptableException;
|
||||||
use Tobyz\JsonApiServer\Exception\NotImplementedException;
|
use Tobyz\JsonApiServer\Exception\NotImplementedException;
|
||||||
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\UnauthorizedException;
|
||||||
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
|
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
|
||||||
use Tobyz\JsonApiServer\Handler\Concerns\FindsResources;
|
use Tobyz\JsonApiServer\Handler\Concerns\FindsResources;
|
||||||
use Tobyz\JsonApiServer\Http\MediaTypes;
|
use Tobyz\JsonApiServer\Http\MediaTypes;
|
||||||
|
|
@ -34,6 +36,7 @@ final class JsonApi implements RequestHandlerInterface
|
||||||
|
|
||||||
private $resources = [];
|
private $resources = [];
|
||||||
private $baseUrl;
|
private $baseUrl;
|
||||||
|
private $authenticated = false;
|
||||||
|
|
||||||
public function __construct(string $baseUrl)
|
public function __construct(string $baseUrl)
|
||||||
{
|
{
|
||||||
|
|
@ -50,6 +53,8 @@ final class JsonApi implements RequestHandlerInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get defined resource types.
|
* Get defined resource types.
|
||||||
|
*
|
||||||
|
* @return ResourceType[]
|
||||||
*/
|
*/
|
||||||
public function getResources(): array
|
public function getResources(): array
|
||||||
{
|
{
|
||||||
|
|
@ -209,6 +214,10 @@ final class JsonApi implements RequestHandlerInterface
|
||||||
$e = new InternalServerErrorException;
|
$e = new InternalServerErrorException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $this->authenticated && $e instanceof ForbiddenException) {
|
||||||
|
$e = new UnauthorizedException;
|
||||||
|
}
|
||||||
|
|
||||||
$errors = $e->getJsonApiErrors();
|
$errors = $e->getJsonApiErrors();
|
||||||
$status = $e->getJsonApiStatus();
|
$status = $e->getJsonApiStatus();
|
||||||
|
|
||||||
|
|
@ -226,4 +235,12 @@ final class JsonApi implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
return $this->baseUrl;
|
return $this->baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the consumer is authenticated.
|
||||||
|
*/
|
||||||
|
public function authenticated(): void
|
||||||
|
{
|
||||||
|
$this->authenticated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Forust.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Schema\Concerns;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\Schema\Meta;
|
||||||
|
|
||||||
|
trait HasMeta
|
||||||
|
{
|
||||||
|
private $meta = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a meta attribute.
|
||||||
|
*/
|
||||||
|
public function meta(string $name, callable $value): Meta
|
||||||
|
{
|
||||||
|
return $this->meta[$name] = new Meta($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a meta attribute.
|
||||||
|
*/
|
||||||
|
public function removeMeta(string $name): void
|
||||||
|
{
|
||||||
|
unset($this->meta[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the meta attributes.
|
||||||
|
*
|
||||||
|
* @return Meta[]
|
||||||
|
*/
|
||||||
|
public function getMeta(): array
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,12 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||||
|
|
||||||
abstract class Relationship extends Field
|
abstract class Relationship extends Field
|
||||||
{
|
{
|
||||||
|
use HasMeta;
|
||||||
|
|
||||||
private $type;
|
private $type;
|
||||||
private $linkage = false;
|
private $linkage = false;
|
||||||
private $links = true;
|
private $links = true;
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
||||||
use function Tobyz\JsonApiServer\negate;
|
use function Tobyz\JsonApiServer\negate;
|
||||||
|
|
||||||
final class Type
|
final class Type
|
||||||
{
|
{
|
||||||
use HasListeners;
|
use HasListeners, HasMeta;
|
||||||
|
|
||||||
private $fields = [];
|
private $fields = [];
|
||||||
private $meta = [];
|
|
||||||
private $filters = [];
|
private $filters = [];
|
||||||
private $sortFields = [];
|
private $sortFields = [];
|
||||||
private $perPage = 20;
|
private $perPage = 20;
|
||||||
|
|
@ -95,32 +95,6 @@ final class Type
|
||||||
return $this->fields;
|
return $this->fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a meta attribute to the resource type.
|
|
||||||
*/
|
|
||||||
public function meta(string $name, callable $value): Meta
|
|
||||||
{
|
|
||||||
return $this->meta[$name] = new Meta($name, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a meta attribute from the resource type.
|
|
||||||
*/
|
|
||||||
public function removeMeta(string $name): void
|
|
||||||
{
|
|
||||||
unset($this->meta[$name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the resource type's meta attributes.
|
|
||||||
*
|
|
||||||
* @return Meta[]
|
|
||||||
*/
|
|
||||||
public function getMeta(): array
|
|
||||||
{
|
|
||||||
return $this->meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a filter to the resource type.
|
* Add a filter to the resource type.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,15 @@ namespace Tobyz\JsonApiServer;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use JsonApiPhp\JsonApi as Structure;
|
use JsonApiPhp\JsonApi as Structure;
|
||||||
use JsonApiPhp\JsonApi\EmptyRelationship;
|
|
||||||
use JsonApiPhp\JsonApi\Link\RelatedLink;
|
|
||||||
use JsonApiPhp\JsonApi\Link\SelfLink;
|
|
||||||
use JsonApiPhp\JsonApi\ResourceIdentifier;
|
|
||||||
use JsonApiPhp\JsonApi\ResourceIdentifierCollection;
|
|
||||||
use JsonApiPhp\JsonApi\ToMany;
|
|
||||||
use JsonApiPhp\JsonApi\ToOne;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
use RuntimeException;
|
||||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
|
||||||
use Tobyz\JsonApiServer\Schema\Relationship;
|
|
||||||
|
|
||||||
final class Serializer
|
final class Serializer
|
||||||
{
|
{
|
||||||
protected $api;
|
private $api;
|
||||||
protected $request;
|
private $request;
|
||||||
protected $map = [];
|
private $map = [];
|
||||||
protected $primary = [];
|
private $primary = [];
|
||||||
|
|
||||||
public function __construct(JsonApi $api, Request $request)
|
public function __construct(JsonApi $api, Request $request)
|
||||||
{
|
{
|
||||||
|
|
@ -39,45 +30,60 @@ final class Serializer
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a primary resource to the document.
|
||||||
|
*/
|
||||||
public function add(ResourceType $resource, $model, array $include, bool $single = false): void
|
public function add(ResourceType $resource, $model, array $include, bool $single = false): void
|
||||||
{
|
{
|
||||||
$data = $this->addToMap($resource, $model, $include, $single);
|
$data = $this->addToMap($resource, $model, $include, $single);
|
||||||
|
|
||||||
$this->primary[] = $data['type'].':'.$data['id'];
|
$this->primary[] = $this->key($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addSingle(ResourceType $resource, $model, array $include): void
|
/**
|
||||||
|
* Get the serialized primary resources.
|
||||||
|
*/
|
||||||
|
public function primary(): array
|
||||||
{
|
{
|
||||||
$this->add($resource, $model, $include, true);
|
$primary = array_map(function ($key) {
|
||||||
|
return $this->map[$key];
|
||||||
|
}, $this->primary);
|
||||||
|
|
||||||
|
return $this->resourceObjects($primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addToMap(ResourceType $resource, $model, array $include, bool $single = false)
|
/**
|
||||||
|
* Get the serialized included resources.
|
||||||
|
*/
|
||||||
|
public function included(): array
|
||||||
|
{
|
||||||
|
$included = array_values(array_diff_key($this->map, array_flip($this->primary)));
|
||||||
|
|
||||||
|
return $this->resourceObjects($included);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addToMap(ResourceType $resource, $model, array $include, bool $single = false): array
|
||||||
{
|
{
|
||||||
$adapter = $resource->getAdapter();
|
$adapter = $resource->getAdapter();
|
||||||
$schema = $resource->getSchema();
|
$schema = $resource->getSchema();
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'type' => $type = $resource->getType(),
|
'type' => $type = $resource->getType(),
|
||||||
'id' => $adapter->getId($model),
|
'id' => $id = $adapter->getId($model),
|
||||||
'fields' => [],
|
'fields' => [],
|
||||||
'links' => [],
|
'links' => [],
|
||||||
'meta' => []
|
'meta' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
$resourceUrl = $this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id'];
|
$key = $this->key($data);
|
||||||
|
$url = $this->api->getBaseUrl()."/$type/$id";
|
||||||
$fields = $schema->getFields();
|
$fields = $schema->getFields();
|
||||||
|
|
||||||
$queryParams = $this->request->getQueryParams();
|
$queryParams = $this->request->getQueryParams();
|
||||||
|
|
||||||
if (isset($queryParams['fields'][$type])) {
|
if (isset($queryParams['fields'][$type])) {
|
||||||
$fields = array_intersect_key($fields, array_flip(explode(',', $queryParams['fields'][$type])));
|
$fields = array_intersect_key($fields, array_flip(explode(',', $queryParams['fields'][$type])));
|
||||||
}
|
}
|
||||||
|
|
||||||
ksort($fields);
|
|
||||||
|
|
||||||
$key = $data['type'].':'.$data['id'];
|
|
||||||
|
|
||||||
foreach ($fields as $name => $field) {
|
foreach ($fields as $name => $field) {
|
||||||
if (isset($this->map[$key]['fields'][$name])) {
|
if (isset($this->map[$key]['fields'][$name])) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -92,158 +98,39 @@ final class Serializer
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field instanceof Schema\Attribute) {
|
if ($field instanceof Schema\Attribute) {
|
||||||
$value = $this->attribute($field, $model, $adapter);
|
$value = $this->attribute($field, $resource, $model);
|
||||||
} elseif ($field instanceof Schema\Relationship) {
|
} elseif ($field instanceof Schema\Relationship) {
|
||||||
$isIncluded = isset($include[$name]);
|
$isIncluded = isset($include[$name]);
|
||||||
$isLinkage = evaluate($field->isLinkage(), [$this->request]);
|
$relationshipInclude = $isIncluded ? ($relationshipInclude[$name] ?? []) : null;
|
||||||
|
$links = $this->relationshipLinks($field, $url);
|
||||||
|
$meta = $this->meta($field->getMeta(), $model);
|
||||||
|
$members = array_merge($links, $meta);
|
||||||
|
|
||||||
if (! $isIncluded && ! $isLinkage) {
|
if (! $isIncluded && ! $field->isLinkage()) {
|
||||||
$value = $this->emptyRelationship($field, $resourceUrl);
|
$value = $this->emptyRelationship($field, $members);
|
||||||
} elseif ($field instanceof Schema\HasOne) {
|
} elseif ($field instanceof Schema\HasOne) {
|
||||||
$value = $this->toOne($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl, $single);
|
$value = $this->toOne($field, $members, $resource, $model, $relationshipInclude, $single);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
} elseif ($field instanceof Schema\HasMany) {
|
||||||
$value = $this->toMany($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
|
$value = $this->toMany($field, $members, $resource, $model, $relationshipInclude);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($value)) {
|
if (! empty($value)) {
|
||||||
$data['fields'][$name] = $value;
|
$data['fields'][$name] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['links']['self'] = new SelfLink($resourceUrl);
|
$data['links']['self'] = new Structure\Link\SelfLink($url);
|
||||||
|
$data['meta'] = $this->meta($schema->getMeta(), $model);
|
||||||
$metas = $schema->getMeta();
|
|
||||||
|
|
||||||
ksort($metas);
|
|
||||||
|
|
||||||
foreach ($metas as $name => $meta) {
|
|
||||||
$data['meta'][$name] = new Structure\Meta($meta->getName(), ($meta->getValue())($model, $this->request));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->merge($data);
|
$this->merge($data);
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function attribute(Attribute $field, $model, AdapterInterface $adapter): Structure\Attribute
|
|
||||||
{
|
|
||||||
if ($getter = $field->getGetCallback()) {
|
|
||||||
$value = $getter($model, $this->request);
|
|
||||||
} else {
|
|
||||||
$value = $adapter->getAttribute($model, $field);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($value instanceof DateTimeInterface) {
|
|
||||||
$value = $value->format(DateTime::RFC3339);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Structure\Attribute($field->getName(), $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl, bool $single = false)
|
|
||||||
{
|
|
||||||
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
|
||||||
|
|
||||||
$value = $isIncluded ? (($getter = $field->getGetCallback()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field, false)) : ($isLinkage ? $adapter->getHasOne($model, $field, true) : null);
|
|
||||||
|
|
||||||
if (! $value) {
|
|
||||||
return new Structure\ToNull(
|
|
||||||
$field->getName(),
|
|
||||||
...$links
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isIncluded) {
|
|
||||||
$identifier = $this->addRelated($field, $value, $include, $single);
|
|
||||||
} else {
|
|
||||||
$identifier = $this->relatedResourceIdentifier($field, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return new ToOne(
|
|
||||||
$field->getName(),
|
|
||||||
$identifier,
|
|
||||||
...$links
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
|
||||||
{
|
|
||||||
if ($getter = $field->getGetCallback()) {
|
|
||||||
$value = $getter($model, $this->request);
|
|
||||||
} else {
|
|
||||||
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field, false) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$identifiers = [];
|
|
||||||
|
|
||||||
if ($isIncluded) {
|
|
||||||
foreach ($value as $relatedModel) {
|
|
||||||
$identifiers[] = $this->addRelated($field, $relatedModel, $include);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foreach ($value as $relatedModel) {
|
|
||||||
$identifiers[] = $this->relatedResourceIdentifier($field, $relatedModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ToMany(
|
|
||||||
$field->getName(),
|
|
||||||
new ResourceIdentifierCollection(...$identifiers),
|
|
||||||
...$this->getRelationshipLinks($field, $resourceUrl)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function emptyRelationship(Relationship $field, string $resourceUrl): ?EmptyRelationship
|
|
||||||
{
|
|
||||||
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
|
||||||
|
|
||||||
if (! $links) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmptyRelationship(
|
|
||||||
$field->getName(),
|
|
||||||
...$links
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getRelationshipLinks(Relationship $field, string $resourceUrl): array
|
|
||||||
{
|
|
||||||
if (! $field->isLinks()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
new SelfLink($resourceUrl.'/relationships/'.$field->getName()),
|
|
||||||
new RelatedLink($resourceUrl.'/'.$field->getName())
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addRelated(Relationship $field, $model, array $include, bool $single = false): ResourceIdentifier
|
|
||||||
{
|
|
||||||
$relatedResource = is_string($field->getType()) ? $this->api->getResource($field->getType()) : $this->resourceForModel($model);
|
|
||||||
|
|
||||||
return $this->resourceIdentifier(
|
|
||||||
$this->addToMap($relatedResource, $model, $include, $single)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resourceForModel($model)
|
|
||||||
{
|
|
||||||
foreach ($this->api->getResources() as $resource) {
|
|
||||||
if ($resource->getAdapter()->represents($model)) {
|
|
||||||
return $resource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \RuntimeException('No resource defined to handle model of type '.get_class($model));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function merge($data): void
|
private function merge($data): void
|
||||||
{
|
{
|
||||||
$key = $data['type'].':'.$data['id'];
|
$key = $this->key($data);
|
||||||
|
|
||||||
if (isset($this->map[$key])) {
|
if (isset($this->map[$key])) {
|
||||||
$this->map[$key]['fields'] = array_merge($this->map[$key]['fields'], $data['fields']);
|
$this->map[$key]['fields'] = array_merge($this->map[$key]['fields'], $data['fields']);
|
||||||
|
|
@ -254,20 +141,107 @@ final class Serializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function primary(): array
|
private function attribute(Schema\Attribute $field, ResourceType $resource, $model): Structure\Attribute
|
||||||
{
|
{
|
||||||
$primary = array_map(function ($key) {
|
if ($getCallback = $field->getGetCallback()) {
|
||||||
return $this->map[$key];
|
$value = $getCallback($model, $this->request);
|
||||||
}, $this->primary);
|
} else {
|
||||||
|
$value = $resource->getAdapter()->getAttribute($model, $field);
|
||||||
return $this->resourceObjects($primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function included(): array
|
if ($value instanceof DateTimeInterface) {
|
||||||
{
|
$value = $value->format(DateTime::RFC3339);
|
||||||
$included = array_values(array_diff_key($this->map, array_flip($this->primary)));
|
}
|
||||||
|
|
||||||
return $this->resourceObjects($included);
|
return new Structure\Attribute($field->getName(), $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toOne(Schema\HasOne $field, array $members, ResourceType $resource, $model, ?array $include, bool $single)
|
||||||
|
{
|
||||||
|
$included = $include !== null;
|
||||||
|
|
||||||
|
$model = ($included && $getCallback = $field->getGetCallback())
|
||||||
|
? $getCallback($model, $this->request)
|
||||||
|
: $resource->getAdapter()->getHasOne($model, $field, ! $included);
|
||||||
|
|
||||||
|
if (! $model) {
|
||||||
|
return new Structure\ToNull($field->getName(), ...$members);
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $include !== null
|
||||||
|
? $this->addRelated($field, $model, $include, $single)
|
||||||
|
: $this->relatedResourceIdentifier($field, $model);
|
||||||
|
|
||||||
|
return new Structure\ToOne($field->getName(), $identifier, ...$members);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toMany(Schema\HasMany $field, array $members, ResourceType $resource, $model, ?array $include)
|
||||||
|
{
|
||||||
|
$included = $include !== null;
|
||||||
|
|
||||||
|
$models = ($included && $getCallback = $field->getGetCallback())
|
||||||
|
? $getCallback($model, $this->request)
|
||||||
|
: $resource->getAdapter()->getHasMany($model, $field, ! $included);
|
||||||
|
|
||||||
|
$identifiers = [];
|
||||||
|
|
||||||
|
foreach ($models as $relatedModel) {
|
||||||
|
$identifiers[] = $included
|
||||||
|
? $this->addRelated($field, $relatedModel, $include)
|
||||||
|
: $this->relatedResourceIdentifier($field, $relatedModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Structure\ToMany(
|
||||||
|
$field->getName(),
|
||||||
|
new Structure\ResourceIdentifierCollection(...$identifiers),
|
||||||
|
...$members
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emptyRelationship(Schema\Relationship $field, array $members): ?Structure\EmptyRelationship
|
||||||
|
{
|
||||||
|
if (! $members) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Structure\EmptyRelationship($field->getName(), ...$members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Structure\Internal\RelationshipMember
|
||||||
|
*/
|
||||||
|
private function relationshipLinks(Schema\Relationship $field, string $url): array
|
||||||
|
{
|
||||||
|
if (! $field->isLinks()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
new Structure\Link\SelfLink($url.'/relationships/'.$field->getName()),
|
||||||
|
new Structure\Link\RelatedLink($url.'/'.$field->getName())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addRelated(Schema\Relationship $field, $model, array $include, bool $single = false): Structure\ResourceIdentifier
|
||||||
|
{
|
||||||
|
$relatedResource = is_string($field->getType())
|
||||||
|
? $this->api->getResource($field->getType())
|
||||||
|
: $this->resourceForModel($model);
|
||||||
|
|
||||||
|
return $this->resourceIdentifier(
|
||||||
|
$this->addToMap($relatedResource, $model, $include, $single)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resourceForModel($model): ResourceType
|
||||||
|
{
|
||||||
|
foreach ($this->api->getResources() as $resource) {
|
||||||
|
if ($resource->getAdapter()->represents($model)) {
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException('No resource defined to represent model of type '.get_class($model));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resourceObjects(array $items): array
|
private function resourceObjects(array $items): array
|
||||||
|
|
@ -290,21 +264,36 @@ final class Serializer
|
||||||
|
|
||||||
private function resourceIdentifier(array $data): Structure\ResourceIdentifier
|
private function resourceIdentifier(array $data): Structure\ResourceIdentifier
|
||||||
{
|
{
|
||||||
return new Structure\ResourceIdentifier(
|
return new Structure\ResourceIdentifier($data['type'], $data['id']);
|
||||||
$data['type'],
|
|
||||||
$data['id']
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function relatedResourceIdentifier(Schema\Relationship $field, $model)
|
private function relatedResourceIdentifier(Schema\Relationship $field, $model)
|
||||||
{
|
{
|
||||||
$type = $field->getType();
|
$type = $field->getType();
|
||||||
|
$relatedResource = is_string($type)
|
||||||
$relatedResource = is_string($type) ? $this->api->getResource($type) : $this->resourceForModel($model);
|
? $this->api->getResource($type)
|
||||||
|
: $this->resourceForModel($model);
|
||||||
|
|
||||||
return $this->resourceIdentifier([
|
return $this->resourceIdentifier([
|
||||||
'type' => $relatedResource->getType(),
|
'type' => $relatedResource->getType(),
|
||||||
'id' => $relatedResource->getAdapter()->getId($model)
|
'id' => $relatedResource->getAdapter()->getId($model)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Structure\Internal\RelationshipMember
|
||||||
|
*/
|
||||||
|
private function meta(array $items, $model): array
|
||||||
|
{
|
||||||
|
ksort($items);
|
||||||
|
|
||||||
|
return array_map(function (Schema\Meta $meta) use ($model) {
|
||||||
|
return new Structure\Meta($meta->getName(), ($meta->getValue())($model, $this->request));
|
||||||
|
}, $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function key(array $data)
|
||||||
|
{
|
||||||
|
return $data['type'].':'.$data['id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of tobyz/json-api-server.
|
||||||
|
*
|
||||||
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Laravel;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Field;
|
||||||
|
|
||||||
|
function rules($rules, array $messages = [], array $customAttributes = [])
|
||||||
|
{
|
||||||
|
if (is_string($rules)) {
|
||||||
|
$rules = [$rules];
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (callable $fail, $value, $model, Request $request, Field $field) use ($rules, $messages, $customAttributes) {
|
||||||
|
$key = $field->getName();
|
||||||
|
$validationRules = [$key => []];
|
||||||
|
|
||||||
|
foreach ($rules as $k => $v) {
|
||||||
|
if (! is_numeric($k)) {
|
||||||
|
$validationRules[$key.'.'.$k] = $v;
|
||||||
|
} else {
|
||||||
|
$validationRules[$key][] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$validation = Validator::make($value !== null ? [$key => $value] : [], $validationRules, $messages, $customAttributes);
|
||||||
|
|
||||||
|
if ($validation->fails()) {
|
||||||
|
foreach ($validation->errors()->all() as $message) {
|
||||||
|
$fail($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -53,12 +53,12 @@ class MockAdapter implements AdapterInterface
|
||||||
return $model->{$this->getProperty($attribute)} ?? 'default';
|
return $model->{$this->getProperty($attribute)} ?? 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasOne($model, HasOne $relationship, array $fields = null)
|
public function getHasOne($model, HasOne $relationship, bool $linkage)
|
||||||
{
|
{
|
||||||
return $model->{$this->getProperty($relationship)} ?? null;
|
return $model->{$this->getProperty($relationship)} ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasMany($model, HasMany $relationship, array $fields = null): array
|
public function getHasMany($model, HasMany $relationship, bool $linkage): array
|
||||||
{
|
{
|
||||||
return $model->{$this->getProperty($relationship)} ?? [];
|
return $model->{$this->getProperty($relationship)} ?? [];
|
||||||
}
|
}
|
||||||
|
|
@ -122,20 +122,13 @@ class MockAdapter implements AdapterInterface
|
||||||
$query->paginate[] = [$limit, $offset];
|
$query->paginate[] = [$limit, $offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(array $models, array $relationships, Closure $scope): void
|
public function load(array $models, array $relationships, Closure $scope, bool $linkage): void
|
||||||
{
|
{
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
$model->load[] = $relationships;
|
$model->load[] = $relationships;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadIds(array $models, Relationship $relationship): void
|
|
||||||
{
|
|
||||||
foreach ($models as $model) {
|
|
||||||
$model->loadIds[] = $relationship;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getProperty(Field $field)
|
private function getProperty(Field $field)
|
||||||
{
|
{
|
||||||
return $field->getProperty() ?: $field->getName();
|
return $field->getProperty() ?: $field->getName();
|
||||||
|
|
@ -146,26 +139,8 @@ class MockAdapter implements AdapterInterface
|
||||||
return isset($model['type']) && $model['type'] === $this->type;
|
return isset($model['type']) && $model['type'] === $this->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of results from the query.
|
|
||||||
*
|
|
||||||
* @param $query
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function count($query): int
|
public function count($query): int
|
||||||
{
|
{
|
||||||
return count($this->models);
|
return count($this->models);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the related resource for a has-one relationship.
|
|
||||||
*
|
|
||||||
* @param $model
|
|
||||||
* @param HasOne $relationship
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
|
||||||
public function getHasOneId($model, HasOne $relationship): ?string
|
|
||||||
{
|
|
||||||
// TODO: Implement getHasOneId() method.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ class CreateTest extends AbstractTestCase
|
||||||
|
|
||||||
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($createdModel) {
|
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($createdModel) {
|
||||||
$type->creatable();
|
$type->creatable();
|
||||||
$type->create(function ($request) use ($createdModel) {
|
$type->createModel(function ($request) use ($createdModel) {
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
return $createdModel;
|
return $createdModel;
|
||||||
});
|
});
|
||||||
|
|
@ -185,13 +185,13 @@ class CreateTest extends AbstractTestCase
|
||||||
|
|
||||||
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($adapter, $createdModel, &$called) {
|
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($adapter, $createdModel, &$called) {
|
||||||
$type->creatable();
|
$type->creatable();
|
||||||
$type->creating(function ($model, $request) use ($adapter, $createdModel, &$called) {
|
$type->onCreating(function ($model, $request) use ($adapter, $createdModel, &$called) {
|
||||||
$this->assertSame($createdModel, $model);
|
$this->assertSame($createdModel, $model);
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
$adapter->save($createdModel)->shouldNotHaveBeenCalled();
|
$adapter->save($createdModel)->shouldNotHaveBeenCalled();
|
||||||
$called++;
|
$called++;
|
||||||
});
|
});
|
||||||
$type->created(function ($model, $request) use ($adapter, $createdModel, &$called) {
|
$type->onCreated(function ($model, $request) use ($adapter, $createdModel, &$called) {
|
||||||
$this->assertSame($createdModel, $model);
|
$this->assertSame($createdModel, $model);
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
$adapter->save($createdModel)->shouldHaveBeenCalled();
|
$adapter->save($createdModel)->shouldHaveBeenCalled();
|
||||||
|
|
|
||||||
|
|
@ -165,13 +165,13 @@ class DeleteTest extends AbstractTestCase
|
||||||
|
|
||||||
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($adapter, $deletingModel, &$called) {
|
$this->api->resource('users', $adapter->reveal(), function (Type $type) use ($adapter, $deletingModel, &$called) {
|
||||||
$type->deletable();
|
$type->deletable();
|
||||||
$type->deleting(function ($model, $request) use ($adapter, $deletingModel, &$called) {
|
$type->onDeleting(function ($model, $request) use ($adapter, $deletingModel, &$called) {
|
||||||
$this->assertSame($deletingModel, $model);
|
$this->assertSame($deletingModel, $model);
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
$adapter->delete($deletingModel)->shouldNotHaveBeenCalled();
|
$adapter->delete($deletingModel)->shouldNotHaveBeenCalled();
|
||||||
$called++;
|
$called++;
|
||||||
});
|
});
|
||||||
$type->deleted(function ($model, $request) use ($adapter, $deletingModel, &$called) {
|
$type->onDeleted(function ($model, $request) use ($adapter, $deletingModel, &$called) {
|
||||||
$this->assertSame($deletingModel, $model);
|
$this->assertSame($deletingModel, $model);
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
$adapter->delete($deletingModel)->shouldHaveBeenCalled();
|
$adapter->delete($deletingModel)->shouldHaveBeenCalled();
|
||||||
|
|
|
||||||
|
|
@ -29,21 +29,6 @@ class MetaTest extends AbstractTestCase
|
||||||
$this->api = new JsonApi('http://example.com');
|
$this->api = new JsonApi('http://example.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_meta_fields_can_be_added_to_resources_with_a_value()
|
|
||||||
{
|
|
||||||
$this->api->resource('users', new MockAdapter(), function (Type $type) {
|
|
||||||
$type->meta('foo', 'bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
$response = $this->api->handle(
|
|
||||||
$this->buildRequest('GET', '/users/1')
|
|
||||||
);
|
|
||||||
|
|
||||||
$document = json_decode($response->getBody(), true);
|
|
||||||
|
|
||||||
$this->assertEquals(['foo' => 'bar'], $document['data']['meta']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_meta_fields_can_be_added_to_resources_with_a_closure()
|
public function test_meta_fields_can_be_added_to_resources_with_a_closure()
|
||||||
{
|
{
|
||||||
$adapter = new MockAdapter(['1' => (object) ['id' => '1']]);
|
$adapter = new MockAdapter(['1' => (object) ['id' => '1']]);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer\feature;
|
namespace Tobyz\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\JsonApi;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Schema\Type;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
|
@ -67,30 +66,6 @@ class SortingTest extends AbstractTestCase
|
||||||
$this->assertContains([$attribute, 'asc'], $this->adapter->query->sort);
|
$this->assertContains([$attribute, 'asc'], $this->adapter->query->sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_attributes_can_be_sortable_with_custom_logic()
|
|
||||||
{
|
|
||||||
$called = false;
|
|
||||||
|
|
||||||
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
|
||||||
$type->attribute('name')
|
|
||||||
->sortable(function ($query, $direction, $request) use (&$called) {
|
|
||||||
$this->assertSame($this->adapter->query, $query);
|
|
||||||
$this->assertEquals('asc', $direction);
|
|
||||||
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
|
||||||
|
|
||||||
$called = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->api->handle(
|
|
||||||
$this->buildRequest('GET', '/users')
|
|
||||||
->withQueryParams(['sort' => 'name'])
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertTrue($called);
|
|
||||||
$this->assertTrue(empty($this->adapter->query->sort));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_attribute_sortable_callback_receives_correct_parameters()
|
public function test_attribute_sortable_callback_receives_correct_parameters()
|
||||||
{
|
{
|
||||||
$this->markTestIncomplete();
|
$this->markTestIncomplete();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue