wip
This commit is contained in:
parent
40776bc6ab
commit
46d5ebd2a9
|
|
@ -1,2 +1,3 @@
|
||||||
composer.lock
|
composer.lock
|
||||||
vendor
|
vendor
|
||||||
|
.phpunit.result.cache
|
||||||
77
README.md
77
README.md
|
|
@ -12,27 +12,27 @@ composer require tobyz/json-api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Api;
|
|
||||||
use Tobyz\JsonApiServer\Adapter\EloquentAdapter;
|
use Tobyz\JsonApiServer\Adapter\EloquentAdapter;
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
|
||||||
$api = new Api('http://example.com/api');
|
$api = new JsonApi('http://example.com/api');
|
||||||
|
|
||||||
$api->resource('articles', new EloquentAdapter(new Article), function (Builder $schema) {
|
$api->resource('articles', new EloquentAdapter(Article::class), function (Type $type) {
|
||||||
$schema->attribute('title');
|
$type->attribute('title');
|
||||||
$schema->hasOne('author', 'people');
|
$type->hasOne('author')->type('people');
|
||||||
$schema->hasMany('comments');
|
$type->hasMany('comments');
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('people', new EloquentAdapter(new User), function (Builder $schema) {
|
$api->resource('people', new EloquentAdapter(User::class), function (Type $type) {
|
||||||
$schema->attribute('firstName');
|
$type->attribute('firstName');
|
||||||
$schema->attribute('lastName');
|
$type->attribute('lastName');
|
||||||
$schema->attribute('twitter');
|
$type->attribute('twitter');
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('comments', new EloquentAdapter(new Comment), function (Builder $schema) {
|
$api->resource('comments', new EloquentAdapter(Comment::class), function (Type $type) {
|
||||||
$schema->attribute('body');
|
$type->attribute('body');
|
||||||
$schema->hasOne('author', 'people');
|
$type->hasOne('author')->type('people');
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @var Psr\Http\Message\ServerRequestInterface $request */
|
/** @var Psr\Http\Message\ServerRequestInterface $request */
|
||||||
|
|
@ -60,9 +60,9 @@ The schema definition is extremely powerful and lets you easily apply [permissio
|
||||||
### Handling Requests
|
### Handling Requests
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
|
||||||
$api = new Api('http://example.com/api');
|
$api = new JsonApi('http://example.com/api');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$response = $api->handle($request);
|
$response = $api->handle($request);
|
||||||
|
|
@ -71,26 +71,26 @@ try {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`Tobyz\JsonApiServer\Api` 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 `Api` handler take it from there. Catch any exceptions and give them back to `Api` 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` if you want a JSON:API error response.
|
||||||
|
|
||||||
### Defining Resources
|
### Defining Resources
|
||||||
|
|
||||||
Define your API's resources using the `resource` method. The first argument is the [resource type](https://jsonapi.org/format/#document-resource-object-identification). The second is an instance of `Tobyz\JsonApiServer\Adapter\AdapterInterface` which will allow the handler to interact with your models. The third is a closure in which you'll build the schema for your resource.
|
Define your API's resources using the `resource` method. The first argument is the [resource type](https://jsonapi.org/format/#document-resource-object-identification). The second is an instance of `Tobyz\JsonApiServer\Adapter\AdapterInterface` which will allow the handler to interact with your models. The third is a closure in which you'll build the schema for your resource.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
|
||||||
$api->resource('comments', $adapter, function (Builder $schema) {
|
$api->resource('comments', $adapter, function (Schema $schema) {
|
||||||
// define your schema
|
// define your schema
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
We provide an `EloquentAdapter` to hook your resources up with Laravel [Eloquent](https://laravel.com/docs/5.8/eloquent) models. Set it up with an instance of the model that your resource represents. You can [implement your own adapter](https://github.com/tobyz/json-api-server/blob/master/src/Adapter/AdapterInterface.php) if you use a different ORM.
|
We provide an `EloquentAdapter` to hook your resources up with Laravel [Eloquent](https://laravel.com/docs/5.8/eloquent) models. Set it up with the name of the model that your resource represents. You can [implement your own adapter](https://github.com/tobyz/json-api-server/blob/master/src/Adapter/AdapterInterface.php) if you use a different ORM.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApiServer\Adapter\EloquentAdapter;
|
use Tobyz\JsonApiServer\Adapter\EloquentAdapter;
|
||||||
|
|
||||||
$adapter = new EloquentAdapter(new User);
|
$adapter = new EloquentAdapter(User::class);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Attributes
|
### Attributes
|
||||||
|
|
@ -116,10 +116,10 @@ $schema->hasOne('user');
|
||||||
$schema->hasMany('comments');
|
$schema->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, provide it as a second argument:
|
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', 'people');
|
$schema->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, provide it as a third argument.
|
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, provide it as a third argument.
|
||||||
|
|
@ -173,14 +173,14 @@ $schema->hasOne('user')
|
||||||
|
|
||||||
#### Polymorphic Relationships
|
#### Polymorphic Relationships
|
||||||
|
|
||||||
Define polymorphic relationships on your resource using the `morphOne` and `morphMany` methods:
|
Define a relationship as polymorphic using the `polymorphic` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->morphOne('commentable');
|
$schema->hasOne('commentable')->polymorphic();
|
||||||
$schema->morphMany('taggable');
|
$schema->hasMany('taggable')->polymorphic();
|
||||||
```
|
```
|
||||||
|
|
||||||
Polymorphic relationships do not accept a second argument for the resource type, because it will be automatically derived from each related resource. 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
|
||||||
|
|
||||||
|
|
@ -257,7 +257,6 @@ $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')
|
$schema->attribute('joinedAt')
|
||||||
->default(new DateTime);
|
->default(new DateTime);
|
||||||
|
|
@ -299,7 +298,7 @@ $schema->hasMany('groups')
|
||||||
You can easily use Laravel's [Validation](https://laravel.com/docs/5.8/validation) component for field validation with the `rules` function:
|
You can easily use Laravel's [Validation](https://laravel.com/docs/5.8/validation) component for field validation with the `rules` function:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Tobyz\JsonApi\Server\Laravel\rules;
|
use Tobyz\JsonApiServer\Laravel\rules;
|
||||||
|
|
||||||
$schema->attribute('username')
|
$schema->attribute('username')
|
||||||
->validate(rules('required', 'min:3', 'max:30'));
|
->validate(rules('required', 'min:3', 'max:30'));
|
||||||
|
|
@ -312,7 +311,7 @@ Use the `set` method to define custom mutation logic for your field, instead of
|
||||||
```php
|
```php
|
||||||
$schema->attribute('firstName')
|
$schema->attribute('firstName')
|
||||||
->set(function ($model, $value, $request) {
|
->set(function ($model, $value, $request) {
|
||||||
return $model->first_name = strtolower($value);
|
$model->first_name = strtolower($value);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -396,6 +395,14 @@ You can set a default sort string to be used when the consumer has not supplied
|
||||||
$schema->defaultSort('-updatedAt,-createdAt');
|
$schema->defaultSort('-updatedAt,-createdAt');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To define sortable criteria that does not correspond to an attribute, use the `sort` method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$schema->sort('relevance', function ($query, $direction, $request) {
|
||||||
|
$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 limit using the `paginate` method on the schema builder, or you can remove it by passing `null`:
|
||||||
|
|
@ -434,9 +441,11 @@ $schema->meta('requestTime', function ($request) {
|
||||||
|
|
||||||
### 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:
|
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.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
$schema->creatable();
|
||||||
|
|
||||||
$schema->creatable(function ($request) {
|
$schema->creatable(function ($request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
|
|
@ -444,7 +453,7 @@ $schema->creatable(function ($request) {
|
||||||
|
|
||||||
#### Customizing the Model
|
#### Customizing the Model
|
||||||
|
|
||||||
When creating a resource, an empty model is supplied by the adapter. You may wish to 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 `create` method:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$schema->create(function ($request) {
|
$schema->create(function ($request) {
|
||||||
|
|
@ -457,6 +466,8 @@ $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();
|
||||||
|
|
||||||
$schema->updatable(function ($request) {
|
$schema->updatable(function ($request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
|
|
@ -467,6 +478,8 @@ $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();
|
||||||
|
|
||||||
$schema->deletable(function ($request) {
|
$schema->deletable(function ($request) {
|
||||||
return $request->getAttribute('isAdmin');
|
return $request->getAttribute('isAdmin');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Tobyz\\JsonApiServer\\": "src/"
|
"Tobyz\\JsonApiServer\\": "src/"
|
||||||
}
|
},
|
||||||
|
"files": ["src/functions.php"]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
@ -26,7 +27,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^7.4"
|
"dms/phpunit-arraysubset-asserts": "^0.1.0",
|
||||||
|
"helmich/phpunit-json-assert": "^3.0",
|
||||||
|
"phpunit/phpunit": "^8.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
|
|
|
||||||
|
|
@ -9,47 +9,231 @@ use Tobyz\JsonApiServer\Schema\Relationship;
|
||||||
|
|
||||||
interface AdapterInterface
|
interface AdapterInterface
|
||||||
{
|
{
|
||||||
public function handles($model);
|
/**
|
||||||
|
* Create a new query builder instance.
|
||||||
public function create();
|
*
|
||||||
|
* This is used as a basis for building the queries which show a resource
|
||||||
|
* or list a resource index. It will be passed around through the relevant
|
||||||
|
* scopes, filters, and sorting methods before finally being passed into
|
||||||
|
* the `find` or `get` methods.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function query();
|
public function query();
|
||||||
|
|
||||||
public function find($query, $id);
|
/**
|
||||||
|
* Manipulate the query to only include resources with the given IDs.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param array $ids
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function filterByIds($query, array $ids): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulate the query to only include resources with a certain attribute
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param Attribute $attribute
|
||||||
|
* @param $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function filterByAttribute($query, Attribute $attribute, $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulate the query to only include resources with any one of the given
|
||||||
|
* resource IDs in a has-one relationship.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param HasOne $relationship
|
||||||
|
* @param array $ids
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function filterByHasOne($query, HasOne $relationship, array $ids): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulate the query to only include resources with any one of the given
|
||||||
|
* resource IDs in a has-many relationship.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param HasMany $relationship
|
||||||
|
* @param array $ids
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function filterByHasMany($query, HasMany $relationship, array $ids): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulate the query to sort by the given attribute in the given direction.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param Attribute $attribute
|
||||||
|
* @param string $direction
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function sortByAttribute($query, Attribute $attribute, string $direction): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulate the query to only include a certain number of results,
|
||||||
|
* starting from the given offset.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param int $limit
|
||||||
|
* @param int $offset
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function paginate($query, int $limit, int $offset): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a single resource by ID from the query.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @param string $id
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function find($query, string $id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of resources from the query.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
public function get($query): array;
|
public function get($query): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of results from the query.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function count($query): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether or not this resource type represents the given model.
|
||||||
|
*
|
||||||
|
* This is used for polymorphic relationships, where there are one or many
|
||||||
|
* related models of unknown type. The first resource type with an adapter
|
||||||
|
* that responds positively from this method will be used.
|
||||||
|
*
|
||||||
|
* @param mixed $model
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function represents($model): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new model instance.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function create();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID from the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
public function getId($model): string;
|
public function getId($model): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of an attribute from the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param Attribute $attribute
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
public function getAttribute($model, Attribute $attribute);
|
public function getAttribute($model, Attribute $attribute);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the model for a has-one relationship for the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param HasOne $relationship
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
public function getHasOne($model, HasOne $relationship);
|
public function getHasOne($model, HasOne $relationship);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of models for a has-many relationship for the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param HasMany $relationship
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
public function getHasMany($model, HasMany $relationship): array;
|
public function getHasMany($model, HasMany $relationship): array;
|
||||||
|
|
||||||
public function applyAttribute($model, Attribute $attribute, $value);
|
/**
|
||||||
|
* Apply an attribute value to the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param Attribute $attribute
|
||||||
|
* @param $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function setAttribute($model, Attribute $attribute, $value): void;
|
||||||
|
|
||||||
public function applyHasOne($model, HasOne $relationship, $related);
|
/**
|
||||||
|
* Apply a has-one relationship value to the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param HasOne $relationship
|
||||||
|
* @param $related
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function setHasOne($model, HasOne $relationship, $related): void;
|
||||||
|
|
||||||
public function save($model);
|
/**
|
||||||
|
* Save the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function save($model): void;
|
||||||
|
|
||||||
public function saveHasMany($model, HasMany $relationship, array $related);
|
/**
|
||||||
|
* Save a has-many relationship for the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @param HasMany $relationship
|
||||||
|
* @param array $related
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function saveHasMany($model, HasMany $relationship, array $related): void;
|
||||||
|
|
||||||
public function delete($model);
|
/**
|
||||||
|
* Delete the model.
|
||||||
|
*
|
||||||
|
* @param $model
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function delete($model): void;
|
||||||
|
|
||||||
public function filterByIds($query, array $ids);
|
/**
|
||||||
|
* Load information about related resources onto a collection of models.
|
||||||
|
*
|
||||||
|
* @param array $models
|
||||||
|
* @param array $relationships
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function load(array $models, array $relationships): void;
|
||||||
|
|
||||||
public function filterByAttribute($query, Attribute $attribute, $value);
|
/**
|
||||||
|
* Load information about the IDs of related resources onto a collection
|
||||||
public function filterByHasOne($query, HasOne $relationship, array $ids);
|
* of models.
|
||||||
|
*
|
||||||
public function filterByHasMany($query, HasMany $relationship, array $ids);
|
* @param array $models
|
||||||
|
* @param Relationship $relationship
|
||||||
public function sortByAttribute($query, Attribute $attribute, string $direction);
|
* @return mixed
|
||||||
|
*/
|
||||||
public function paginate($query, int $limit, int $offset);
|
public function loadIds(array $models, Relationship $relationship): void;
|
||||||
|
|
||||||
public function load(array $models, array $relationships);
|
|
||||||
|
|
||||||
public function loadIds(array $models, Relationship $relationship);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
use Tobyz\JsonApiServer\Schema\HasMany;
|
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||||
use Tobyz\JsonApiServer\Schema\HasOne;
|
use Tobyz\JsonApiServer\Schema\HasOne;
|
||||||
|
|
@ -24,11 +25,11 @@ class EloquentAdapter implements AdapterInterface
|
||||||
$this->model = is_string($model) ? new $model : $model;
|
$this->model = is_string($model) ? new $model : $model;
|
||||||
|
|
||||||
if (! $this->model instanceof Model) {
|
if (! $this->model instanceof Model) {
|
||||||
throw new \InvalidArgumentException('Model must be an instance of '.Model::class);
|
throw new InvalidArgumentException('Model must be an instance of '.Model::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handles($model)
|
public function represents($model): bool
|
||||||
{
|
{
|
||||||
return $model instanceof $this->model;
|
return $model instanceof $this->model;
|
||||||
}
|
}
|
||||||
|
|
@ -43,7 +44,7 @@ class EloquentAdapter implements AdapterInterface
|
||||||
return $this->model->query();
|
return $this->model->query();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function find($query, $id)
|
public function find($query, string $id)
|
||||||
{
|
{
|
||||||
return $query->find($id);
|
return $query->find($id);
|
||||||
}
|
}
|
||||||
|
|
@ -63,64 +64,63 @@ class EloquentAdapter implements AdapterInterface
|
||||||
return $model->getKey();
|
return $model->getKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttribute($model, Attribute $field)
|
public function getAttribute($model, Attribute $attribute)
|
||||||
{
|
{
|
||||||
return $model->{$this->getAttributeProperty($field)};
|
return $model->{$this->getAttributeProperty($attribute)};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasOneId($model, HasOne $field)
|
public function getHasOneId($model, HasOne $relationship): ?string
|
||||||
{
|
{
|
||||||
$relation = $model->{$this->getRelationshipProperty($field)}();
|
$relation = $this->getRelation($model, $relationship);
|
||||||
|
|
||||||
|
// If this is a belongs-to relation, we can simply return the value of
|
||||||
|
// the foreign key on the model.
|
||||||
if ($relation instanceof BelongsTo) {
|
if ($relation instanceof BelongsTo) {
|
||||||
$related = $relation->getRelated();
|
return $model->{$relation->getForeignKeyName()};
|
||||||
|
|
||||||
$key = $model->{$relation->getForeignKeyName()};
|
|
||||||
|
|
||||||
if ($key) {
|
|
||||||
return $related->forceFill([$related->getKeyName() => $key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $model->{$this->getRelationshipProperty($field)};
|
$related = $this->getRelationValue($model, $relationship);
|
||||||
|
|
||||||
|
return $related ? $related->getKey() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasOne($model, HasOne $field)
|
public function getHasOne($model, HasOne $relationship)
|
||||||
{
|
{
|
||||||
return $model->{$this->getRelationshipProperty($field)};
|
return $this->getRelationValue($model, $relationship);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasMany($model, HasMany $field): array
|
public function getHasMany($model, HasMany $relationship): array
|
||||||
{
|
{
|
||||||
$collection = $model->{$this->getRelationshipProperty($field)};
|
$collection = $this->getRelationValue($model, $relationship);
|
||||||
|
|
||||||
return $collection ? $collection->all() : [];
|
return $collection ? $collection->all() : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyAttribute($model, Attribute $field, $value)
|
public function setAttribute($model, Attribute $attribute, $value): void
|
||||||
{
|
{
|
||||||
$model->{$this->getAttributeProperty($field)} = $value;
|
$model->{$this->getAttributeProperty($attribute)} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyHasOne($model, HasOne $field, $related)
|
public function setHasOne($model, HasOne $relationship, $related): void
|
||||||
{
|
{
|
||||||
$model->{$this->getRelationshipProperty($field)}()->associate($related);
|
$this->getRelation($model, $relationship)->associate($related);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save($model)
|
public function save($model): void
|
||||||
{
|
{
|
||||||
$model->save();
|
$model->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveHasMany($model, HasMany $field, array $related)
|
public function saveHasMany($model, HasMany $relationship, array $related): void
|
||||||
{
|
{
|
||||||
$model->{$this->getRelationshipProperty($field)}()->sync(Collection::make($related));
|
$this->getRelation($model, $relationship)->sync(new Collection($related));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($model)
|
public function delete($model): void
|
||||||
{
|
{
|
||||||
|
// For models that use the SoftDeletes trait, deleting the resource from
|
||||||
|
// the API implies permanent deletion. Non-permanent deletion should be
|
||||||
|
// achieved by manipulating a resource attribute.
|
||||||
if (method_exists($model, 'forceDelete')) {
|
if (method_exists($model, 'forceDelete')) {
|
||||||
$model->forceDelete();
|
$model->forceDelete();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -128,17 +128,18 @@ class EloquentAdapter implements AdapterInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByIds($query, array $ids)
|
public function filterByIds($query, array $ids): void
|
||||||
{
|
{
|
||||||
$key = $query->getModel()->getQualifiedKeyName();
|
$key = $query->getModel()->getQualifiedKeyName();
|
||||||
|
|
||||||
$query->whereIn($key, $ids);
|
$query->whereIn($key, $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByAttribute($query, Attribute $field, $value)
|
public function filterByAttribute($query, Attribute $attribute, $value): void
|
||||||
{
|
{
|
||||||
$property = $this->getAttributeProperty($field);
|
$property = $this->getAttributeProperty($attribute);
|
||||||
|
|
||||||
|
// TODO: extract this into non-adapter territory
|
||||||
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
|
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
|
||||||
if ($matches[1] !== '*') {
|
if ($matches[1] !== '*') {
|
||||||
$query->where($property, '>=', $matches[1]);
|
$query->where($property, '>=', $matches[1]);
|
||||||
|
|
@ -161,19 +162,17 @@ class EloquentAdapter implements AdapterInterface
|
||||||
$query->where($property, $value);
|
$query->where($property, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByHasOne($query, HasOne $field, array $ids)
|
public function filterByHasOne($query, HasOne $relationship, array $ids): void
|
||||||
{
|
{
|
||||||
$relation = $query->getModel()->{$this->getRelationshipProperty($field)}();
|
$relation = $this->getRelation($query->getModel(), $relationship);
|
||||||
|
|
||||||
$foreignKey = $relation->getQualifiedForeignKeyName();
|
$query->whereIn($relation->getQualifiedForeignKeyName(), $ids);
|
||||||
|
|
||||||
$query->whereIn($foreignKey, $ids);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByHasMany($query, HasMany $field, array $ids)
|
public function filterByHasMany($query, HasMany $relationship, array $ids): void
|
||||||
{
|
{
|
||||||
$property = $this->getRelationshipProperty($field);
|
$property = $this->getRelationshipProperty($relationship);
|
||||||
$relation = $query->getModel()->{$property}();
|
$relation = $this->getRelation($query->getModel(), $relationship);
|
||||||
$relatedKey = $relation->getRelated()->getQualifiedKeyName();
|
$relatedKey = $relation->getRelated()->getQualifiedKeyName();
|
||||||
|
|
||||||
$query->whereHas($property, function ($query) use ($relatedKey, $ids) {
|
$query->whereHas($property, function ($query) use ($relatedKey, $ids) {
|
||||||
|
|
@ -181,22 +180,22 @@ class EloquentAdapter implements AdapterInterface
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sortByAttribute($query, Attribute $field, string $direction)
|
public function sortByAttribute($query, Attribute $field, string $direction): void
|
||||||
{
|
{
|
||||||
$query->orderBy($this->getAttributeProperty($field), $direction);
|
$query->orderBy($this->getAttributeProperty($field), $direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginate($query, int $limit, int $offset)
|
public function paginate($query, int $limit, int $offset): void
|
||||||
{
|
{
|
||||||
$query->take($limit)->skip($offset);
|
$query->take($limit)->skip($offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(array $models, array $trail)
|
public function load(array $models, array $relationships): void
|
||||||
{
|
{
|
||||||
(new Collection($models))->loadMissing($this->relationshipTrailToPath($trail));
|
(new Collection($models))->loadMissing($this->getRelationshipPath($relationships));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadIds(array $models, Relationship $relationship)
|
public function loadIds(array $models, Relationship $relationship): void
|
||||||
{
|
{
|
||||||
if (empty($models)) {
|
if (empty($models)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -219,20 +218,28 @@ class EloquentAdapter implements AdapterInterface
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAttributeProperty(Attribute $field)
|
private function getAttributeProperty(Attribute $attribute): string
|
||||||
{
|
{
|
||||||
return $field->property ?: strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $field->name));
|
return $attribute->getProperty() ?: strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $attribute->getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getRelationshipProperty(Relationship $field)
|
private function getRelationshipProperty(Relationship $relationship): string
|
||||||
{
|
{
|
||||||
return $field->property ?: $field->name;
|
return $relationship->getProperty() ?: $relationship->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function relationshipTrailToPath(array $trail)
|
private function getRelationshipPath(array $trail): string
|
||||||
{
|
{
|
||||||
return implode('.', array_map(function ($relationship) {
|
return implode('.', array_map([$this, 'getRelationshipProperty'], $trail));
|
||||||
return $this->getRelationshipProperty($relationship);
|
}
|
||||||
}, $trail));
|
|
||||||
|
private function getRelation($model, Relationship $relationship)
|
||||||
|
{
|
||||||
|
return $model->{$this->getRelationshipProperty($relationship)}();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRelationValue($model, Relationship $relationship)
|
||||||
|
{
|
||||||
|
return $model->{$this->getRelationshipProperty($relationship)};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
src/Api.php
144
src/Api.php
|
|
@ -1,144 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use JsonApiPhp\JsonApi;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
|
||||||
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
|
||||||
use Tobyz\JsonApiServer\Exception\NotImplementedException;
|
|
||||||
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
|
||||||
use Tobyz\JsonApiServer\Handler\Concerns\FindsResources;
|
|
||||||
|
|
||||||
class Api implements RequestHandlerInterface
|
|
||||||
{
|
|
||||||
use FindsResources;
|
|
||||||
|
|
||||||
protected $resources = [];
|
|
||||||
protected $baseUrl;
|
|
||||||
|
|
||||||
public function __construct(string $baseUrl)
|
|
||||||
{
|
|
||||||
$this->baseUrl = $baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resource(string $type, $adapter, Closure $buildSchema = null): void
|
|
||||||
{
|
|
||||||
$this->resources[$type] = new ResourceType($type, $adapter, $buildSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getResources(): array
|
|
||||||
{
|
|
||||||
return $this->resources;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getResource(string $type): ResourceType
|
|
||||||
{
|
|
||||||
if (! isset($this->resources[$type])) {
|
|
||||||
throw new ResourceNotFoundException($type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->resources[$type];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
|
||||||
{
|
|
||||||
$path = $this->stripBasePath(
|
|
||||||
$request->getUri()->getPath()
|
|
||||||
);
|
|
||||||
|
|
||||||
$segments = explode('/', trim($path, '/'));
|
|
||||||
$count = count($segments);
|
|
||||||
|
|
||||||
$resource = $this->getResource($segments[0]);
|
|
||||||
|
|
||||||
if ($count === 1) {
|
|
||||||
switch ($request->getMethod()) {
|
|
||||||
case 'GET':
|
|
||||||
return $this->handleWithHandler($request, new Handler\Index($this, $resource));
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
return $this->handleWithHandler($request, new Handler\Create($this, $resource));
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new MethodNotAllowedException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$model = $this->findResource($request, $resource, $segments[1]);
|
|
||||||
|
|
||||||
if ($count === 2) {
|
|
||||||
switch ($request->getMethod()) {
|
|
||||||
case 'PATCH':
|
|
||||||
return $this->handleWithHandler($request, new Handler\Update($this, $resource, $model));
|
|
||||||
|
|
||||||
case 'GET':
|
|
||||||
return $this->handleWithHandler($request, new Handler\Show($this, $resource, $model));
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
return $this->handleWithHandler($request, new Handler\Delete($resource, $model));
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new MethodNotAllowedException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count === 3) {
|
|
||||||
throw new NotImplementedException;
|
|
||||||
|
|
||||||
// return $this->handleRelated($request, $resource, $model, $segments[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count === 4 && $segments[2] === 'relationships') {
|
|
||||||
throw new NotImplementedException;
|
|
||||||
|
|
||||||
// return $this->handleRelationship($request, $resource, $model, $segments[3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function stripBasePath(string $path): string
|
|
||||||
{
|
|
||||||
$basePath = parse_url($this->baseUrl, PHP_URL_PATH);
|
|
||||||
|
|
||||||
$len = strlen($basePath);
|
|
||||||
|
|
||||||
if (substr($path, 0, $len) === $basePath) {
|
|
||||||
$path = substr($path, $len + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleWithHandler(Request $request, RequestHandlerInterface $handler)
|
|
||||||
{
|
|
||||||
$request = $request->withAttribute('jsonApiHandler', $handler);
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function error($e)
|
|
||||||
{
|
|
||||||
if (! $e instanceof ErrorProviderInterface) {
|
|
||||||
$e = new Exception\InternalServerErrorException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = $e->getJsonApiErrors();
|
|
||||||
$status = $e->getJsonApiStatus();
|
|
||||||
|
|
||||||
$data = new JsonApi\ErrorDocument(
|
|
||||||
...$errors
|
|
||||||
);
|
|
||||||
|
|
||||||
return new JsonApiResponse($data, $status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBaseUrl(): string
|
|
||||||
{
|
|
||||||
return $this->baseUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class BadRequestException extends \DomainException implements ErrorProviderInterface
|
class BadRequestException extends DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class ForbiddenException extends \DomainException implements ErrorProviderInterface
|
class ForbiddenException extends DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use RuntimeException;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class InternalServerErrorException extends \RuntimeException implements ErrorProviderInterface
|
class InternalServerErrorException extends RuntimeException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Error(
|
new Error(
|
||||||
new Error\Title('Internal Server Error'),
|
new Error\Title('Internal Server Error'),
|
||||||
new Error\Status('500')
|
new Error\Status($this->getJsonApiStatus())
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,24 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException as DomainExceptionAlias;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class MethodNotAllowedException extends \DomainException implements ErrorProviderInterface
|
class MethodNotAllowedException extends DomainExceptionAlias implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Error(
|
new Error(
|
||||||
new Error\Title('Method Not Allowed'),
|
new Error\Title('Method Not Allowed'),
|
||||||
new Error\Status('405')
|
new Error\Status($this->getJsonApiStatus())
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getJsonApiStatus(): string
|
||||||
|
{
|
||||||
|
return '405';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use RuntimeException;
|
||||||
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class NotAcceptableException extends RuntimeException implements ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Not Acceptable'),
|
||||||
|
new Error\Status($this->getJsonApiStatus())
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJsonApiStatus(): string
|
||||||
|
{
|
||||||
|
return '406';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,18 +2,24 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class NotImplementedException extends \DomainException implements ErrorProviderInterface
|
class NotImplementedException extends DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new Error(
|
new Error(
|
||||||
new Error\Title('Not Implemented'),
|
new Error\Title('Not Implemented'),
|
||||||
new Error\Status('501')
|
new Error\Status($this->getJsonApiStatus())
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getJsonApiStatus(): string
|
||||||
|
{
|
||||||
|
return '501';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use RuntimeException;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class ResourceNotFoundException extends \RuntimeException implements ErrorProviderInterface
|
class ResourceNotFoundException extends RuntimeException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
protected $type;
|
protected $type;
|
||||||
protected $id;
|
protected $id;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class UnauthorizedException extends \DomainException implements ErrorProviderInterface
|
class UnauthorizedException extends DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Exception;
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
use JsonApiPhp\JsonApi\Error;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class UnprocessableEntityException extends \DomainException implements ErrorProviderInterface
|
class UnprocessableEntityException extends DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
private $failures;
|
private $failures;
|
||||||
|
|
||||||
|
|
@ -19,11 +20,19 @@ class UnprocessableEntityException extends \DomainException implements ErrorProv
|
||||||
public function getJsonApiErrors(): array
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
return array_map(function ($failure) {
|
return array_map(function ($failure) {
|
||||||
return new Error(
|
$members = [
|
||||||
new Error\Status($this->getJsonApiStatus()),
|
new Error\Status($this->getJsonApiStatus()),
|
||||||
new Error\SourcePointer('/data/'.$failure['field']->location.'/'.$failure['field']->name),
|
];
|
||||||
new Error\Detail($failure['message'])
|
|
||||||
);
|
if ($field = $failure['field']) {
|
||||||
|
$members[] = new Error\SourcePointer('/data/'.$field->getLocation().'/'.$field->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failure['message']) {
|
||||||
|
$members[] = new Error\Detail($failure['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error(...$members);
|
||||||
}, $this->failures);
|
}, $this->failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use RuntimeException;
|
||||||
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class UnsupportedMediaTypeException extends RuntimeException implements ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Unsupported Media Type'),
|
||||||
|
new Error\Status($this->getJsonApiStatus())
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJsonApiStatus(): string
|
||||||
|
{
|
||||||
|
return '415';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,18 +5,17 @@ namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
|
use function Tobyz\JsonApiServer\run_callbacks;
|
||||||
|
|
||||||
trait FindsResources
|
trait FindsResources
|
||||||
{
|
{
|
||||||
private function findResource(Request $request, ResourceType $resource, $id)
|
private function findResource(Request $request, ResourceType $resource, string $id)
|
||||||
{
|
{
|
||||||
$adapter = $resource->getAdapter();
|
$adapter = $resource->getAdapter();
|
||||||
|
|
||||||
$query = $adapter->query();
|
$query = $adapter->query();
|
||||||
|
|
||||||
foreach ($resource->getSchema()->scopes as $scope) {
|
run_callbacks($resource->getSchema()->getScopes(), [$query, $request, $id]);
|
||||||
$scope($request, $query, $id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$model = $adapter->find($query, $id);
|
$model = $adapter->find($query, $id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use function Tobyz\JsonApiServer\evaluate;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
use Tobyz\JsonApiServer\Schema\HasMany;
|
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||||
|
|
@ -47,18 +49,18 @@ trait IncludesData
|
||||||
|
|
||||||
private function validateInclude(ResourceType $resource, array $include, string $path = '')
|
private function validateInclude(ResourceType $resource, array $include, string $path = '')
|
||||||
{
|
{
|
||||||
$schema = $resource->getSchema();
|
$fields = $resource->getSchema()->getFields();
|
||||||
|
|
||||||
foreach ($include as $name => $nested) {
|
foreach ($include as $name => $nested) {
|
||||||
if (! isset($schema->fields[$name])
|
if (! isset($fields[$name])
|
||||||
|| ! $schema->fields[$name] instanceof Relationship
|
|| ! $fields[$name] instanceof Relationship
|
||||||
|| ($schema->fields[$name] instanceof HasMany && ! $schema->fields[$name]->includable)
|
|| ! $fields[$name]->isIncludable()
|
||||||
) {
|
) {
|
||||||
throw new BadRequestException("Invalid include [{$path}{$name}]", 'include');
|
throw new BadRequestException("Invalid include [{$path}{$name}]", 'include');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($schema->fields[$name]->resource) {
|
if ($type = $fields[$name]->getType()) {
|
||||||
$relatedResource = $this->api->getResource($schema->fields[$name]->resource);
|
$relatedResource = $this->api->getResource($type);
|
||||||
|
|
||||||
$this->validateInclude($relatedResource, $nested, $name.'.');
|
$this->validateInclude($relatedResource, $nested, $name.'.');
|
||||||
} elseif ($nested) {
|
} elseif ($nested) {
|
||||||
|
|
@ -69,18 +71,18 @@ trait IncludesData
|
||||||
|
|
||||||
private function buildRelationshipTrails(ResourceType $resource, array $include): array
|
private function buildRelationshipTrails(ResourceType $resource, array $include): array
|
||||||
{
|
{
|
||||||
$schema = $resource->getSchema();
|
$fields = $resource->getSchema()->getFields();
|
||||||
$trails = [];
|
$trails = [];
|
||||||
|
|
||||||
foreach ($include as $name => $nested) {
|
foreach ($include as $name => $nested) {
|
||||||
$relationship = $schema->fields[$name];
|
$relationship = $fields[$name];
|
||||||
|
|
||||||
if ($relationship->loadable) {
|
if ($relationship->getLoadable()) {
|
||||||
$trails[] = [$relationship];
|
$trails[] = [$relationship];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($schema->fields[$name]->resource) {
|
if ($type = $fields[$name]->getType()) {
|
||||||
$relatedResource = $this->api->getResource($relationship->resource);
|
$relatedResource = $this->api->getResource($type);
|
||||||
|
|
||||||
$trails = array_merge(
|
$trails = array_merge(
|
||||||
$trails,
|
$trails,
|
||||||
|
|
@ -100,15 +102,15 @@ trait IncludesData
|
||||||
private function loadRelationships(array $models, array $include, Request $request)
|
private function loadRelationships(array $models, array $include, Request $request)
|
||||||
{
|
{
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
$schema = $this->resource->getSchema();
|
$fields = $this->resource->getSchema()->getFields();
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($fields as $name => $field) {
|
||||||
if (! $field instanceof Relationship || ! ($field->linkage)($request) || ! $field->loadable) {
|
if (! $field instanceof Relationship || ! evaluate($field->getLinkage(), [$request]) || ! $field->getLoadable()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field->loader) {
|
if (($load = $field->getLoadable()) instanceof Closure) {
|
||||||
($field->loader)($models, true);
|
$load($models, true);
|
||||||
} else {
|
} else {
|
||||||
$adapter->loadIds($models, $field);
|
$adapter->loadIds($models, $field);
|
||||||
}
|
}
|
||||||
|
|
@ -117,9 +119,9 @@ trait IncludesData
|
||||||
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
||||||
|
|
||||||
foreach ($trails as $relationships) {
|
foreach ($trails as $relationships) {
|
||||||
if ($loader = end($relationships)->loader) {
|
if (($load = end($relationships)->getLoadable()) instanceof Closure) {
|
||||||
// TODO: probably need to loop through relationships here
|
// TODO: probably need to loop through relationships here
|
||||||
($loader)($models, false);
|
$load($models, false);
|
||||||
} else {
|
} else {
|
||||||
$adapter->load($models, $relationships);
|
$adapter->load($models, $relationships);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,42 +3,21 @@
|
||||||
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use function Tobyz\JsonApiServer\evaluate;
|
||||||
|
use function Tobyz\JsonApiServer\get_value;
|
||||||
|
use function Tobyz\JsonApiServer\has_value;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use function Tobyz\JsonApiServer\run_callbacks;
|
||||||
use Tobyz\JsonApiServer\Schema;
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
|
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||||
|
use Tobyz\JsonApiServer\Schema\HasOne;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Relationship;
|
||||||
|
|
||||||
trait SavesData
|
trait SavesData
|
||||||
{
|
{
|
||||||
use FindsResources;
|
use FindsResources;
|
||||||
|
|
||||||
private function save($model, Request $request, bool $creating = false): void
|
|
||||||
{
|
|
||||||
$data = $this->parseData($request->getParsedBody());
|
|
||||||
|
|
||||||
$adapter = $this->resource->getAdapter();
|
|
||||||
|
|
||||||
$this->assertFieldsExist($data);
|
|
||||||
|
|
||||||
$this->assertFieldsWritable($data, $model, $request);
|
|
||||||
|
|
||||||
if ($creating) {
|
|
||||||
$this->fillDefaultValues($data, $request);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->loadRelatedResources($data, $request);
|
|
||||||
|
|
||||||
$this->assertDataValid($data, $model, $request, $creating);
|
|
||||||
|
|
||||||
$this->applyValues($data, $model, $request);
|
|
||||||
|
|
||||||
$this->saveModel($model, $request);
|
|
||||||
|
|
||||||
$this->saveFields($data, $model, $request);
|
|
||||||
|
|
||||||
$this->runSavedCallbacks($data, $model, $request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseData($body): array
|
private function parseData($body): array
|
||||||
{
|
{
|
||||||
if (! is_array($body) && ! is_object($body)) {
|
if (! is_array($body) && ! is_object($body)) {
|
||||||
|
|
@ -80,15 +59,19 @@ trait SavesData
|
||||||
return $this->findResource($request, $resource, $identifier['id']);
|
return $this->findResource($request, $resource, $identifier['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function validateFields(array $data, $model, Request $request)
|
||||||
|
{
|
||||||
|
$this->assertFieldsExist($data);
|
||||||
|
$this->assertFieldsWritable($data, $model, $request);
|
||||||
|
}
|
||||||
|
|
||||||
private function assertFieldsExist(array $data)
|
private function assertFieldsExist(array $data)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
$fields = $this->resource->getSchema()->getFields();
|
||||||
|
|
||||||
foreach (['attributes', 'relationships'] as $location) {
|
foreach (['attributes', 'relationships'] as $location) {
|
||||||
foreach ($data[$location] as $name => $value) {
|
foreach ($data[$location] as $name => $value) {
|
||||||
if (! isset($schema->fields[$name])
|
if (! isset($fields[$name]) || $location !== $fields[$name]->getLocation()) {
|
||||||
|| $location !== $schema->fields[$name]->location
|
|
||||||
) {
|
|
||||||
throw new BadRequestException("Unknown field [$name]");
|
throw new BadRequestException("Unknown field [$name]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,52 +80,29 @@ trait SavesData
|
||||||
|
|
||||||
private function assertFieldsWritable(array $data, $model, Request $request)
|
private function assertFieldsWritable(array $data, $model, Request $request)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
|
if (has_value($data, $field) && ! evaluate($field->getWritable(), [$model, $request])) {
|
||||||
foreach ($schema->fields as $name => $field) {
|
throw new BadRequestException("Field [{$field->getName()}] is not writable");
|
||||||
$valueProvided = isset($data[$field->location][$name]);
|
|
||||||
|
|
||||||
if ($valueProvided && ! ($field->isWritable)($request, $model)) {
|
|
||||||
throw new BadRequestException("Field [$name] is not writable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fillDefaultValues(array &$data, Request $request)
|
|
||||||
{
|
|
||||||
$schema = $this->resource->getSchema();
|
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
|
||||||
$valueProvided = isset($data[$field->location][$name]);
|
|
||||||
|
|
||||||
if (! $valueProvided && $field->default) {
|
|
||||||
$data[$field->location][$name] = ($field->default)($request);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadRelatedResources(array &$data, Request $request)
|
private function loadRelatedResources(array &$data, Request $request)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
|
if (! $field instanceof Relationship || ! has_value($data, $field)) {
|
||||||
foreach ($schema->fields as $name => $field) {
|
|
||||||
if (! isset($data[$field->location][$name])) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = &$data[$field->location][$name];
|
$value = &get_value($data, $field);
|
||||||
|
|
||||||
if ($field instanceof Schema\Relationship) {
|
if (isset($value['data'])) {
|
||||||
$value = $value['data'];
|
if ($field instanceof HasOne) {
|
||||||
|
$value = $this->getModelForIdentifier($request, $value['data']);
|
||||||
if ($value) {
|
} elseif ($field instanceof HasMany) {
|
||||||
if ($field instanceof Schema\HasOne) {
|
$value = array_map(function ($identifier) use ($request) {
|
||||||
$value = $this->getModelForIdentifier($request, $value);
|
return $this->getModelForIdentifier($request, $identifier);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
}, $value['data']);
|
||||||
$value = array_map(function ($identifier) use ($request) {
|
|
||||||
return $this->getModelForIdentifier($request, $identifier);
|
|
||||||
}, $value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,22 +110,21 @@ trait SavesData
|
||||||
|
|
||||||
private function assertDataValid(array $data, $model, Request $request, bool $all): void
|
private function assertDataValid(array $data, $model, Request $request, bool $all): void
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
|
||||||
|
|
||||||
$failures = [];
|
$failures = [];
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
if (! $all && ! isset($data[$field->location][$name])) {
|
if (! $all && ! has_value($data, $field)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fail = function ($message) use (&$failures, $field) {
|
$fail = function ($message = null) use (&$failures, $field) {
|
||||||
$failures[] = compact('field', 'message');
|
$failures[] = compact('field', 'message');
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach ($field->validators as $validator) {
|
run_callbacks(
|
||||||
$validator($fail, $data[$field->location][$name] ?? null, $model, $request, $field);
|
$field->getListeners('validate'),
|
||||||
}
|
[$fail, get_value($data, $field), $model, $request, $field]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($failures)) {
|
if (count($failures)) {
|
||||||
|
|
@ -173,78 +132,81 @@ trait SavesData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyValues(array $data, $model, Request $request)
|
private function setValues(array $data, $model, Request $request)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
if (! isset($data[$field->location][$name])) {
|
if (! has_value($data, $field)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = $data[$field->location][$name];
|
$value = get_value($data, $field);
|
||||||
|
|
||||||
if ($field->setter || $field->saver) {
|
|
||||||
if ($field->setter) {
|
|
||||||
($field->setter)($request, $model, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if ($setter = $field->getSetter()) {
|
||||||
|
$setter($model, $value, $request);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field instanceof Schema\Attribute) {
|
if ($field->getSaver()) {
|
||||||
$adapter->applyAttribute($model, $field, $value);
|
continue;
|
||||||
} elseif ($field instanceof Schema\HasOne) {
|
}
|
||||||
$adapter->applyHasOne($model, $field, $value);
|
|
||||||
|
if ($field instanceof Attribute) {
|
||||||
|
$adapter->setAttribute($model, $field, $value);
|
||||||
|
} elseif ($field instanceof HasOne) {
|
||||||
|
$adapter->setHasOne($model, $field, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function save(array $data, $model, Request $request)
|
||||||
|
{
|
||||||
|
$this->saveModel($model, $request);
|
||||||
|
$this->saveFields($data, $model, $request);
|
||||||
|
}
|
||||||
|
|
||||||
private function saveModel($model, Request $request)
|
private function saveModel($model, Request $request)
|
||||||
{
|
{
|
||||||
$adapter = $this->resource->getAdapter();
|
if ($saver = $this->resource->getSchema()->getSaver()) {
|
||||||
$schema = $this->resource->getSchema();
|
$saver($model, $request);
|
||||||
|
|
||||||
if ($schema->saver) {
|
|
||||||
($schema->saver)($request, $model);
|
|
||||||
} else {
|
} else {
|
||||||
$adapter->save($model);
|
$this->resource->getAdapter()->save($model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveFields(array $data, $model, Request $request)
|
private function saveFields(array $data, $model, Request $request)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
if (! isset($data[$field->location][$name])) {
|
if (! has_value($data, $field)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = $data[$field->location][$name];
|
$value = get_value($data, $field);
|
||||||
|
|
||||||
if ($field->saver) {
|
if ($saver = $field->getSaver()) {
|
||||||
($field->saver)($request, $model, $value);
|
$saver($model, $value, $request);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
} elseif ($field instanceof HasMany) {
|
||||||
$adapter->saveHasMany($model, $field, $value);
|
$adapter->saveHasMany($model, $field, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->runSavedCallbacks($data, $model, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function runSavedCallbacks(array $data, $model, Request $request)
|
private function runSavedCallbacks(array $data, $model, Request $request)
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
|
if (! has_value($data, $field)) {
|
||||||
foreach ($schema->fields as $name => $field) {
|
|
||||||
if (! isset($data[$field->location][$name])) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($field->savedCallbacks as $callback) {
|
run_callbacks(
|
||||||
$callback($request, $model, $data[$field->location][$name]);
|
$field->getListeners('saved'),
|
||||||
}
|
[$model, get_value($data, $field), $request]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,13 @@ namespace Tobyz\JsonApiServer\Handler;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Tobyz\JsonApiServer\Api;
|
use function Tobyz\JsonApiServer\evaluate;
|
||||||
|
use function Tobyz\JsonApiServer\has_value;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
|
use function Tobyz\JsonApiServer\run_callbacks;
|
||||||
|
use function Tobyz\JsonApiServer\set_value;
|
||||||
|
|
||||||
class Create implements RequestHandlerInterface
|
class Create implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +20,7 @@ class Create implements RequestHandlerInterface
|
||||||
private $api;
|
private $api;
|
||||||
private $resource;
|
private $resource;
|
||||||
|
|
||||||
public function __construct(Api $api, ResourceType $resource)
|
public function __construct(JsonApi $api, ResourceType $resource)
|
||||||
{
|
{
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
|
|
@ -26,44 +30,43 @@ class Create implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
if (! ($schema->isCreatable)($request)) {
|
if (! evaluate($schema->getCreatable(), [$request])) {
|
||||||
throw new ForbiddenException('You cannot create this resource');
|
throw new ForbiddenException;
|
||||||
}
|
}
|
||||||
|
|
||||||
$model = $schema->createModel ? ($schema->createModel)($request) : $this->resource->getAdapter()->create();
|
$model = $this->createModel($request);
|
||||||
|
|
||||||
$data = $this->parseData($request->getParsedBody());
|
$data = $this->parseData($request->getParsedBody());
|
||||||
|
|
||||||
$adapter = $this->resource->getAdapter();
|
$this->validateFields($data, $model, $request);
|
||||||
|
|
||||||
$this->assertFieldsExist($data);
|
|
||||||
|
|
||||||
$this->assertFieldsWritable($data, $model, $request);
|
|
||||||
|
|
||||||
$this->fillDefaultValues($data, $request);
|
$this->fillDefaultValues($data, $request);
|
||||||
|
|
||||||
$this->loadRelatedResources($data, $request);
|
$this->loadRelatedResources($data, $request);
|
||||||
|
|
||||||
$this->assertDataValid($data, $model, $request, true);
|
$this->assertDataValid($data, $model, $request, true);
|
||||||
|
$this->setValues($data, $model, $request);
|
||||||
|
|
||||||
$this->applyValues($data, $model, $request);
|
run_callbacks($schema->getListeners('creating'), [$request, $model]);
|
||||||
|
|
||||||
foreach ($schema->creatingCallbacks as $callback) {
|
$this->save($data, $model, $request);
|
||||||
$callback($request, $model);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->saveModel($model, $request);
|
run_callbacks($schema->getListeners('created'), [$request, $model]);
|
||||||
|
|
||||||
$this->saveFields($data, $model, $request);
|
|
||||||
|
|
||||||
$this->runSavedCallbacks($data, $model, $request);
|
|
||||||
|
|
||||||
foreach ($schema->createdCallbacks as $callback) {
|
|
||||||
$callback($request, $model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (new Show($this->api, $this->resource, $model))
|
return (new Show($this->api, $this->resource, $model))
|
||||||
->handle($request)
|
->handle($request)
|
||||||
->withStatus(201);
|
->withStatus(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createModel(Request $request)
|
||||||
|
{
|
||||||
|
$creator = $this->resource->getSchema()->getCreator();
|
||||||
|
|
||||||
|
return $creator ? $creator($request) : $this->resource->getAdapter()->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fillDefaultValues(array &$data, Request $request)
|
||||||
|
{
|
||||||
|
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||||
|
if (! has_value($data, $field) && ($default = $field->getDefault())) {
|
||||||
|
set_value($data, $field, $default($request));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ namespace Tobyz\JsonApiServer\Handler;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use function Tobyz\JsonApiServer\evaluate;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
|
use function Tobyz\JsonApiServer\run_callbacks;
|
||||||
use Zend\Diactoros\Response\EmptyResponse;
|
use Zend\Diactoros\Response\EmptyResponse;
|
||||||
|
|
||||||
class Delete implements RequestHandlerInterface
|
class Delete implements RequestHandlerInterface
|
||||||
|
|
@ -24,19 +26,15 @@ class Delete implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
if (! ($schema->isDeletable)($request, $this->model)) {
|
if (! evaluate($schema->getDeletable(), [$request, $this->model])) {
|
||||||
throw new ForbiddenException('You cannot delete this resource');
|
throw new ForbiddenException;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($schema->deletingCallbacks as $callback) {
|
run_callbacks($schema->getListeners('deleting'), [$request, $this->model]);
|
||||||
$callback($request, $this->model);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->resource->getAdapter()->delete($this->model);
|
$this->resource->getAdapter()->delete($this->model);
|
||||||
|
|
||||||
foreach ($schema->deletedCallbacks as $callback) {
|
run_callbacks($schema->getListeners('deleted'), [$request, $this->model]);
|
||||||
$callback($request, $this->model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmptyResponse;
|
return new EmptyResponse;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,24 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Handler;
|
namespace Tobyz\JsonApiServer\Handler;
|
||||||
|
|
||||||
use JsonApiPhp\JsonApi;
|
use Closure;
|
||||||
use JsonApiPhp\JsonApi\Link;
|
use JsonApiPhp\JsonApi as Structure;
|
||||||
|
use JsonApiPhp\JsonApi\Link\LastLink;
|
||||||
|
use JsonApiPhp\JsonApi\Link\NextLink;
|
||||||
|
use JsonApiPhp\JsonApi\Link\PrevLink;
|
||||||
use JsonApiPhp\JsonApi\Meta;
|
use JsonApiPhp\JsonApi\Meta;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\JsonApiResponse;
|
use Tobyz\JsonApiServer\JsonApiResponse;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
use Tobyz\JsonApiServer\Schema;
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
|
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||||
|
use Tobyz\JsonApiServer\Schema\HasOne;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
use Tobyz\JsonApiServer\Serializer;
|
use Tobyz\JsonApiServer\Serializer;
|
||||||
|
|
||||||
class Index implements RequestHandlerInterface
|
class Index implements RequestHandlerInterface
|
||||||
|
|
@ -23,7 +29,7 @@ class Index implements RequestHandlerInterface
|
||||||
private $api;
|
private $api;
|
||||||
private $resource;
|
private $resource;
|
||||||
|
|
||||||
public function __construct(Api $api, ResourceType $resource)
|
public function __construct(JsonApi $api, ResourceType $resource)
|
||||||
{
|
{
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
|
|
@ -38,13 +44,9 @@ class Index implements RequestHandlerInterface
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
$schema = $this->resource->getSchema();
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
if (! ($schema->isVisible)($request)) {
|
|
||||||
throw new ForbiddenException('You cannot view this resource');
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = $adapter->query();
|
$query = $adapter->query();
|
||||||
|
|
||||||
foreach ($schema->scopes as $scope) {
|
foreach ($schema->getScopes() as $scope) {
|
||||||
$request = $scope($request, $query) ?: $request;
|
$request = $scope($request, $query) ?: $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,13 +60,13 @@ class Index implements RequestHandlerInterface
|
||||||
|
|
||||||
$paginationLinks = [];
|
$paginationLinks = [];
|
||||||
$members = [
|
$members = [
|
||||||
new Link\SelfLink($this->buildUrl($request)),
|
new Structure\Link\SelfLink($this->buildUrl($request)),
|
||||||
new Meta('offset', $offset),
|
new Structure\Meta('offset', $offset),
|
||||||
new Meta('limit', $limit),
|
new Structure\Meta('limit', $limit),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($offset > 0) {
|
if ($offset > 0) {
|
||||||
$paginationLinks[] = new Link\FirstLink($this->buildUrl($request, ['page' => ['offset' => 0]]));
|
$paginationLinks[] = new Structure\Link\FirstLink($this->buildUrl($request, ['page' => ['offset' => 0]]));
|
||||||
|
|
||||||
$prevOffset = $offset - $limit;
|
$prevOffset = $offset - $limit;
|
||||||
|
|
||||||
|
|
@ -74,16 +76,16 @@ class Index implements RequestHandlerInterface
|
||||||
$params = ['page' => ['offset' => max(0, $prevOffset)]];
|
$params = ['page' => ['offset' => max(0, $prevOffset)]];
|
||||||
}
|
}
|
||||||
|
|
||||||
$paginationLinks[] = new Link\PrevLink($this->buildUrl($request, $params));
|
$paginationLinks[] = new PrevLink($this->buildUrl($request, $params));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($schema->countable && $schema->paginate) {
|
if ($schema->isCountable() && $schema->getPaginate()) {
|
||||||
$total = $adapter->count($query);
|
$total = $adapter->count($query);
|
||||||
|
|
||||||
$members[] = new Meta('total', $total);
|
$members[] = new Meta('total', $total);
|
||||||
|
|
||||||
if ($offset + $limit < $total) {
|
if ($offset + $limit < $total) {
|
||||||
$paginationLinks[] = new Link\LastLink($this->buildUrl($request, ['page' => ['offset' => floor(($total - 1) / $limit) * $limit]]));
|
$paginationLinks[] = new LastLink($this->buildUrl($request, ['page' => ['offset' => floor(($total - 1) / $limit) * $limit]]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +98,7 @@ class Index implements RequestHandlerInterface
|
||||||
$models = $adapter->get($query);
|
$models = $adapter->get($query);
|
||||||
|
|
||||||
if ((count($models) && $total === null) || $offset + $limit < $total) {
|
if ((count($models) && $total === null) || $offset + $limit < $total) {
|
||||||
$paginationLinks[] = new Link\NextLink($this->buildUrl($request, ['page' => ['offset' => $offset + $limit]]));
|
$paginationLinks[] = new NextLink($this->buildUrl($request, ['page' => ['offset' => $offset + $limit]]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->loadRelationships($models, $include, $request);
|
$this->loadRelationships($models, $include, $request);
|
||||||
|
|
@ -108,12 +110,12 @@ class Index implements RequestHandlerInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonApiResponse(
|
return new JsonApiResponse(
|
||||||
new JsonApi\CompoundDocument(
|
new Structure\CompoundDocument(
|
||||||
new JsonApi\PaginatedCollection(
|
new Structure\PaginatedCollection(
|
||||||
new JsonApi\Pagination(...$paginationLinks),
|
new Structure\Pagination(...$paginationLinks),
|
||||||
new JsonApi\ResourceCollection(...$serializer->primary())
|
new Structure\ResourceCollection(...$serializer->primary())
|
||||||
),
|
),
|
||||||
new JsonApi\Included(...$serializer->included()),
|
new Structure\Included(...$serializer->included()),
|
||||||
...$members
|
...$members
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -149,7 +151,7 @@ class Index implements RequestHandlerInterface
|
||||||
|
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
|
|
||||||
$limit = $this->resource->getSchema()->paginate;
|
$limit = $this->resource->getSchema()->getPaginate();
|
||||||
|
|
||||||
if (isset($queryParams['page']['limit'])) {
|
if (isset($queryParams['page']['limit'])) {
|
||||||
$limit = $queryParams['page']['limit'];
|
$limit = $queryParams['page']['limit'];
|
||||||
|
|
@ -158,7 +160,7 @@ class Index implements RequestHandlerInterface
|
||||||
throw new BadRequestException('page[limit] must be a positive integer', 'page[limit]');
|
throw new BadRequestException('page[limit] must be a positive integer', 'page[limit]');
|
||||||
}
|
}
|
||||||
|
|
||||||
$limit = min($this->resource->getSchema()->limit, $limit);
|
$limit = min($this->resource->getSchema()->getLimit(), $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
$offset = 0;
|
$offset = 0;
|
||||||
|
|
@ -175,15 +177,16 @@ class Index implements RequestHandlerInterface
|
||||||
->withAttribute('jsonApiLimit', $limit)
|
->withAttribute('jsonApiLimit', $limit)
|
||||||
->withAttribute('jsonApiOffset', $offset);
|
->withAttribute('jsonApiOffset', $offset);
|
||||||
|
|
||||||
$sort = $queryParams['sort'] ?? $this->resource->getSchema()->defaultSort;
|
$sort = $queryParams['sort'] ?? $this->resource->getSchema()->getDefaultSort();
|
||||||
|
|
||||||
if ($sort) {
|
if ($sort) {
|
||||||
$sort = $this->parseSort($sort);
|
$sort = $this->parseSort($sort);
|
||||||
|
$fields = $schema->getFields();
|
||||||
|
|
||||||
foreach ($sort as $name => $direction) {
|
foreach ($sort as $name => $direction) {
|
||||||
if (! isset($schema->fields[$name])
|
if (! isset($fields[$name])
|
||||||
|| ! $schema->fields[$name] instanceof Schema\Attribute
|
|| ! $fields[$name] instanceof Attribute
|
||||||
|| ! $schema->fields[$name]->sortable
|
|| ! $fields[$name]->getSortable()
|
||||||
) {
|
) {
|
||||||
throw new BadRequestException("Invalid sort field [$name]", 'sort');
|
throw new BadRequestException("Invalid sort field [$name]", 'sort');
|
||||||
}
|
}
|
||||||
|
|
@ -199,8 +202,10 @@ class Index implements RequestHandlerInterface
|
||||||
throw new BadRequestException('filter must be an array', 'filter');
|
throw new BadRequestException('filter must be an array', 'filter');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fields = $schema->getFields();
|
||||||
|
|
||||||
foreach ($filter as $name => $value) {
|
foreach ($filter as $name => $value) {
|
||||||
if ($name !== 'id' && (! isset($schema->fields[$name]) || ! $schema->fields[$name]->filterable)) {
|
if ($name !== 'id' && (! isset($fields[$name]) || ! $fields[$name]->getFilterable())) {
|
||||||
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,10 +222,10 @@ class Index implements RequestHandlerInterface
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
|
|
||||||
foreach ($sort as $name => $direction) {
|
foreach ($sort as $name => $direction) {
|
||||||
$attribute = $schema->fields[$name];
|
$attribute = $schema->getFields()[$name];
|
||||||
|
|
||||||
if ($attribute->sorter) {
|
if (($sorter = $attribute->getSortable()) instanceof Closure) {
|
||||||
($attribute->sorter)($request, $query, $direction);
|
$sorter($query, $direction, $request);
|
||||||
} else {
|
} else {
|
||||||
$adapter->sortByAttribute($query, $attribute, $direction);
|
$adapter->sortByAttribute($query, $attribute, $direction);
|
||||||
}
|
}
|
||||||
|
|
@ -267,16 +272,16 @@ class Index implements RequestHandlerInterface
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$field = $schema->fields[$name];
|
$field = $schema->getFields()[$name];
|
||||||
|
|
||||||
if ($field->filter) {
|
if (($filter = $field->getFilterable()) instanceof Closure) {
|
||||||
($field->filter)($query, $value, $request);
|
$filter($query, $value, $request);
|
||||||
} elseif ($field instanceof Schema\Attribute) {
|
} elseif ($field instanceof Attribute) {
|
||||||
$adapter->filterByAttribute($query, $field, $value);
|
$adapter->filterByAttribute($query, $field, $value);
|
||||||
} elseif ($field instanceof Schema\HasOne) {
|
} elseif ($field instanceof HasOne) {
|
||||||
$value = explode(',', $value);
|
$value = explode(',', $value);
|
||||||
$adapter->filterByHasOne($query, $field, $value);
|
$adapter->filterByHasOne($query, $field, $value);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
} elseif ($field instanceof HasMany) {
|
||||||
$value = explode(',', $value);
|
$value = explode(',', $value);
|
||||||
$adapter->filterByHasMany($query, $field, $value);
|
$adapter->filterByHasMany($query, $field, $value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Handler;
|
namespace Tobyz\JsonApiServer\Handler;
|
||||||
|
|
||||||
use JsonApiPhp\JsonApi;
|
use JsonApiPhp\JsonApi\CompoundDocument;
|
||||||
|
use JsonApiPhp\JsonApi\Included;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
|
||||||
use Tobyz\JsonApiServer\JsonApiResponse;
|
use Tobyz\JsonApiServer\JsonApiResponse;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
use Tobyz\JsonApiServer\Serializer;
|
use Tobyz\JsonApiServer\Serializer;
|
||||||
|
|
@ -20,7 +20,7 @@ class Show implements RequestHandlerInterface
|
||||||
private $resource;
|
private $resource;
|
||||||
private $model;
|
private $model;
|
||||||
|
|
||||||
public function __construct(Api $api, ResourceType $resource, $model)
|
public function __construct(JsonApi $api, ResourceType $resource, $model)
|
||||||
{
|
{
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
|
|
@ -29,12 +29,6 @@ class Show implements RequestHandlerInterface
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
|
||||||
|
|
||||||
if (! ($schema->isVisible)($request)) {
|
|
||||||
throw new ForbiddenException('You cannot view this resource');
|
|
||||||
}
|
|
||||||
|
|
||||||
$include = $this->getInclude($request);
|
$include = $this->getInclude($request);
|
||||||
|
|
||||||
$this->loadRelationships([$this->model], $include, $request);
|
$this->loadRelationships([$this->model], $include, $request);
|
||||||
|
|
@ -44,9 +38,9 @@ class Show implements RequestHandlerInterface
|
||||||
$serializer->add($this->resource, $this->model, $include);
|
$serializer->add($this->resource, $this->model, $include);
|
||||||
|
|
||||||
return new JsonApiResponse(
|
return new JsonApiResponse(
|
||||||
new JsonApi\CompoundDocument(
|
new CompoundDocument(
|
||||||
$serializer->primary()[0],
|
$serializer->primary()[0],
|
||||||
new JsonApi\Included(...$serializer->included())
|
new Included(...$serializer->included())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ namespace Tobyz\JsonApiServer\Handler;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Tobyz\JsonApiServer\Api;
|
use function Tobyz\JsonApiServer\evaluate;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\ResourceType;
|
use Tobyz\JsonApiServer\ResourceType;
|
||||||
|
use function Tobyz\JsonApiServer\run_callbacks;
|
||||||
|
|
||||||
class Update implements RequestHandlerInterface
|
class Update implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +19,7 @@ class Update implements RequestHandlerInterface
|
||||||
private $resource;
|
private $resource;
|
||||||
private $model;
|
private $model;
|
||||||
|
|
||||||
public function __construct(Api $api, ResourceType $resource, $model)
|
public function __construct(JsonApi $api, ResourceType $resource, $model)
|
||||||
{
|
{
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
|
|
@ -28,19 +30,22 @@ class Update implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
$schema = $this->resource->getSchema();
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
if (! ($schema->isUpdatable)($request, $this->model)) {
|
if (! evaluate($schema->getUpdatable(), [$request, $this->model])) {
|
||||||
throw new ForbiddenException('You cannot update this resource');
|
throw new ForbiddenException;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($schema->updatingCallbacks as $callback) {
|
$data = $this->parseData($request->getParsedBody());
|
||||||
$callback($request, $this->model);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->save($this->model, $request);
|
$this->validateFields($data, $this->model, $request);
|
||||||
|
$this->loadRelatedResources($data, $request);
|
||||||
|
$this->assertDataValid($data, $this->model, $request, false);
|
||||||
|
$this->setValues($data, $this->model, $request);
|
||||||
|
|
||||||
foreach ($schema->updatedCallbacks as $callback) {
|
run_callbacks($schema->getListeners('updating'), [$request, $this->model]);
|
||||||
$callback($request, $this->model);
|
|
||||||
}
|
$this->save($data, $this->model, $request);
|
||||||
|
|
||||||
|
run_callbacks($schema->getListeners('updated'), [$request, $this->model]);
|
||||||
|
|
||||||
return (new Show($this->api, $this->resource, $this->model))->handle($request);
|
return (new Show($this->api, $this->resource, $this->model))->handle($request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Http;
|
||||||
|
|
||||||
|
class MediaTypes
|
||||||
|
{
|
||||||
|
private $value;
|
||||||
|
|
||||||
|
public function __construct(string $value)
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the list contains the given type without modifications
|
||||||
|
*
|
||||||
|
* This is meant to ease implementation of JSON:API rules for content
|
||||||
|
* negotiation, which demand HTTP error responses e.g. when all of the
|
||||||
|
* JSON:API media types in the "Accept" header are modified with "media type
|
||||||
|
* parameters". Therefore, this method only returns true when the requested
|
||||||
|
* media type is contained without additional parameters (except for the
|
||||||
|
* weight parameter "q" and "Accept extension parameters").
|
||||||
|
*
|
||||||
|
* @param string $mediaType
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function containsExactly(string $mediaType): bool
|
||||||
|
{
|
||||||
|
$types = array_map('trim', explode(',', $this->value));
|
||||||
|
|
||||||
|
// Accept headers can contain multiple media types, so we need to check
|
||||||
|
// whether any of them matches.
|
||||||
|
foreach ($types as $type) {
|
||||||
|
$parts = array_map('trim', explode(';', $type));
|
||||||
|
|
||||||
|
// The actual media type needs to be an exact match
|
||||||
|
if (array_shift($parts) !== $mediaType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The media type can optionally be followed by "media type
|
||||||
|
// parameters". Parameters after the "q" parameter are considered
|
||||||
|
// "Accept extension parameters", which we don't care about. Thus,
|
||||||
|
// we have an exact match if there are no parameters at all or if
|
||||||
|
// the first one is named "q".
|
||||||
|
// See https://tools.ietf.org/html/rfc7231#section-5.3.2.
|
||||||
|
if (empty($parts) || substr($parts[0], 0, 2) === 'q=') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use JsonApiPhp\JsonApi\ErrorDocument;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\InternalServerErrorException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\NotAcceptableException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\NotImplementedException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
|
||||||
|
use Tobyz\JsonApiServer\Handler\Concerns\FindsResources;
|
||||||
|
use Tobyz\JsonApiServer\Http\MediaTypes;
|
||||||
|
|
||||||
|
final class JsonApi implements RequestHandlerInterface
|
||||||
|
{
|
||||||
|
const CONTENT_TYPE = 'application/vnd.api+json';
|
||||||
|
|
||||||
|
use FindsResources;
|
||||||
|
|
||||||
|
private $resources = [];
|
||||||
|
private $baseUrl;
|
||||||
|
|
||||||
|
public function __construct(string $baseUrl)
|
||||||
|
{
|
||||||
|
$this->baseUrl = $baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resource(string $type, $adapter, Closure $buildSchema = null): void
|
||||||
|
{
|
||||||
|
$this->resources[$type] = new ResourceType($type, $adapter, $buildSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResources(): array
|
||||||
|
{
|
||||||
|
return $this->resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResource(string $type): ResourceType
|
||||||
|
{
|
||||||
|
if (! isset($this->resources[$type])) {
|
||||||
|
throw new ResourceNotFoundException($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->resources[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Request $request): Response
|
||||||
|
{
|
||||||
|
$this->validateRequest($request);
|
||||||
|
|
||||||
|
$path = $this->stripBasePath(
|
||||||
|
$request->getUri()->getPath()
|
||||||
|
);
|
||||||
|
|
||||||
|
$segments = explode('/', trim($path, '/'));
|
||||||
|
|
||||||
|
switch (count($segments)) {
|
||||||
|
case 1:
|
||||||
|
return $this->handleWithHandler(
|
||||||
|
$request,
|
||||||
|
$this->getCollectionHandler($request, $segments)
|
||||||
|
);
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return $this->handleWithHandler(
|
||||||
|
$request,
|
||||||
|
$this->getMemberHandler($request, $segments)
|
||||||
|
);
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
// return $this->handleRelated($request, $resource, $model, $segments[2]);
|
||||||
|
throw new NotImplementedException;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
if ($segments[2] === 'relationships') {
|
||||||
|
// return $this->handleRelationship($request, $resource, $model, $segments[3]);
|
||||||
|
throw new NotImplementedException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateRequest(Request $request): void
|
||||||
|
{
|
||||||
|
$this->validateRequestContentType($request);
|
||||||
|
$this->validateRequestAccepts($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateRequestContentType(Request $request): void
|
||||||
|
{
|
||||||
|
$header = $request->getHeaderLine('Content-Type');
|
||||||
|
|
||||||
|
if (empty($header)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((new MediaTypes($header))->containsExactly(self::CONTENT_TYPE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedMediaTypeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateRequestAccepts(Request $request): void
|
||||||
|
{
|
||||||
|
$header = $request->getHeaderLine('Accept');
|
||||||
|
|
||||||
|
if (empty($header)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((new MediaTypes($header))->containsExactly(self::CONTENT_TYPE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotAcceptableException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stripBasePath(string $path): string
|
||||||
|
{
|
||||||
|
$basePath = parse_url($this->baseUrl, PHP_URL_PATH);
|
||||||
|
|
||||||
|
$len = strlen($basePath);
|
||||||
|
|
||||||
|
if (substr($path, 0, $len) === $basePath) {
|
||||||
|
$path = substr($path, $len + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCollectionHandler(Request $request, array $segments): RequestHandlerInterface
|
||||||
|
{
|
||||||
|
$resource = $this->getResource($segments[0]);
|
||||||
|
|
||||||
|
switch ($request->getMethod()) {
|
||||||
|
case 'GET':
|
||||||
|
return new Handler\Index($this, $resource);
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
return new Handler\Create($this, $resource);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new MethodNotAllowedException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMemberHandler(Request $request, array $segments): RequestHandlerInterface
|
||||||
|
{
|
||||||
|
$resource = $this->getResource($segments[0]);
|
||||||
|
$model = $this->findResource($request, $resource, $segments[1]);
|
||||||
|
|
||||||
|
switch ($request->getMethod()) {
|
||||||
|
case 'PATCH':
|
||||||
|
return new Handler\Update($this, $resource, $model);
|
||||||
|
|
||||||
|
case 'GET':
|
||||||
|
return new Handler\Show($this, $resource, $model);
|
||||||
|
|
||||||
|
case 'DELETE':
|
||||||
|
return new Handler\Delete($resource, $model);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new MethodNotAllowedException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleWithHandler(Request $request, RequestHandlerInterface $handler)
|
||||||
|
{
|
||||||
|
$request = $request->withAttribute('jsonApiHandler', $handler);
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function error($e)
|
||||||
|
{
|
||||||
|
if (! $e instanceof ErrorProviderInterface) {
|
||||||
|
$e = new InternalServerErrorException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $e->getJsonApiErrors();
|
||||||
|
$status = $e->getJsonApiStatus();
|
||||||
|
|
||||||
|
$data = new ErrorDocument(
|
||||||
|
...$errors
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonApiResponse($data, $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseUrl(): string
|
||||||
|
{
|
||||||
|
return $this->baseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ class JsonApiResponse extends JsonResponse
|
||||||
array $headers = [],
|
array $headers = [],
|
||||||
$encodingOptions = self::DEFAULT_JSON_FLAGS
|
$encodingOptions = self::DEFAULT_JSON_FLAGS
|
||||||
) {
|
) {
|
||||||
$headers['content-type'] = 'application/vnd.api+json';
|
$headers['content-type'] = JsonApi::CONTENT_TYPE;
|
||||||
|
|
||||||
parent::__construct($data, $status, $headers, $encodingOptions);
|
parent::__construct($data, $status, $headers, $encodingOptions);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ namespace Tobyz\JsonApiServer;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
|
||||||
class ResourceType
|
final class ResourceType
|
||||||
{
|
{
|
||||||
protected $type;
|
private $type;
|
||||||
protected $adapter;
|
private $adapter;
|
||||||
protected $buildSchema;
|
private $buildSchema;
|
||||||
protected $schema;
|
private $schema;
|
||||||
|
|
||||||
public function __construct(string $type, AdapterInterface $adapter, Closure $buildSchema = null)
|
public function __construct(string $type, AdapterInterface $adapter, Closure $buildSchema = null)
|
||||||
{
|
{
|
||||||
|
|
@ -30,10 +30,10 @@ class ResourceType
|
||||||
return $this->adapter;
|
return $this->adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSchema(): Builder
|
public function getSchema(): Type
|
||||||
{
|
{
|
||||||
if (! $this->schema) {
|
if (! $this->schema) {
|
||||||
$this->schema = new Builder;
|
$this->schema = new Type;
|
||||||
|
|
||||||
if ($this->buildSchema) {
|
if ($this->buildSchema) {
|
||||||
($this->buildSchema)($this->schema);
|
($this->buildSchema)($this->schema);
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,31 @@ namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
|
||||||
class Attribute extends Field
|
final class Attribute extends Field
|
||||||
{
|
{
|
||||||
public $location = 'attributes';
|
private $sortable = false;
|
||||||
public $sortable = false;
|
|
||||||
public $sorter;
|
|
||||||
|
|
||||||
public function sortable(Closure $callback = null)
|
public function sortable(Closure $callback = null)
|
||||||
{
|
{
|
||||||
$this->sortable = true;
|
$this->sortable = $callback ?: true;
|
||||||
$this->sorter = $callback;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function notSortable()
|
||||||
|
{
|
||||||
|
$this->sortable = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSortable()
|
||||||
|
{
|
||||||
|
return $this->sortable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocation(): string
|
||||||
|
{
|
||||||
|
return 'attributes';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,241 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
|
|
||||||
class Builder
|
|
||||||
{
|
|
||||||
public $fields = [];
|
|
||||||
public $meta = [];
|
|
||||||
public $paginate = 20;
|
|
||||||
public $limit = 50;
|
|
||||||
public $countable = true;
|
|
||||||
public $scopes = [];
|
|
||||||
public $isCreatable;
|
|
||||||
public $creatingCallbacks = [];
|
|
||||||
public $createdCallbacks = [];
|
|
||||||
public $isUpdatable;
|
|
||||||
public $updatingCallbacks = [];
|
|
||||||
public $updatedCallbacks = [];
|
|
||||||
public $isDeletable;
|
|
||||||
public $deletingCallbacks = [];
|
|
||||||
public $deletedCallbacks = [];
|
|
||||||
public $defaultSort;
|
|
||||||
public $createModel;
|
|
||||||
public $saver;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->visible();
|
|
||||||
$this->notCreatable();
|
|
||||||
$this->notUpdatable();
|
|
||||||
$this->notDeletable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function attribute(string $name, string $property = null): Attribute
|
|
||||||
{
|
|
||||||
return $this->field(Attribute::class, $name, $property);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasOne(string $name, $resource = null, string $property = null): HasOne
|
|
||||||
{
|
|
||||||
$field = $this->field(HasOne::class, $name, $property);
|
|
||||||
|
|
||||||
if ($resource) {
|
|
||||||
$field->resource($resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $field;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function morphOne(string $name, string $property = null): HasOne
|
|
||||||
{
|
|
||||||
return $this->field(HasOne::class, $name, $property)->resource(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasMany(string $name, $resource = null, string $property = null): HasMany
|
|
||||||
{
|
|
||||||
$field = $this->field(HasMany::class, $name, $property);
|
|
||||||
|
|
||||||
if ($resource) {
|
|
||||||
$field->resource($resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $field;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function meta(string $name, $value)
|
|
||||||
{
|
|
||||||
return $this->meta[$name] = new Meta($name, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paginate(?int $perPage)
|
|
||||||
{
|
|
||||||
$this->paginate = $perPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function limit(?int $limit)
|
|
||||||
{
|
|
||||||
$this->limit = $limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function countable()
|
|
||||||
{
|
|
||||||
$this->countable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uncountable()
|
|
||||||
{
|
|
||||||
$this->countable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createModel(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->createModel = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scope(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->scopes[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function creatableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
$this->isCreatable = $condition;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function creatable()
|
|
||||||
{
|
|
||||||
return $this->creatableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notCreatableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
return $this->creatableIf(function (...$args) use ($condition) {
|
|
||||||
return ! $condition(...$args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notCreatable()
|
|
||||||
{
|
|
||||||
return $this->notCreatableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function creating(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->creatingCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function created(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->createdCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
$this->isUpdatable = $condition;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatable()
|
|
||||||
{
|
|
||||||
return $this->updatableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notUpdatableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
return $this->updatableIf(function (...$args) use ($condition) {
|
|
||||||
return ! $condition(...$args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notUpdatable()
|
|
||||||
{
|
|
||||||
return $this->notUpdatableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updating(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->updatingCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updated(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->updatedCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function save(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->saver = $callback;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deletableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
$this->isDeletable = $condition;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deletable()
|
|
||||||
{
|
|
||||||
return $this->deletableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notDeletableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
return $this->deletableIf(function (...$args) use ($condition) {
|
|
||||||
return ! $condition(...$args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notDeletable()
|
|
||||||
{
|
|
||||||
return $this->notDeletableIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleting(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->deletingCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deleted(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->deletedCallbacks[] = $callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function defaultSort(string $sort)
|
|
||||||
{
|
|
||||||
$this->defaultSort = $sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function field(string $class, string $name, string $property = null)
|
|
||||||
{
|
|
||||||
if (! isset($this->fields[$name]) || ! $this->fields[$name] instanceof $class) {
|
|
||||||
$this->fields[$name] = new $class($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($property) {
|
|
||||||
$this->fields[$name]->property($property);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->fields[$name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?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\Schema\Concerns;
|
||||||
|
|
||||||
|
trait HasListeners
|
||||||
|
{
|
||||||
|
private $listeners = [];
|
||||||
|
|
||||||
|
public function getListeners(string $event)
|
||||||
|
{
|
||||||
|
return $this->listeners[$event] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,30 +3,31 @@
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use function Tobyz\JsonApiServer\negate;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
||||||
|
use function Tobyz\JsonApiServer\wrap;
|
||||||
|
|
||||||
abstract class Field
|
abstract class Field
|
||||||
{
|
{
|
||||||
public $name;
|
use HasListeners;
|
||||||
public $property;
|
|
||||||
public $isVisible;
|
private $name;
|
||||||
public $isWritable;
|
private $property;
|
||||||
public $getter;
|
private $visible = true;
|
||||||
public $setter;
|
private $writable = false;
|
||||||
public $saver;
|
private $getter;
|
||||||
public $savedCallbacks = [];
|
private $setter;
|
||||||
public $default;
|
private $saver;
|
||||||
public $validators = [];
|
private $default;
|
||||||
public $filterable = false;
|
private $filterable = false;
|
||||||
public $filter;
|
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
|
|
||||||
$this->visible();
|
|
||||||
$this->readonly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract public function getLocation(): string;
|
||||||
|
|
||||||
public function property(string $property)
|
public function property(string $property)
|
||||||
{
|
{
|
||||||
$this->property = $property;
|
$this->property = $property;
|
||||||
|
|
@ -34,65 +35,37 @@ abstract class Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function visibleIf(Closure $condition)
|
public function visible(Closure $condition = null)
|
||||||
{
|
{
|
||||||
$this->isVisible = $condition;
|
$this->visible = $condition ?: true;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function visible()
|
public function hidden(Closure $condition = null)
|
||||||
{
|
{
|
||||||
return $this->visibleIf(function () {
|
$this->visible = $condition ? negate($condition) : false;
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hiddenIf(Closure $condition)
|
|
||||||
{
|
|
||||||
return $this->visibleIf(function (...$args) use ($condition) {
|
|
||||||
return ! $condition(...$args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hidden()
|
|
||||||
{
|
|
||||||
return $this->hiddenIf(function () {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function writableIf(Closure $condition)
|
|
||||||
{
|
|
||||||
$this->isWritable = $condition;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function writable()
|
public function writable(Closure $condition = null)
|
||||||
{
|
{
|
||||||
return $this->writableIf(function () {
|
$this->writable = $condition ?: true;
|
||||||
return true;
|
|
||||||
});
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function readonlyIf(Closure $condition)
|
public function readonly(Closure $condition = null)
|
||||||
{
|
{
|
||||||
return $this->writableIf(function (...$args) use ($condition) {
|
$this->writable = $condition ? negate($condition) : false;
|
||||||
return ! $condition(...$args);
|
|
||||||
});
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function readonly()
|
public function get($value)
|
||||||
{
|
{
|
||||||
return $this->readonlyIf(function () {
|
$this->getter = wrap($value);
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get($callback)
|
|
||||||
{
|
|
||||||
$this->getter = $this->wrap($callback);
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
@ -113,41 +86,86 @@ abstract class Field
|
||||||
|
|
||||||
public function saved(Closure $callback)
|
public function saved(Closure $callback)
|
||||||
{
|
{
|
||||||
$this->savedCallbacks[] = $callback;
|
$this->listeners['saved'][] = $callback;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function default($value)
|
public function default($value)
|
||||||
{
|
{
|
||||||
$this->default = $this->wrap($value);
|
$this->default = wrap($value);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validate(Closure $callback)
|
public function validate(Closure $callback)
|
||||||
{
|
{
|
||||||
$this->validators[] = $callback;
|
$this->listeners['validate'][] = $callback;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterable(Closure $callback = null)
|
public function filterable(Closure $callback = null)
|
||||||
{
|
{
|
||||||
$this->filterable = true;
|
$this->filterable = $callback ?: true;
|
||||||
$this->filter = $callback;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function wrap($value)
|
public function notFilterable()
|
||||||
{
|
{
|
||||||
if (! $value instanceof Closure) {
|
$this->filterable = false;
|
||||||
$value = function () use ($value) {
|
|
||||||
return $value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProperty()
|
||||||
|
{
|
||||||
|
return $this->property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool|Closure
|
||||||
|
*/
|
||||||
|
public function getVisible()
|
||||||
|
{
|
||||||
|
return $this->visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWritable()
|
||||||
|
{
|
||||||
|
return $this->writable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGetter()
|
||||||
|
{
|
||||||
|
return $this->getter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSetter()
|
||||||
|
{
|
||||||
|
return $this->setter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSaver()
|
||||||
|
{
|
||||||
|
return $this->saver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefault()
|
||||||
|
{
|
||||||
|
return $this->default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilterable()
|
||||||
|
{
|
||||||
|
return $this->filterable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
class HasMany extends Relationship
|
final class HasMany extends Relationship
|
||||||
{
|
{
|
||||||
public $includable = false;
|
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
{
|
{
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
|
|
||||||
$this->resource = $name;
|
$this->type($name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Doctrine\Common\Inflector\Inflector;
|
use Doctrine\Common\Inflector\Inflector;
|
||||||
|
|
||||||
class HasOne extends Relationship
|
final class HasOne extends Relationship
|
||||||
{
|
{
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
{
|
{
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
|
|
||||||
$this->resource = Inflector::pluralize($name);
|
$this->type(Inflector::pluralize($name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,26 @@
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use function Tobyz\JsonApiServer\wrap;
|
||||||
|
|
||||||
class Meta
|
final class Meta
|
||||||
{
|
{
|
||||||
public $name;
|
private $name;
|
||||||
public $value;
|
private $value;
|
||||||
|
|
||||||
public function __construct(string $name, $value)
|
public function __construct(string $name, $value)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->value = $this->wrap($value);
|
$this->value = wrap($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function wrap($value)
|
public function getName(): string
|
||||||
{
|
{
|
||||||
if (! $value instanceof Closure) {
|
return $this->name;
|
||||||
$value = function () use ($value) {
|
}
|
||||||
return $value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
public function getValue(): Closure
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,47 @@
|
||||||
namespace Tobyz\JsonApiServer\Schema;
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Tobyz\JsonApiServer\Handler\Show;
|
use function Tobyz\JsonApiServer\negate;
|
||||||
|
|
||||||
abstract class Relationship extends Field
|
abstract class Relationship extends Field
|
||||||
{
|
{
|
||||||
public $location = 'relationships';
|
private $type;
|
||||||
public $linkage;
|
private $linkage = false;
|
||||||
public $hasLinks = true;
|
private $links = true;
|
||||||
public $loadable = true;
|
private $loadable = true;
|
||||||
public $loader;
|
private $includable = false;
|
||||||
public $included = false;
|
|
||||||
public $includable = true;
|
|
||||||
public $resource;
|
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function type($type)
|
||||||
{
|
{
|
||||||
parent::__construct($name);
|
$this->type = $type;
|
||||||
|
|
||||||
$this->noLinkage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function resource($resource)
|
|
||||||
{
|
|
||||||
$this->resource = $resource;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function linkageIf(Closure $condition)
|
public function polymorphic()
|
||||||
{
|
{
|
||||||
$this->linkage = $condition;
|
$this->type = null;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function linkage()
|
public function linkage(Closure $condition = null)
|
||||||
{
|
{
|
||||||
return $this->linkageIf(function () {
|
$this->linkage = $condition ?: true;
|
||||||
return true;
|
|
||||||
});
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function noLinkage()
|
public function noLinkage(Closure $condition = null)
|
||||||
{
|
{
|
||||||
return $this->linkageIf(function () {
|
$this->linkage = $condition ? negate($condition) : false;
|
||||||
return false;
|
|
||||||
});
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadable()
|
public function loadable(Closure $callback = null)
|
||||||
{
|
{
|
||||||
$this->loadable = true;
|
$this->loadable = $callback ?: true;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
@ -65,13 +55,6 @@ abstract class Relationship extends Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(Closure $callback)
|
|
||||||
{
|
|
||||||
$this->loader = $callback;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function includable()
|
public function includable()
|
||||||
{
|
{
|
||||||
$this->includable = true;
|
$this->includable = true;
|
||||||
|
|
@ -86,10 +69,47 @@ abstract class Relationship extends Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function noLinks()
|
public function getType()
|
||||||
{
|
{
|
||||||
$this->hasLinks = false;
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function links()
|
||||||
|
{
|
||||||
|
$this->links = true;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function noLinks()
|
||||||
|
{
|
||||||
|
$this->links = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLinkage()
|
||||||
|
{
|
||||||
|
return $this->linkage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasLinks(): bool
|
||||||
|
{
|
||||||
|
return $this->links;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLoadable()
|
||||||
|
{
|
||||||
|
return $this->loadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isIncludable(): bool
|
||||||
|
{
|
||||||
|
return $this->includable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLocation(): string
|
||||||
|
{
|
||||||
|
return 'relationships';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer\Schema;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use function Tobyz\JsonApiServer\negate;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
||||||
|
|
||||||
|
final class Type
|
||||||
|
{
|
||||||
|
use HasListeners;
|
||||||
|
|
||||||
|
private $fields = [];
|
||||||
|
private $meta = [];
|
||||||
|
private $paginate = 20;
|
||||||
|
private $limit = 50;
|
||||||
|
private $countable = true;
|
||||||
|
private $defaultSort;
|
||||||
|
private $scopes = [];
|
||||||
|
private $saver;
|
||||||
|
private $creatable = false;
|
||||||
|
private $create;
|
||||||
|
private $updatable = false;
|
||||||
|
private $deletable = false;
|
||||||
|
private $delete;
|
||||||
|
|
||||||
|
public function attribute(string $name): Attribute
|
||||||
|
{
|
||||||
|
return $this->field(Attribute::class, $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasOne(string $name): HasOne
|
||||||
|
{
|
||||||
|
return $this->field(HasOne::class, $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasMany(string $name): HasMany
|
||||||
|
{
|
||||||
|
return $this->field(HasMany::class, $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function field(string $class, string $name)
|
||||||
|
{
|
||||||
|
if (! isset($this->fields[$name]) || ! $this->fields[$name] instanceof $class) {
|
||||||
|
$this->fields[$name] = new $class($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fields[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeField(string $name)
|
||||||
|
{
|
||||||
|
unset($this->fields[$name]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Field[]
|
||||||
|
*/
|
||||||
|
public function getFields(): array
|
||||||
|
{
|
||||||
|
return $this->fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function meta(string $name, $value)
|
||||||
|
{
|
||||||
|
return $this->meta[$name] = new Meta($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeMeta(string $name)
|
||||||
|
{
|
||||||
|
unset($this->meta[$name]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMeta(): array
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paginate(int $perPage)
|
||||||
|
{
|
||||||
|
$this->paginate = $perPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dontPaginate()
|
||||||
|
{
|
||||||
|
$this->paginate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPaginate(): int
|
||||||
|
{
|
||||||
|
return $this->paginate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function limit(int $limit)
|
||||||
|
{
|
||||||
|
$this->limit = $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function noLimit()
|
||||||
|
{
|
||||||
|
$this->limit = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLimit(): int
|
||||||
|
{
|
||||||
|
return $this->limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countable()
|
||||||
|
{
|
||||||
|
$this->countable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uncountable()
|
||||||
|
{
|
||||||
|
$this->countable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCountable(): bool
|
||||||
|
{
|
||||||
|
return $this->countable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scope(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->scopes[] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScopes(): array
|
||||||
|
{
|
||||||
|
return $this->scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(?Closure $callback)
|
||||||
|
{
|
||||||
|
$this->create = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreator()
|
||||||
|
{
|
||||||
|
return $this->create;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creatable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->creatable = $condition ?: true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notCreatable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->creatable = $condition ? negate($condition) : false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatable()
|
||||||
|
{
|
||||||
|
return $this->creatable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creating(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['creating'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function created(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['created'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->updatable = $condition ?: true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notUpdatable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->updatable = $condition ? negate($condition) : false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUpdatable()
|
||||||
|
{
|
||||||
|
return $this->updatable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updating(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['updating'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['updated'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(?Closure $callback)
|
||||||
|
{
|
||||||
|
$this->saver = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSaver()
|
||||||
|
{
|
||||||
|
return $this->saver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deletable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->deletable = $condition ?: true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notDeletable(Closure $condition = null)
|
||||||
|
{
|
||||||
|
$this->deletable = $condition ? negate($condition) : false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeletable()
|
||||||
|
{
|
||||||
|
return $this->deletable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(?Closure $callback)
|
||||||
|
{
|
||||||
|
$this->delete = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDelete()
|
||||||
|
{
|
||||||
|
return $this->delete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleting(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['deleting'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->listeners['deleted'][] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultSort(?string $sort)
|
||||||
|
{
|
||||||
|
$this->defaultSort = $sort;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultSort()
|
||||||
|
{
|
||||||
|
return $this->defaultSort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,18 @@ namespace Tobyz\JsonApiServer;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use JsonApiPhp\JsonApi;
|
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 Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Relationship;
|
||||||
|
|
||||||
class Serializer
|
class Serializer
|
||||||
{
|
{
|
||||||
|
|
@ -15,7 +24,7 @@ class Serializer
|
||||||
protected $map = [];
|
protected $map = [];
|
||||||
protected $primary = [];
|
protected $primary = [];
|
||||||
|
|
||||||
public function __construct(Api $api, Request $request)
|
public function __construct(JsonApi $api, Request $request)
|
||||||
{
|
{
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
|
|
@ -43,7 +52,7 @@ class Serializer
|
||||||
|
|
||||||
$resourceUrl = $this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id'];
|
$resourceUrl = $this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id'];
|
||||||
|
|
||||||
$fields = $schema->fields;
|
$fields = $schema->getFields();
|
||||||
|
|
||||||
$queryParams = $this->request->getQueryParams();
|
$queryParams = $this->request->getQueryParams();
|
||||||
|
|
||||||
|
|
@ -60,7 +69,7 @@ class Serializer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! ($field->isVisible)($this->request, $model)) {
|
if (! evaluate($field->getVisible(), [$model, $this->request])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +77,7 @@ class Serializer
|
||||||
$value = $this->attribute($field, $model, $adapter);
|
$value = $this->attribute($field, $model, $adapter);
|
||||||
} elseif ($field instanceof Schema\Relationship) {
|
} elseif ($field instanceof Schema\Relationship) {
|
||||||
$isIncluded = isset($include[$name]);
|
$isIncluded = isset($include[$name]);
|
||||||
$isLinkage = ($field->linkage)($this->request);
|
$isLinkage = evaluate($field->getLinkage(), [$this->request]);
|
||||||
|
|
||||||
if (! $isIncluded && ! $isLinkage) {
|
if (! $isIncluded && ! $isLinkage) {
|
||||||
$value = $this->emptyRelationship($field, $resourceUrl);
|
$value = $this->emptyRelationship($field, $resourceUrl);
|
||||||
|
|
@ -82,12 +91,14 @@ class Serializer
|
||||||
$data['fields'][$name] = $value;
|
$data['fields'][$name] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['links']['self'] = new JsonApi\Link\SelfLink($resourceUrl);
|
$data['links']['self'] = new SelfLink($resourceUrl);
|
||||||
|
|
||||||
ksort($schema->meta);
|
$metas = $schema->getMeta();
|
||||||
|
|
||||||
foreach ($schema->meta as $name => $meta) {
|
ksort($metas);
|
||||||
$data['meta'][$name] = new JsonApi\Meta($meta->name, ($meta->value)($this->request, $model));
|
|
||||||
|
foreach ($metas as $name => $meta) {
|
||||||
|
$data['meta'][$name] = new Structure\Meta($meta->name, ($meta->value)($this->request, $model));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->merge($data);
|
$this->merge($data);
|
||||||
|
|
@ -95,10 +106,10 @@ class Serializer
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function attribute(Schema\Attribute $field, $model, AdapterInterface $adapter): JsonApi\Attribute
|
private function attribute(Attribute $field, $model, AdapterInterface $adapter): Structure\Attribute
|
||||||
{
|
{
|
||||||
if ($field->getter) {
|
if ($getter = $field->getGetter()) {
|
||||||
$value = ($field->getter)($this->request, $model);
|
$value = $getter($model, $this->request);
|
||||||
} else {
|
} else {
|
||||||
$value = $adapter->getAttribute($model, $field);
|
$value = $adapter->getAttribute($model, $field);
|
||||||
}
|
}
|
||||||
|
|
@ -107,18 +118,18 @@ class Serializer
|
||||||
$value = $value->format(DateTime::RFC3339);
|
$value = $value->format(DateTime::RFC3339);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonApi\Attribute($field->name, $value);
|
return new Structure\Attribute($field->getName(), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||||
{
|
{
|
||||||
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
||||||
|
|
||||||
$value = $isIncluded ? ($field->getter ? ($field->getter)($this->request, $model) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->loadable ? $adapter->getHasOneId($model, $field) : null);
|
$value = $isIncluded ? (($getter = $field->getGetter()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->getLoadable() ? $adapter->getHasOneId($model, $field) : null);
|
||||||
|
|
||||||
if (! $value) {
|
if (! $value) {
|
||||||
return new JsonApi\ToNull(
|
return new Structure\ToNull(
|
||||||
$field->name,
|
$field->getName(),
|
||||||
...$links
|
...$links
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -130,8 +141,8 @@ class Serializer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new JsonApi\ToOne(
|
return new ToOne(
|
||||||
$field->name,
|
$field->getName(),
|
||||||
$identifier,
|
$identifier,
|
||||||
...$links
|
...$links
|
||||||
);
|
);
|
||||||
|
|
@ -139,8 +150,8 @@ class Serializer
|
||||||
|
|
||||||
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||||
{
|
{
|
||||||
if ($field->getter) {
|
if ($getter = $field->getGetter()) {
|
||||||
$value = ($field->getter)($this->request, $model);
|
$value = $getter($model, $this->request);
|
||||||
} else {
|
} else {
|
||||||
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field) : null;
|
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field) : null;
|
||||||
}
|
}
|
||||||
|
|
@ -157,36 +168,36 @@ class Serializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonApi\ToMany(
|
return new ToMany(
|
||||||
$field->name,
|
$field->getName(),
|
||||||
new JsonApi\ResourceIdentifierCollection(...$identifiers),
|
new ResourceIdentifierCollection(...$identifiers),
|
||||||
...$this->getRelationshipLinks($field, $resourceUrl)
|
...$this->getRelationshipLinks($field, $resourceUrl)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function emptyRelationship(Schema\Relationship $field, string $resourceUrl): JsonApi\EmptyRelationship
|
private function emptyRelationship(Relationship $field, string $resourceUrl): EmptyRelationship
|
||||||
{
|
{
|
||||||
return new JsonApi\EmptyRelationship(
|
return new EmptyRelationship(
|
||||||
$field->name,
|
$field->getName(),
|
||||||
...$this->getRelationshipLinks($field, $resourceUrl)
|
...$this->getRelationshipLinks($field, $resourceUrl)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getRelationshipLinks(Schema\Relationship $field, string $resourceUrl): array
|
private function getRelationshipLinks(Relationship $field, string $resourceUrl): array
|
||||||
{
|
{
|
||||||
if (! $field->hasLinks) {
|
if (! $field->hasLinks()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new JsonApi\Link\SelfLink($resourceUrl.'/relationships/'.$field->name),
|
new SelfLink($resourceUrl.'/relationships/'.$field->getName()),
|
||||||
new JsonApi\Link\RelatedLink($resourceUrl.'/'.$field->name)
|
new RelatedLink($resourceUrl.'/'.$field->getName())
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addRelated(Schema\Relationship $field, $model, array $include): JsonApi\ResourceIdentifier
|
private function addRelated(Relationship $field, $model, array $include): ResourceIdentifier
|
||||||
{
|
{
|
||||||
$relatedResource = $field->resource ? $this->api->getResource($field->resource) : $this->resourceForModel($model);
|
$relatedResource = $field->getType() ? $this->api->getResource($field->getType()) : $this->resourceForModel($model);
|
||||||
|
|
||||||
return $this->resourceIdentifier(
|
return $this->resourceIdentifier(
|
||||||
$this->addToMap($relatedResource, $model, $include)
|
$this->addToMap($relatedResource, $model, $include)
|
||||||
|
|
@ -238,9 +249,9 @@ class Serializer
|
||||||
}, $items);
|
}, $items);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resourceObject(array $data): JsonApi\ResourceObject
|
private function resourceObject(array $data): Structure\ResourceObject
|
||||||
{
|
{
|
||||||
return new JsonApi\ResourceObject(
|
return new Structure\ResourceObject(
|
||||||
$data['type'],
|
$data['type'],
|
||||||
$data['id'],
|
$data['id'],
|
||||||
...array_values($data['fields']),
|
...array_values($data['fields']),
|
||||||
|
|
@ -249,9 +260,9 @@ class Serializer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resourceIdentifier(array $data): JsonApi\ResourceIdentifier
|
private function resourceIdentifier(array $data): Structure\ResourceIdentifier
|
||||||
{
|
{
|
||||||
return new JsonApi\ResourceIdentifier(
|
return new Structure\ResourceIdentifier(
|
||||||
$data['type'],
|
$data['type'],
|
||||||
$data['id']
|
$data['id']
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tobyz\JsonApiServer;
|
|
||||||
|
|
||||||
interface StatusProviderInterface
|
|
||||||
{
|
|
||||||
public function getJsonApiStatus(): array;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobyz\JsonApiServer;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Field;
|
||||||
|
|
||||||
|
function negate(Closure $condition)
|
||||||
|
{
|
||||||
|
return function (...$args) use ($condition) {
|
||||||
|
return ! $condition(...$args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrap($value)
|
||||||
|
{
|
||||||
|
if (! $value instanceof Closure) {
|
||||||
|
$value = function () use ($value) {
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluate($condition, array $params)
|
||||||
|
{
|
||||||
|
return $condition === true || ($condition instanceof Closure && $condition(...$params));
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_callbacks(array $callbacks, array $params)
|
||||||
|
{
|
||||||
|
foreach ($callbacks as $callback) {
|
||||||
|
$callback(...$params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function has_value(array $data, Field $field)
|
||||||
|
{
|
||||||
|
return isset($data[$field->getLocation()][$field->getName()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function &get_value(array $data, Field $field)
|
||||||
|
{
|
||||||
|
return $data[$field->getLocation()][$field->getName()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_value(array &$data, Field $field, $value)
|
||||||
|
{
|
||||||
|
$data[$field->getLocation()][$field->getName()] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function array_set(array $array, $key, $value)
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
|
||||||
|
while (count($keys) > 1) {
|
||||||
|
$array = &$array[array_shift($keys)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$array[array_shift($keys)] = $value;
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
@ -11,19 +11,19 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer;
|
namespace Tobyz\Tests\JsonApiServer;
|
||||||
|
|
||||||
|
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
use Zend\Diactoros\Uri;
|
use Zend\Diactoros\Uri;
|
||||||
|
|
||||||
abstract class AbstractTestCase extends TestCase
|
abstract class AbstractTestCase extends TestCase
|
||||||
{
|
{
|
||||||
public static function assertEncodesTo(string $expected, $obj, string $message = '')
|
use ArraySubsetAsserts;
|
||||||
|
|
||||||
|
protected function assertJsonApiDocumentSubset($subset, string $body, bool $checkForObjectIdentity = false, string $message = ''): void
|
||||||
{
|
{
|
||||||
self::assertEquals(
|
static::assertArraySubset($subset, json_decode($body, true), $checkForObjectIdentity, $message);
|
||||||
json_decode($expected),
|
|
||||||
json_decode(json_encode($obj, JSON_UNESCAPED_SLASHES)),
|
|
||||||
$message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildRequest(string $method, string $uri): ServerRequest
|
protected function buildRequest(string $method, string $uri): ServerRequest
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer;
|
namespace Tobyz\Tests\JsonApiServer;
|
||||||
|
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\Serializer;
|
use Tobyz\JsonApiServer\Serializer;
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use JsonApiPhp\JsonApi;
|
use JsonApiPhp\JsonApi;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
|
|
@ -25,9 +25,9 @@ class CreateTest extends AbstractTestCase
|
||||||
{
|
{
|
||||||
public function testResourceNotCreatableByDefault()
|
public function testResourceNotCreatableByDefault()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', new MockAdapter(), function (Type $schema) {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -41,9 +41,9 @@ class CreateTest extends AbstractTestCase
|
||||||
|
|
||||||
public function testCreateResourceValidatesBody()
|
public function testCreateResourceValidatesBody()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', new MockAdapter(), function (Type $schema) {
|
||||||
$schema->creatable();
|
$schema->creatable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -56,9 +56,9 @@ class CreateTest extends AbstractTestCase
|
||||||
|
|
||||||
public function testCreateResource()
|
public function testCreateResource()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter = new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) {
|
||||||
$schema->creatable();
|
$schema->creatable();
|
||||||
|
|
||||||
$schema->attribute('name')->writable();
|
$schema->attribute('name')->writable();
|
||||||
|
|
@ -110,9 +110,9 @@ class CreateTest extends AbstractTestCase
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter = new MockAdapter(), function (Builder $schema) use ($adapter, $request) {
|
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($adapter, $request) {
|
||||||
$schema->creatable();
|
$schema->creatable();
|
||||||
|
|
||||||
$schema->attribute('writable1')->writable();
|
$schema->attribute('writable1')->writable();
|
||||||
|
|
@ -148,9 +148,9 @@ class CreateTest extends AbstractTestCase
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter = new MockAdapter(), function (Builder $schema) use ($adapter, $request) {
|
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($adapter, $request) {
|
||||||
$schema->creatable();
|
$schema->creatable();
|
||||||
|
|
||||||
$schema->attribute('readonly')->readonly();
|
$schema->attribute('readonly')->readonly();
|
||||||
|
|
@ -175,9 +175,9 @@ class CreateTest extends AbstractTestCase
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter = new MockAdapter(), function (Builder $schema) use ($request) {
|
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($request) {
|
||||||
$schema->creatable();
|
$schema->creatable();
|
||||||
|
|
||||||
$schema->attribute('attribute1')->default('defaultValue');
|
$schema->attribute('attribute1')->default('defaultValue');
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer;
|
namespace Tobyz\Tests\JsonApiServer;
|
||||||
|
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobyz\JsonApiServer\Serializer;
|
use Tobyz\JsonApiServer\Serializer;
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use JsonApiPhp\JsonApi;
|
use JsonApiPhp\JsonApi;
|
||||||
use Zend\Diactoros\ServerRequest;
|
use Zend\Diactoros\ServerRequest;
|
||||||
|
|
@ -25,9 +25,9 @@ class DeleteTest extends AbstractTestCase
|
||||||
{
|
{
|
||||||
public function testResourceNotDeletableByDefault()
|
public function testResourceNotDeletableByDefault()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', new MockAdapter(), function (Type $schema) {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -45,9 +45,9 @@ class DeleteTest extends AbstractTestCase
|
||||||
'1' => $user = (object)['id' => '1']
|
'1' => $user = (object)['id' => '1']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) {
|
$api->resource('users', $usersAdapter, function (Type $schema) {
|
||||||
$schema->deletable();
|
$schema->deletable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -13,6 +13,7 @@ class MockAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
public $models = [];
|
public $models = [];
|
||||||
public $createdModel;
|
public $createdModel;
|
||||||
|
public $query;
|
||||||
private $type;
|
private $type;
|
||||||
|
|
||||||
public function __construct(array $models = [], string $type = null)
|
public function __construct(array $models = [], string $type = null)
|
||||||
|
|
@ -28,10 +29,10 @@ class MockAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function query()
|
public function query()
|
||||||
{
|
{
|
||||||
return (object) [];
|
return $this->query = (object) [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function find($query, $id)
|
public function find($query, string $id)
|
||||||
{
|
{
|
||||||
return $this->models[$id] ?? (object) ['id' => $id];
|
return $this->models[$id] ?? (object) ['id' => $id];
|
||||||
}
|
}
|
||||||
|
|
@ -61,17 +62,17 @@ class MockAdapter implements AdapterInterface
|
||||||
return $model->{$this->getProperty($relationship)} ?? [];
|
return $model->{$this->getProperty($relationship)} ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyAttribute($model, Attribute $attribute, $value)
|
public function setAttribute($model, Attribute $attribute, $value): void
|
||||||
{
|
{
|
||||||
$model->{$this->getProperty($attribute)} = $value;
|
$model->{$this->getProperty($attribute)} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyHasOne($model, HasOne $relationship, $related)
|
public function setHasOne($model, HasOne $relationship, $related): void
|
||||||
{
|
{
|
||||||
$model->{$this->getProperty($relationship)} = $related;
|
$model->{$this->getProperty($relationship)} = $related;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save($model)
|
public function save($model): void
|
||||||
{
|
{
|
||||||
$model->saveWasCalled = true;
|
$model->saveWasCalled = true;
|
||||||
|
|
||||||
|
|
@ -80,59 +81,54 @@ class MockAdapter implements AdapterInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveHasMany($model, HasMany $relationship, array $related)
|
public function saveHasMany($model, HasMany $relationship, array $related): void
|
||||||
{
|
{
|
||||||
$model->saveHasManyWasCalled = true;
|
$model->saveHasManyWasCalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($model)
|
public function delete($model): void
|
||||||
{
|
{
|
||||||
$model->deleteWasCalled = true;
|
$model->deleteWasCalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByIds($query, array $ids)
|
public function filterByIds($query, array $ids): void
|
||||||
{
|
{
|
||||||
$query->filters[] = ['ids', $ids];
|
$query->filter[] = ['ids', $ids];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByAttribute($query, Attribute $attribute, $value)
|
public function filterByAttribute($query, Attribute $attribute, $value): void
|
||||||
{
|
{
|
||||||
$query->filters[] = [$attribute, $value];
|
$query->filter[] = [$attribute, $value];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByHasOne($query, HasOne $relationship, array $ids)
|
public function filterByHasOne($query, HasOne $relationship, array $ids): void
|
||||||
{
|
{
|
||||||
$query->filters[] = [$relationship, $ids];
|
$query->filter[] = [$relationship, $ids];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByHasMany($query, HasMany $relationship, array $ids)
|
public function filterByHasMany($query, HasMany $relationship, array $ids): void
|
||||||
{
|
{
|
||||||
$query->filters[] = [$relationship, $ids];
|
$query->filter[] = [$relationship, $ids];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sortByAttribute($query, Attribute $attribute, string $direction)
|
public function sortByAttribute($query, Attribute $attribute, string $direction): void
|
||||||
{
|
{
|
||||||
$query->sort[] = [$attribute, $direction];
|
$query->sort[] = [$attribute, $direction];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginate($query, int $limit, int $offset)
|
public function paginate($query, int $limit, int $offset): void
|
||||||
{
|
{
|
||||||
$query->paginate[] = [$limit, $offset];
|
$query->paginate[] = [$limit, $offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function include($query, array $relationships)
|
public function load(array $models, array $relationships): void
|
||||||
{
|
|
||||||
$query->include[] = $relationships;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function load(array $models, array $relationships)
|
|
||||||
{
|
{
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
$model->load[] = $relationships;
|
$model->load[] = $relationships;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadIds(array $models, Relationship $relationship)
|
public function loadIds(array $models, Relationship $relationship): void
|
||||||
{
|
{
|
||||||
foreach ($models as $model) {
|
foreach ($models as $model) {
|
||||||
$model->loadIds[] = $relationship;
|
$model->loadIds[] = $relationship;
|
||||||
|
|
@ -141,11 +137,34 @@ class MockAdapter implements AdapterInterface
|
||||||
|
|
||||||
private function getProperty(Field $field)
|
private function getProperty(Field $field)
|
||||||
{
|
{
|
||||||
return $field->property ?: $field->name;
|
return $field->getProperty() ?: $field->getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handles($model)
|
public function represents($model): bool
|
||||||
{
|
{
|
||||||
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
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,17 @@
|
||||||
|
|
||||||
namespace Tobyz\Tests\JsonApiServer;
|
namespace Tobyz\Tests\JsonApiServer;
|
||||||
|
|
||||||
use Tobyz\JsonApiServer\Api;
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobyz\JsonApiServer\Serializer;
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
use Tobyz\JsonApiServer\Schema\Builder;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use JsonApiPhp\JsonApi;
|
|
||||||
use Zend\Diactoros\ServerRequest;
|
|
||||||
use Zend\Diactoros\Uri;
|
|
||||||
|
|
||||||
class ShowTest extends AbstractTestCase
|
class ShowTest extends AbstractTestCase
|
||||||
{
|
{
|
||||||
public function testResourceWithNoFields()
|
public function test_resource_with_no_fields()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
$api->resource('users', new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', new MockAdapter(), function (Type $schema) {
|
||||||
// no fields
|
// no fields
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -35,17 +31,19 @@ class ShowTest extends AbstractTestCase
|
||||||
$this->assertEquals($response->getStatusCode(), 200);
|
$this->assertEquals($response->getStatusCode(), 200);
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'type' => 'users',
|
'data' => [
|
||||||
'id' => '1',
|
'type' => 'users',
|
||||||
'links' => [
|
'id' => '1',
|
||||||
'self' => 'http://example.com/users/1'
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1'
|
||||||
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
json_decode($response->getBody(), true)['data']
|
json_decode($response->getBody(), true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAttributes()
|
public function test_attributes()
|
||||||
{
|
{
|
||||||
$adapter = new MockAdapter([
|
$adapter = new MockAdapter([
|
||||||
'1' => (object) [
|
'1' => (object) [
|
||||||
|
|
@ -58,9 +56,9 @@ class ShowTest extends AbstractTestCase
|
||||||
|
|
||||||
$request = $this->buildRequest('GET', '/users/1');
|
$request = $this->buildRequest('GET', '/users/1');
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter, function (Builder $schema) {
|
$api->resource('users', $adapter, function (Type $schema) {
|
||||||
$schema->attribute('attribute1');
|
$schema->attribute('attribute1');
|
||||||
$schema->attribute('attribute2', 'property2');
|
$schema->attribute('attribute2', 'property2');
|
||||||
$schema->attribute('attribute3')->property('property3');
|
$schema->attribute('attribute3')->property('property3');
|
||||||
|
|
@ -88,9 +86,9 @@ class ShowTest extends AbstractTestCase
|
||||||
|
|
||||||
$request = $this->buildRequest('GET', '/users/1');
|
$request = $this->buildRequest('GET', '/users/1');
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $adapter, function (Builder $schema) use ($model, $request) {
|
$api->resource('users', $adapter, function (Type $schema) use ($model, $request) {
|
||||||
$schema->attribute('attribute1')
|
$schema->attribute('attribute1')
|
||||||
->get(function ($arg1, $arg2) use ($model, $request) {
|
->get(function ($arg1, $arg2) use ($model, $request) {
|
||||||
$this->assertInstanceOf(Request::class, $arg1);
|
$this->assertInstanceOf(Request::class, $arg1);
|
||||||
|
|
@ -120,8 +118,8 @@ class ShowTest extends AbstractTestCase
|
||||||
|
|
||||||
$request = $this->buildRequest('GET', '/users/1');
|
$request = $this->buildRequest('GET', '/users/1');
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
$api->resource('users', $adapter, function (Builder $schema) use ($model, $request) {
|
$api->resource('users', $adapter, function (Type $schema) use ($model, $request) {
|
||||||
$schema->attribute('visible1');
|
$schema->attribute('visible1');
|
||||||
|
|
||||||
$schema->attribute('visible2')->visible();
|
$schema->attribute('visible2')->visible();
|
||||||
|
|
@ -183,19 +181,19 @@ class ShowTest extends AbstractTestCase
|
||||||
$request = $this->buildRequest('GET', '/users/1')
|
$request = $this->buildRequest('GET', '/users/1')
|
||||||
->withQueryParams(['include' => 'phone,phone2,phone3']);
|
->withQueryParams(['include' => 'phone,phone2,phone3']);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) {
|
$api->resource('users', $usersAdapter, function (Type $schema) {
|
||||||
$schema->hasOne('phone');
|
$schema->hasOne('phone');
|
||||||
|
|
||||||
$schema->hasOne('phone2', 'phones', 'property2');
|
$schema->hasOne('phone2', 'phones', 'property2');
|
||||||
|
|
||||||
$schema->hasOne('phone3')
|
$schema->hasOne('phone3')
|
||||||
->resource('phones')
|
->type('phones')
|
||||||
->property('property3');
|
->property('property3');
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('phones', $phonesAdapter, function (Builder $schema) {
|
$api->resource('phones', $phonesAdapter, function (Type $schema) {
|
||||||
$schema->attribute('number');
|
$schema->attribute('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -267,15 +265,15 @@ class ShowTest extends AbstractTestCase
|
||||||
$request = $this->buildRequest('GET', '/users/1')
|
$request = $this->buildRequest('GET', '/users/1')
|
||||||
->withQueryParams(['include' => 'phone2']);
|
->withQueryParams(['include' => 'phone2']);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) {
|
$api->resource('users', $usersAdapter, function (Type $schema) {
|
||||||
$schema->hasOne('phone');
|
$schema->hasOne('phone');
|
||||||
|
|
||||||
$schema->hasOne('phone2', 'phones', 'property2');
|
$schema->hasOne('phone2', 'phones', 'property2');
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('phones', $phonesAdapter, function (Builder $schema) {
|
$api->resource('phones', $phonesAdapter, function (Type $schema) {
|
||||||
$schema->attribute('number');
|
$schema->attribute('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -338,9 +336,9 @@ class ShowTest extends AbstractTestCase
|
||||||
|
|
||||||
public function testHasManyRelationshipNotIncludableByDefault()
|
public function testHasManyRelationshipNotIncludableByDefault()
|
||||||
{
|
{
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', new MockAdapter(), function (Builder $schema) {
|
$api->resource('users', new MockAdapter(), function (Type $schema) {
|
||||||
$schema->hasMany('groups');
|
$schema->hasMany('groups');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -365,9 +363,9 @@ class ShowTest extends AbstractTestCase
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) {
|
$api->resource('users', $usersAdapter, function (Type $schema) {
|
||||||
$schema->hasMany('groups');
|
$schema->hasMany('groups');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -400,11 +398,11 @@ class ShowTest extends AbstractTestCase
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$relationships = [];
|
$relationships = [];
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) use (&$relationships) {
|
$api->resource('users', $usersAdapter, function (Type $schema) use (&$relationships) {
|
||||||
$relationships[] = $schema->hasMany('groups1', 'groups', 'property1')
|
$relationships[] = $schema->hasMany('groups1', 'groups', 'property1')
|
||||||
->includable();
|
->includable();
|
||||||
|
|
||||||
|
|
@ -412,7 +410,7 @@ class ShowTest extends AbstractTestCase
|
||||||
->includable();
|
->includable();
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('groups', $groupsAdapter, function (Builder $schema) {
|
$api->resource('groups', $groupsAdapter, function (Type $schema) {
|
||||||
$schema->attribute('name');
|
$schema->attribute('name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -519,19 +517,19 @@ class ShowTest extends AbstractTestCase
|
||||||
'1' => $post = (object) ['id' => '1', 'user' => $user]
|
'1' => $post = (object) ['id' => '1', 'user' => $user]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$api = new Api('http://example.com');
|
$api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
$relationships = [];
|
$relationships = [];
|
||||||
|
|
||||||
$api->resource('posts', $postsAdapter, function (Builder $schema) use (&$relationships) {
|
$api->resource('posts', $postsAdapter, function (Type $schema) use (&$relationships) {
|
||||||
$relationships[] = $schema->hasOne('user');
|
$relationships[] = $schema->hasOne('user');
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('users', $usersAdapter, function (Builder $schema) use (&$relationships) {
|
$api->resource('users', $usersAdapter, function (Type $schema) use (&$relationships) {
|
||||||
$relationships[] = $schema->hasMany('groups')->includable();
|
$relationships[] = $schema->hasMany('groups')->includable();
|
||||||
});
|
});
|
||||||
|
|
||||||
$api->resource('groups', $groupsAdapter, function (Builder $schema) {
|
$api->resource('groups', $groupsAdapter, function (Type $schema) {
|
||||||
$schema->attribute('name');
|
$schema->attribute('name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class AttributeFilterableTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MockAdapter
|
||||||
|
*/
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$this->adapter = new MockAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_are_not_filterable_by_default()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('field');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['filter' => ['field' => 'Toby']])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_filterable()
|
||||||
|
{
|
||||||
|
$attribute = null;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$attribute) {
|
||||||
|
$attribute = $type->attribute('name')->filterable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['filter' => ['name' => 'Toby']])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertContains([$attribute, 'Toby'], $this->adapter->query->filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_filterable_with_custom_logic()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->attribute('name')
|
||||||
|
->filterable(function ($query, $value, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->query, $query);
|
||||||
|
$this->assertEquals('Toby', $value);
|
||||||
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
|
|
||||||
|
$called = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['filter' => ['name' => 'Toby']])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
$this->assertTrue(empty($this->adapter->query->filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_explicitly_not_filterable()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('name')->notFilterable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['filter' => ['name' => 'Toby']])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class AttributeSortableTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MockAdapter
|
||||||
|
*/
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$this->adapter = new MockAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_sortable()
|
||||||
|
{
|
||||||
|
$attribute = null;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$attribute) {
|
||||||
|
$attribute = $type->attribute('name')->sortable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['sort' => 'name'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$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_attributes_are_not_sortable_by_default()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('name');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['sort' => 'name'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_explicitly_not_sortable()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('name')->notSortable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
->withQueryParams(['sort' => 'name'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use JsonApiPhp\JsonApi\ErrorDocument;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Tobyz\JsonApiServer\ErrorProviderInterface;
|
||||||
|
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class AttributeTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MockAdapter
|
||||||
|
*/
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$this->adapter = new MockAdapter([
|
||||||
|
'1' => (object) ['id' => '1', 'name' => 'Toby', 'color' => 'yellow'],
|
||||||
|
'2' => (object) ['id' => '2', 'name' => 'Franz', 'color' => 'blue'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_multiple_attributes()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('name');
|
||||||
|
$type->attribute('color');
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Toby',
|
||||||
|
'color' => 'yellow',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_specify_a_property()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->attribute('name')
|
||||||
|
->property('color');
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'yellow',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_have_getters()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->attribute('name')
|
||||||
|
->get('Toby');
|
||||||
|
|
||||||
|
$type->attribute('color')
|
||||||
|
->get(function ($model, $request) use (&$called) {
|
||||||
|
$called = true;
|
||||||
|
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
|
||||||
|
return 'yellow';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Toby',
|
||||||
|
'color' => 'yellow',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_setter_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable()
|
||||||
|
->set(function ($model, $value, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertEquals('value', $value);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
|
||||||
|
$called = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_setter_precludes_adapter_action()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable()
|
||||||
|
->set(function () {});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue(empty($this->adapter->models['1']->writable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_saver_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable()
|
||||||
|
->save(function ($model, $value, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertEquals('value', $value);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
|
||||||
|
$called = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_saver_precludes_adapter_action()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable()
|
||||||
|
->save(function () {});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue(empty($this->adapter->models['1']->writable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_run_callback_after_being_saved()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable()
|
||||||
|
->saved(function ($model, $value, $request) use (&$called) {
|
||||||
|
$this->assertTrue($this->adapter->models['1']->saveWasCalled);
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertEquals('value', $value);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
|
||||||
|
$called = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_have_default_values()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->creatable();
|
||||||
|
|
||||||
|
$type->attribute('name')
|
||||||
|
->default('Toby');
|
||||||
|
|
||||||
|
$type->attribute('color')
|
||||||
|
->default(function () {
|
||||||
|
return 'yellow';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('Toby', $this->adapter->createdModel->name);
|
||||||
|
$this->assertEquals('yellow', $this->adapter->createdModel->color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_default_callback_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->creatable();
|
||||||
|
$type->attribute('attribute')
|
||||||
|
->default(function ($request) {
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_values_from_request_override_default_values()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->creatable();
|
||||||
|
$type->attribute('name')
|
||||||
|
->writable()
|
||||||
|
->default('Toby');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Franz',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('Franz', $this->adapter->createdModel->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_validated()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->creatable();
|
||||||
|
|
||||||
|
$type->attribute('name')
|
||||||
|
->writable()
|
||||||
|
->validate(function ($fail, $value, $model, $request, $field) {
|
||||||
|
$this->assertEquals('Toby', $value);
|
||||||
|
$this->assertSame($this->adapter->createdModel, $model);
|
||||||
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
|
$this->assertInstanceOf(Attribute::class, $field);
|
||||||
|
|
||||||
|
$fail('detail');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityException::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Toby',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} catch (ErrorProviderInterface $e) {
|
||||||
|
$document = new ErrorDocument(...$e->getJsonApiErrors());
|
||||||
|
|
||||||
|
$this->assertArraySubset([
|
||||||
|
'errors' => [
|
||||||
|
[
|
||||||
|
'status' => '422',
|
||||||
|
'source' => [
|
||||||
|
'pointer' => '/data/attributes/name'
|
||||||
|
],
|
||||||
|
'detail' => 'detail'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], json_decode(json_encode($document), true));
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,274 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class AttributeWritableTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MockAdapter
|
||||||
|
*/
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$this->adapter = new MockAdapter([
|
||||||
|
'1' => (object) ['id' => '1']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_are_readonly_by_default()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('readonly');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'readonly' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_explicitly_writable()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')->writable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertEquals('value', $this->adapter->models['1']->writable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_conditionally_writable()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable(function () { return true; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertEquals('value', $this->adapter->models['1']->writable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_conditionally_not_writable()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable(function () { return false; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_writable_callback_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->writable(function ($model, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertInstanceOf(ServerRequestInterface::class, $request);
|
||||||
|
|
||||||
|
return $called = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_explicitly_readonly()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('readonly')->readonly();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'readonly' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_conditionally_readonly()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('readonly')
|
||||||
|
->readonly(function () { return true; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->expectException(BadRequestException::class);
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'readonly' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attribute_readonly_callback_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = false;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('readonly')
|
||||||
|
->readonly(function ($model, $request) use (&$called) {
|
||||||
|
$called = true;
|
||||||
|
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'readonly' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertTrue($called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_attributes_can_be_conditionally_not_readonly()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) {
|
||||||
|
$type->updatable();
|
||||||
|
$type->attribute('writable')
|
||||||
|
->readonly(function () { return false; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'writable' => 'value',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertEquals('value', $this->adapter->models['1']->writable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class BasicTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$adapter = new MockAdapter([
|
||||||
|
'1' => (object) [
|
||||||
|
'id' => '1',
|
||||||
|
'name' => 'Toby',
|
||||||
|
],
|
||||||
|
'2' => (object) [
|
||||||
|
'id' => '2',
|
||||||
|
'name' => 'Franz',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->api->resource('users', $adapter, function (Type $type) {
|
||||||
|
$type->attribute('name')->writable();
|
||||||
|
$type->creatable();
|
||||||
|
$type->updatable();
|
||||||
|
$type->deletable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_show_resource()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Toby'
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_resources()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Toby'
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '2',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Franz'
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/2'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_resource()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('POST', '/users')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Bob',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response->getStatusCode());
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Bob',
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_resource()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withParsedBody([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Bob',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
|
$this->assertJsonApiDocumentSubset([
|
||||||
|
'data' => [
|
||||||
|
'type' => 'users',
|
||||||
|
'id' => '1',
|
||||||
|
'attributes' => [
|
||||||
|
'name' => 'Bob',
|
||||||
|
],
|
||||||
|
'links' => [
|
||||||
|
'self' => 'http://example.com/users/1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], $response->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_resource()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('DELETE', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(204, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
<?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\Tests\JsonApiServer\feature;
|
||||||
|
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class FieldVisibilityTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MockAdapter
|
||||||
|
*/
|
||||||
|
private $adapter;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
|
||||||
|
$this->adapter = new MockAdapter([
|
||||||
|
'1' => (object) ['id' => '1']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fields_are_visible_by_default()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', new MockAdapter, function (Type $type) {
|
||||||
|
$type->attribute('visibleAttribute');
|
||||||
|
$type->hasOne('visibleHasOne');
|
||||||
|
$type->hasMany('visibleHasMany');
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$document = json_decode($response->getBody(), true);
|
||||||
|
$attributes = $document['data']['attributes'] ?? [];
|
||||||
|
$relationships = $document['data']['relationships'] ?? [];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('visibleAttribute', $attributes);
|
||||||
|
$this->assertArrayHasKey('visibleHasOne', $relationships);
|
||||||
|
$this->assertArrayHasKey('visibleHasMany', $relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fields_can_be_explicitly_visible()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', new MockAdapter, function (Type $type) {
|
||||||
|
$type->attribute('visibleAttribute')->visible();
|
||||||
|
$type->hasOne('visibleHasOne')->visible();
|
||||||
|
$type->hasMany('visibleHasMany')->visible();
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$document = json_decode($response->getBody(), true);
|
||||||
|
$attributes = $document['data']['attributes'] ?? [];
|
||||||
|
$relationships = $document['data']['relationships'] ?? [];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('visibleAttribute', $attributes);
|
||||||
|
$this->assertArrayHasKey('visibleHasOne', $relationships);
|
||||||
|
$this->assertArrayHasKey('visibleHasMany', $relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fields_can_be_conditionally_visible()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', new MockAdapter, function (Type $type) {
|
||||||
|
$type->attribute('visibleAttribute')
|
||||||
|
->visible(function () { return true; });
|
||||||
|
|
||||||
|
$type->attribute('hiddenAttribute')
|
||||||
|
->visible(function () { return false; });
|
||||||
|
|
||||||
|
$type->hasOne('visibleHasOne')
|
||||||
|
->visible(function () { return true; });
|
||||||
|
|
||||||
|
$type->hasOne('hiddenHasOne')
|
||||||
|
->visible(function () { return false; });
|
||||||
|
|
||||||
|
$type->hasMany('visibleHasMany')
|
||||||
|
->visible(function () { return true; });
|
||||||
|
|
||||||
|
$type->hasMany('hiddenHasMany')
|
||||||
|
->visible(function () { return false; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$document = json_decode($response->getBody(), true);
|
||||||
|
$attributes = $document['data']['attributes'] ?? [];
|
||||||
|
$relationships = $document['data']['relationships'] ?? [];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('visibleAttribute', $attributes);
|
||||||
|
$this->assertArrayHasKey('visibleHasOne', $relationships);
|
||||||
|
$this->assertArrayHasKey('visibleHasMany', $relationships);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('hiddenAttribute', $attributes);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasOne', $relationships);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasMany', $relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_visible_callback_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = 0;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$callback = function ($model, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
$called++;
|
||||||
|
};
|
||||||
|
|
||||||
|
$type->attribute('attribute')
|
||||||
|
->visible($callback);
|
||||||
|
|
||||||
|
$type->hasOne('hasOne')
|
||||||
|
->visible($callback);
|
||||||
|
|
||||||
|
$type->hasMany('hasMany')
|
||||||
|
->visible($callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(3, $called);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fields_can_be_explicitly_hidden()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', new MockAdapter, function (Type $type) {
|
||||||
|
$type->attribute('hiddenAttribute')->hidden();
|
||||||
|
$type->hasOne('hiddenHasOne')->hidden();
|
||||||
|
$type->hasMany('hiddenHasMany')->hidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$document = json_decode($response->getBody(), true);
|
||||||
|
$attributes = $document['data']['attributes'] ?? [];
|
||||||
|
$relationships = $document['data']['relationships'] ?? [];
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('hiddenAttribute', $attributes);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasOne', $relationships);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasMany', $relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fields_can_be_conditionally_hidden()
|
||||||
|
{
|
||||||
|
$this->api->resource('users', new MockAdapter, function (Type $type) {
|
||||||
|
$type->attribute('visibleAttribute')
|
||||||
|
->hidden(function () { return false; });
|
||||||
|
|
||||||
|
$type->attribute('hiddenAttribute')
|
||||||
|
->hidden(function () { return true; });
|
||||||
|
|
||||||
|
$type->hasOne('visibleHasOne')
|
||||||
|
->hidden(function () { return false; });
|
||||||
|
|
||||||
|
$type->hasOne('hiddenHasOne')
|
||||||
|
->hidden(function () { return true; });
|
||||||
|
|
||||||
|
$type->hasMany('visibleHasMany')
|
||||||
|
->hidden(function () { return false; });
|
||||||
|
|
||||||
|
$type->hasMany('hiddenHasMany')
|
||||||
|
->hidden(function () { return true; });
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$document = json_decode($response->getBody(), true);
|
||||||
|
$attributes = $document['data']['attributes'] ?? [];
|
||||||
|
$relationships = $document['data']['relationships'] ?? [];
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('visibleAttribute', $attributes);
|
||||||
|
$this->assertArrayHasKey('visibleHasOne', $relationships);
|
||||||
|
$this->assertArrayHasKey('visibleHasMany', $relationships);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('hiddenAttribute', $attributes);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasOne', $relationships);
|
||||||
|
$this->assertArrayNotHasKey('hiddenHasMany', $relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_hidden_callback_receives_correct_parameters()
|
||||||
|
{
|
||||||
|
$called = 0;
|
||||||
|
|
||||||
|
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
|
||||||
|
$callback = function ($model, $request) use (&$called) {
|
||||||
|
$this->assertSame($this->adapter->models['1'], $model);
|
||||||
|
$this->assertInstanceOf(RequestInterface::class, $request);
|
||||||
|
$called++;
|
||||||
|
};
|
||||||
|
|
||||||
|
$type->attribute('attribute')
|
||||||
|
->hidden($callback);
|
||||||
|
|
||||||
|
$type->hasOne('hasOne')
|
||||||
|
->hidden($callback);
|
||||||
|
|
||||||
|
$type->hasMany('hasMany')
|
||||||
|
->hidden($callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(3, $called);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?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\Tests\JsonApiServer\specification;
|
||||||
|
|
||||||
|
use Tobyz\JsonApiServer\JsonApi;
|
||||||
|
use Tobyz\JsonApiServer\Exception\NotAcceptableException;
|
||||||
|
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
|
||||||
|
use Tobyz\JsonApiServer\Schema\Type;
|
||||||
|
use Tobyz\Tests\JsonApiServer\AbstractTestCase;
|
||||||
|
use Tobyz\Tests\JsonApiServer\MockAdapter;
|
||||||
|
|
||||||
|
class ContentNegotiationTest extends AbstractTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JsonApi
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->api = new JsonApi('http://example.com');
|
||||||
|
$this->api->resource('users', new MockAdapter(), function (Type $type) {
|
||||||
|
// no fields
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJsonApiContentTypeIsReturned()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'application/vnd.api+json',
|
||||||
|
$response->getHeaderLine('Content-Type')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testErrorWhenValidRequestContentTypeHasParameters()
|
||||||
|
{
|
||||||
|
$request = $this->buildRequest('PATCH', '/users/1')
|
||||||
|
->withHeader('Content-Type', 'application/vnd.api+json;profile="http://example.com/last-modified"');
|
||||||
|
|
||||||
|
$this->expectException(UnsupportedMediaTypeException::class);
|
||||||
|
|
||||||
|
$this->api->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testErrorWhenAllValidAcceptsHaveParameters()
|
||||||
|
{
|
||||||
|
$request = $this->buildRequest('GET', '/users/1')
|
||||||
|
->withHeader('Accept', 'application/vnd.api+json;profile="http://example.com/last-modified", application/vnd.api+json;profile="http://example.com/versioning"');
|
||||||
|
|
||||||
|
$this->expectException(NotAcceptableException::class);
|
||||||
|
|
||||||
|
$this->api->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuccessWhenOnlySomeAcceptsHaveParameters()
|
||||||
|
{
|
||||||
|
$response = $this->api->handle(
|
||||||
|
$this->buildRequest('GET', '/users/1')
|
||||||
|
->withHeader('Accept', 'application/vnd.api+json;profile="http://example.com/last-modified", application/vnd.api+json')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of JSON-API.
|
||||||
|
*
|
||||||
|
* (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\Tests\JsonApiServer\unit\Http;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Tobyz\JsonApiServer\Http\MediaTypes;
|
||||||
|
|
||||||
|
class MediaTypesTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testContainsOnExactMatch()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContainsDoesNotMatchWithExtraParameters()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json; profile=foo');
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContainsMatchesWhenOnlyWeightIsProvided()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json; q=0.8');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContainsDoesNotMatchWithExtraParametersBeforeWeight()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json; profile=foo; q=0.8');
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContainsMatchesWithExtraParametersAfterWeight()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json; q=0.8; profile=foo');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContainsMatchesWhenOneOfMultipleMediaTypesIsValid()
|
||||||
|
{
|
||||||
|
$header = new MediaTypes('application/json; profile=foo, application/json; q=0.6');
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$header->containsExactly('application/json')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue