This commit is contained in:
Toby Zerner 2019-08-19 18:36:16 +02:00
parent 269207eedb
commit ca9cd62711
13 changed files with 136 additions and 890 deletions

View File

@ -138,15 +138,15 @@ class EloquentAdapter implements AdapterInterface
public function filterByAttribute($query, Attribute $attribute, $value): void public function filterByAttribute($query, Attribute $attribute, $value): void
{ {
$property = $this->getAttributeProperty($attribute); $column = $this->getAttributeColumn($attribute);
// TODO: extract this into non-adapter territory // 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($column, '>=', $matches[1]);
} }
if ($matches[2] !== '*') { if ($matches[2] !== '*') {
$query->where($property, '<=', $matches[2]); $query->where($column, '<=', $matches[2]);
} }
return; return;
@ -154,13 +154,13 @@ class EloquentAdapter implements AdapterInterface
foreach (['>=', '>', '<=', '<'] as $operator) { foreach (['>=', '>', '<=', '<'] as $operator) {
if (strpos($value, $operator) === 0) { if (strpos($value, $operator) === 0) {
$query->where($property, $operator, substr($value, strlen($operator))); $query->where($column, $operator, substr($value, strlen($operator)));
return; return;
} }
} }
$query->where($property, $value); $query->where($column, $value);
} }
public function filterByHasOne($query, HasOne $relationship, array $ids): void public function filterByHasOne($query, HasOne $relationship, array $ids): void
@ -181,9 +181,9 @@ class EloquentAdapter implements AdapterInterface
}); });
} }
public function sortByAttribute($query, Attribute $field, string $direction): void public function sortByAttribute($query, Attribute $attribute, string $direction): void
{ {
$query->orderBy($this->getAttributeProperty($field), $direction); $query->orderBy($this->getAttributeColumn($attribute), $direction);
} }
public function paginate($query, int $limit, int $offset): void public function paginate($query, int $limit, int $offset): void
@ -224,6 +224,11 @@ class EloquentAdapter implements AdapterInterface
return $attribute->getProperty() ?: strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $attribute->getName())); return $attribute->getProperty() ?: strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $attribute->getName()));
} }
private function getAttributeColumn(Attribute $attribute): string
{
return $this->model->getTable().'.'.$this->getAttributeProperty($attribute);
}
private function getRelationshipProperty(Relationship $relationship): string private function getRelationshipProperty(Relationship $relationship): string
{ {
return $relationship->getProperty() ?: $relationship->getName(); return $relationship->getProperty() ?: $relationship->getName();

View File

@ -103,6 +103,7 @@ trait IncludesData
$adapter = $this->resource->getAdapter(); $adapter = $this->resource->getAdapter();
$fields = $this->resource->getSchema()->getFields(); $fields = $this->resource->getSchema()->getFields();
// TODO: don't load IDs for relationships which are included below
foreach ($fields as $name => $field) { foreach ($fields as $name => $field) {
if (! $field instanceof Relationship || ! evaluate($field->getLinkage(), [$request]) || ! $field->getLoadable()) { if (! $field instanceof Relationship || ! evaluate($field->getLinkage(), [$request]) || ! $field->getLoadable()) {
continue; continue;

View File

@ -13,13 +13,11 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Tobyz\JsonApiServer\JsonApi; use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\Exception\BadRequestException;
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\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;
use Tobyz\JsonApiServer\Schema\Type;
use Tobyz\JsonApiServer\Serializer; use Tobyz\JsonApiServer\Serializer;
class Index implements RequestHandlerInterface class Index implements RequestHandlerInterface
@ -47,7 +45,7 @@ class Index implements RequestHandlerInterface
$query = $adapter->query(); $query = $adapter->query();
foreach ($schema->getScopes() as $scope) { foreach ($schema->getScopes() as $scope) {
$request = $scope($request, $query) ?: $request; $request = $scope($query, $request, null) ?: $request;
} }
if ($filter = $request->getAttribute('jsonApiFilter')) { if ($filter = $request->getAttribute('jsonApiFilter')) {

View File

@ -162,6 +162,9 @@ abstract class Field
return $this->default; return $this->default;
} }
/**
* @return bool|Closure
*/
public function getFilterable() public function getFilterable()
{ {
return $this->filterable; return $this->filterable;

View File

@ -48,12 +48,12 @@ abstract class Relationship extends Field
return $this; return $this;
} }
// public function notLoadable() public function notLoadable()
// { {
// $this->loadable = false; $this->loadable = false;
//
// return $this; return $this;
// } }
public function includable() public function includable()
{ {
@ -98,6 +98,9 @@ abstract class Relationship extends Field
return $this->links; return $this->links;
} }
/**
* @return bool|Closure
*/
public function getLoadable() public function getLoadable()
{ {
return $this->loadable; return $this->loadable;

View File

@ -16,6 +16,7 @@ final class Type
private $limit = 50; private $limit = 50;
private $countable = true; private $countable = true;
private $defaultSort; private $defaultSort;
private $defaultFilter;
private $scopes = []; private $scopes = [];
private $saver; private $saver;
private $creatable = false; private $creatable = false;
@ -80,6 +81,15 @@ final class Type
return $this->meta; return $this->meta;
} }
public function filter(string $name, Closure $callback)
{
$this->attribute($name)
->hidden()
->filterable($callback);
return $this;
}
public function paginate(int $perPage) public function paginate(int $perPage)
{ {
$this->paginate = $perPage; $this->paginate = $perPage;
@ -90,7 +100,7 @@ final class Type
$this->paginate = null; $this->paginate = null;
} }
public function getPaginate(): int public function getPaginate(): ?int
{ {
return $this->paginate; return $this->paginate;
} }
@ -279,4 +289,16 @@ final class Type
{ {
return $this->defaultSort; return $this->defaultSort;
} }
public function defaultFilter(?array $filter)
{
$this->defaultFilter = $filter;
return $this;
}
public function getDefaultFilter()
{
return $this->defaultFilter;
}
} }

View File

@ -98,7 +98,7 @@ final class Serializer
ksort($metas); ksort($metas);
foreach ($metas as $name => $meta) { foreach ($metas as $name => $meta) {
$data['meta'][$name] = new Structure\Meta($meta->name, ($meta->value)($this->request, $model)); $data['meta'][$name] = new Structure\Meta($meta->getName(), ($meta->getValue())($model, $this->request));
} }
$this->merge($data); $this->merge($data);
@ -270,11 +270,11 @@ final class Serializer
private function relatedResourceIdentifier(Schema\Relationship $field, $model) private function relatedResourceIdentifier(Schema\Relationship $field, $model)
{ {
$relatedResource = $this->api->getResource($field->resource); $relatedResource = $this->api->getResource($type = $field->getType());
return $this->resourceIdentifier([ return $this->resourceIdentifier([
'type' => $field->resource, 'type' => $type,
'id' => $relatedResource->getAdapter()->getId($model) 'id' => is_string($model) ? $model : $relatedResource->getAdapter()->getId($model)
]); ]);
} }
} }

View File

@ -1,202 +0,0 @@
<?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;
use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use Tobyz\JsonApiServer\Serializer;
use Tobyz\JsonApiServer\Schema\Type;
use Psr\Http\Message\ServerRequestInterface as Request;
use JsonApiPhp\JsonApi;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Uri;
class CreateTest extends AbstractTestCase
{
public function testResourceNotCreatableByDefault()
{
$api = new JsonApi('http://example.com');
$api->resource('users', new MockAdapter(), function (Type $schema) {
//
});
$request = $this->buildRequest('POST', '/users');
$this->expectException(ForbiddenException::class);
$this->expectExceptionMessage('You cannot create this resource');
$api->handle($request);
}
public function testCreateResourceValidatesBody()
{
$api = new JsonApi('http://example.com');
$api->resource('users', new MockAdapter(), function (Type $schema) {
$schema->creatable();
});
$request = $this->buildRequest('POST', '/users');
$this->expectException(BadRequestException::class);
$api->handle($request);
}
public function testCreateResource()
{
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) {
$schema->creatable();
$schema->attribute('name')->writable();
});
$request = $this->buildRequest('POST', '/users')
->withParsedBody([
'data' => [
'type' => 'users',
'id' => '1',
'attributes' => [
'name' => 'Toby'
]
]
]);
$response = $api->handle($request);
$this->assertTrue($adapter->createdModel->saveWasCalled);
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals(
[
'type' => 'users',
'id' => '1',
'attributes' => [
'name' => 'Toby'
],
'links' => [
'self' => 'http://example.com/users/1'
]
],
json_decode($response->getBody(), true)['data']
);
}
public function testAttributeWritable()
{
$request = $this->buildRequest('POST', '/users')
->withParsedBody([
'data' => [
'type' => 'users',
'id' => '1',
'attributes' => [
'writable1' => 'value',
'writable2' => 'value',
'writable3' => 'value',
]
]
]);
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($adapter, $request) {
$schema->creatable();
$schema->attribute('writable1')->writable();
$schema->attribute('writable2')->writableIf(function ($arg1, $arg2) use ($adapter, $request) {
$this->assertInstanceOf(Request::class, $arg1);
$this->assertEquals($adapter->createdModel, $arg2);
return true;
});
$schema->attribute('writable3')->readonlyIf(function ($arg1, $arg2) use ($adapter, $request) {
$this->assertInstanceOf(Request::class, $arg1);
$this->assertEquals($adapter->createdModel, $arg2);
return false;
});
});
$response = $api->handle($request);
$this->assertEquals(201, $response->getStatusCode());
}
public function testAttributeReadonly()
{
$request = $this->buildRequest('POST', '/users')
->withParsedBody([
'data' => [
'type' => 'users',
'id' => '1',
'attributes' => [
'readonly' => 'value',
]
]
]);
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($adapter, $request) {
$schema->creatable();
$schema->attribute('readonly')->readonly();
});
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Field [readonly] is not writable');
$api->handle($request);
}
public function testAttributeDefault()
{
$request = $this->buildRequest('POST', '/users')
->withParsedBody([
'data' => [
'type' => 'users',
'id' => '1',
'attributes' => [
'attribute3' => 'userValue'
]
]
]);
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter = new MockAdapter(), function (Type $schema) use ($request) {
$schema->creatable();
$schema->attribute('attribute1')->default('defaultValue');
$schema->attribute('attribute2')->default(function ($arg1) use ($request) {
$this->assertInstanceOf(Request::class, $arg1);
return 'defaultValue';
});
$schema->attribute('attribute3')->writable()->default('defaultValue');
});
$response = $api->handle($request);
$this->assertEquals(
[
'attribute1' => 'defaultValue',
'attribute2' => 'defaultValue',
'attribute3' => 'userValue'
],
json_decode($response->getBody(), true)['data']['attributes']
);
}
}

View File

@ -1,60 +0,0 @@
<?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;
use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\Exception\ForbiddenException;
use Tobyz\JsonApiServer\Serializer;
use Tobyz\JsonApiServer\Schema\Type;
use Psr\Http\Message\ServerRequestInterface as Request;
use JsonApiPhp\JsonApi;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Uri;
class DeleteTest extends AbstractTestCase
{
public function testResourceNotDeletableByDefault()
{
$api = new JsonApi('http://example.com');
$api->resource('users', new MockAdapter(), function (Type $schema) {
//
});
$request = $this->buildRequest('DELETE', '/users/1');
$this->expectException(ForbiddenException::class);
$this->expectExceptionMessage('You cannot delete this resource');
$api->handle($request);
}
public function testDeleteResource()
{
$usersAdapter = new MockAdapter([
'1' => $user = (object)['id' => '1']
]);
$api = new JsonApi('http://example.com');
$api->resource('users', $usersAdapter, function (Type $schema) {
$schema->deletable();
});
$request = $this->buildRequest('DELETE', '/users/1');
$response = $api->handle($request);
$this->assertEquals(204, $response->getStatusCode());
$this->assertTrue($user->deleteWasCalled);
}
}

View File

@ -1,600 +0,0 @@
<?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;
use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\Schema\Type;
use Psr\Http\Message\ServerRequestInterface as Request;
class ShowTest extends AbstractTestCase
{
public function test_resource_with_no_fields()
{
$api = new JsonApi('http://example.com');
$api->resource('users', new MockAdapter(), function (Type $schema) {
// no fields
});
$request = $this->buildRequest('GET', '/users/1');
$response = $api->handle($request);
$this->assertEquals($response->getStatusCode(), 200);
$this->assertEquals(
[
'data' => [
'type' => 'users',
'id' => '1',
'links' => [
'self' => 'http://example.com/users/1'
]
]
],
json_decode($response->getBody(), true)
);
}
public function test_attributes()
{
$adapter = new MockAdapter([
'1' => (object) [
'id' => '1',
'attribute1' => 'value1',
'property2' => 'value2',
'property3' => 'value3'
]
]);
$request = $this->buildRequest('GET', '/users/1');
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter, function (Type $schema) {
$schema->attribute('attribute1');
$schema->attribute('attribute2', 'property2');
$schema->attribute('attribute3')->property('property3');
});
$response = $api->handle($request);
$this->assertArraySubset(
[
'attributes' => [
'attribute1' => 'value1',
'attribute2' => 'value2',
'attribute3' => 'value3'
]
],
json_decode($response->getBody(), true)['data']
);
}
public function testAttributeGetter()
{
$adapter = new MockAdapter([
'1' => $model = (object) ['id' => '1']
]);
$request = $this->buildRequest('GET', '/users/1');
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter, function (Type $schema) use ($model, $request) {
$schema->attribute('attribute1')
->get(function ($arg1, $arg2) use ($model, $request) {
$this->assertInstanceOf(Request::class, $arg1);
$this->assertEquals($model, $arg2);
return 'value1';
});
});
$response = $api->handle($request);
$this->assertEquals($response->getStatusCode(), 200);
$this->assertArraySubset(
[
'attributes' => [
'attribute1' => 'value1'
]
],
json_decode($response->getBody(), true)['data']
);
}
public function testAttributeVisibility()
{
$adapter = new MockAdapter([
'1' => $model = (object) ['id' => '1']
]);
$request = $this->buildRequest('GET', '/users/1');
$api = new JsonApi('http://example.com');
$api->resource('users', $adapter, function (Type $schema) use ($model, $request) {
$schema->attribute('visible1');
$schema->attribute('visible2')->visible();
$schema->attribute('visible3')->visibleIf(function ($arg1, $arg2) use ($model, $request) {
$this->assertInstanceOf(Request::class, $arg1);
$this->assertEquals($model, $arg2);
return true;
});
$schema->attribute('visible4')->hiddenIf(function ($arg1, $arg2) use ($model, $request) {
$this->assertInstanceOf(Request::class, $arg1);
$this->assertEquals($model, $arg2);
return false;
});
$schema->attribute('hidden1')->hidden();
$schema->attribute('hidden2')->visibleIf(function () {
return false;
});
$schema->attribute('hidden3')->hiddenIf(function () {
return true;
});
});
$response = $api->handle($request);
$attributes = json_decode($response->getBody(), true)['data']['attributes'];
$this->assertArrayHasKey('visible1', $attributes);
$this->assertArrayHasKey('visible2', $attributes);
$this->assertArrayHasKey('visible3', $attributes);
$this->assertArrayHasKey('visible4', $attributes);
$this->assertArrayNotHasKey('hidden1', $attributes);
$this->assertArrayNotHasKey('hidden2', $attributes);
$this->assertArrayNotHasKey('hidden3', $attributes);
}
public function testHasOneRelationship()
{
$phonesAdapter = new MockAdapter([
'1' => $phone1 = (object) ['id' => '1', 'number' => '8881'],
'2' => $phone2 = (object) ['id' => '2', 'number' => '8882'],
'3' => $phone3 = (object) ['id' => '3', 'number' => '8883']
]);
$usersAdapter = new MockAdapter([
'1' => (object) [
'id' => '1',
'phone' => $phone1,
'property2' => $phone2,
'property3' => $phone3,
]
]);
$request = $this->buildRequest('GET', '/users/1')
->withQueryParams(['include' => 'phone,phone2,phone3']);
$api = new JsonApi('http://example.com');
$api->resource('users', $usersAdapter, function (Type $schema) {
$schema->hasOne('phone');
$schema->hasOne('phone2', 'phones', 'property2');
$schema->hasOne('phone3')
->type('phones')
->property('property3');
});
$api->resource('phones', $phonesAdapter, function (Type $schema) {
$schema->attribute('number');
});
$response = $api->handle($request);
$this->assertArraySubset(
[
'relationships' => [
'phone' => [
'data' => ['type' => 'phones', 'id' => '1']
],
'phone2' => [
'data' => ['type' => 'phones', 'id' => '2']
],
'phone3' => [
'data' => ['type' => 'phones', 'id' => '3']
]
]
],
json_decode($response->getBody(), true)['data']
);
$this->assertEquals(
[
[
'type' => 'phones',
'id' => '1',
'attributes' => ['number' => '8881'],
'links' => [
'self' => 'http://example.com/phones/1'
]
],
[
'type' => 'phones',
'id' => '2',
'attributes' => ['number' => '8882'],
'links' => [
'self' => 'http://example.com/phones/2'
]
],
[
'type' => 'phones',
'id' => '3',
'attributes' => ['number' => '8883'],
'links' => [
'self' => 'http://example.com/phones/3'
]
]
],
json_decode($response->getBody(), true)['included']
);
}
public function testHasOneRelationshipInclusion()
{
$phonesAdapter = new MockAdapter([
'1' => $phone1 = (object) ['id' => '1', 'number' => '8881'],
'2' => $phone2 = (object) ['id' => '2', 'number' => '8882']
]);
$usersAdapter = new MockAdapter([
'1' => (object) [
'id' => '1',
'phone' => $phone1,
'property2' => $phone2
]
]);
$request = $this->buildRequest('GET', '/users/1')
->withQueryParams(['include' => 'phone2']);
$api = new JsonApi('http://example.com');
$api->resource('users', $usersAdapter, function (Type $schema) {
$schema->hasOne('phone');
$schema->hasOne('phone2', 'phones', 'property2');
});
$api->resource('phones', $phonesAdapter, function (Type $schema) {
$schema->attribute('number');
});
$response = $api->handle($request);
$this->assertArraySubset(
[
'relationships' => [
'phone2' => [
'data' => ['type' => 'phones', 'id' => '2']
]
]
],
json_decode($response->getBody(), true)['data']
);
$this->assertEquals(
[
[
'type' => 'phones',
'id' => '2',
'attributes' => ['number' => '8882'],
'links' => [
'self' => 'http://example.com/phones/2'
]
]
],
json_decode($response->getBody(), true)['included']
);
$response = $api->handle(
$request->withQueryParams(['include' => 'phone'])
);
$this->assertArraySubset(
[
'relationships' => [
'phone' => [
'data' => ['type' => 'phones', 'id' => '1']
]
]
],
json_decode($response->getBody(), true)['data']
);
$this->assertEquals(
[
[
'type' => 'phones',
'id' => '1',
'attributes' => ['number' => '8881'],
'links' => [
'self' => 'http://example.com/phones/1'
]
]
],
json_decode($response->getBody(), true)['included']
);
}
public function testHasManyRelationshipNotIncludableByDefault()
{
$api = new JsonApi('http://example.com');
$api->resource('users', new MockAdapter(), function (Type $schema) {
$schema->hasMany('groups');
});
$request = $this->buildRequest('GET', '/users/1')
->withQueryParams(['include' => 'groups']);
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage('Invalid include [groups]');
$api->handle($request);
}
public function testHasManyRelationshipNotIncludedByDefault()
{
$usersAdapter = new MockAdapter([
'1' => (object) [
'id' => '1',
'groups' => [
(object) ['id' => '1'],
(object) ['id' => '2'],
]
]
]);
$api = new JsonApi('http://example.com');
$api->resource('users', $usersAdapter, function (Type $schema) {
$schema->hasMany('groups');
});
$api->resource('groups', new MockAdapter());
$request = $this->buildRequest('GET', '/users/1');
$response = $api->handle($request);
$body = json_decode($response->getBody(), true);
$this->assertArrayNotHasKey('data', $body['data']['relationships']);
$this->assertArrayNotHasKey('included', $body);
}
public function testHasManyRelationshipInclusion()
{
$groupsAdapter = new MockAdapter([
'1' => $group1 = (object) ['id' => '1', 'name' => 'Admin'],
'2' => $group2 = (object) ['id' => '2', 'name' => 'Mod'],
'3' => $group3 = (object) ['id' => '3', 'name' => 'Member'],
'4' => $group4 = (object) ['id' => '4', 'name' => 'Guest']
]);
$usersAdapter = new MockAdapter([
'1' => $user = (object) [
'id' => '1',
'property1' => [$group1, $group2],
'property2' => [$group3, $group4],
]
]);
$api = new JsonApi('http://example.com');
$relationships = [];
$api->resource('users', $usersAdapter, function (Type $schema) use (&$relationships) {
$relationships[] = $schema->hasMany('groups1', 'groups', 'property1')
->includable();
$relationships[] = $schema->hasMany('groups2', 'groups', 'property2')
->includable();
});
$api->resource('groups', $groupsAdapter, function (Type $schema) {
$schema->attribute('name');
});
$request = $this->buildRequest('GET', '/users/1')
->withQueryParams(['include' => 'groups1']);
$response = $api->handle($request);
$this->assertEquals([[$relationships[0]]], $user->load);
$this->assertArraySubset(
[
'relationships' => [
'groups1' => [
'data' => [
['type' => 'groups', 'id' => '1'],
['type' => 'groups', 'id' => '2']
]
]
]
],
json_decode($response->getBody(), true)['data']
);
$this->assertEquals(
[
[
'type' => 'groups',
'id' => '1',
'attributes' => ['name' => 'Admin'],
'links' => [
'self' => 'http://example.com/groups/1'
]
],
[
'type' => 'groups',
'id' => '2',
'attributes' => ['name' => 'Mod'],
'links' => [
'self' => 'http://example.com/groups/2'
]
]
],
json_decode($response->getBody(), true)['included']
);
$user->load = [];
$response = $api->handle(
$request->withQueryParams(['include' => 'groups2'])
);
$this->assertEquals([[$relationships[1]]], $user->load);
$this->assertArraySubset(
[
'relationships' => [
'groups2' => [
'data' => [
['type' => 'groups', 'id' => '3'],
['type' => 'groups', 'id' => '4'],
]
]
]
],
json_decode($response->getBody(), true)['data']
);
$this->assertEquals(
[
[
'type' => 'groups',
'id' => '3',
'attributes' => ['name' => 'Member'],
'links' => [
'self' => 'http://example.com/groups/3'
]
],
[
'type' => 'groups',
'id' => '4',
'attributes' => ['name' => 'Guest'],
'links' => [
'self' => 'http://example.com/groups/4'
]
],
],
json_decode($response->getBody(), true)['included']
);
}
public function testNestedRelationshipInclusion()
{
$groupsAdapter = new MockAdapter([
'1' => $group1 = (object) ['id' => '1', 'name' => 'Admin'],
'2' => $group2 = (object) ['id' => '2', 'name' => 'Mod']
]);
$usersAdapter = new MockAdapter([
'1' => $user = (object) ['id' => '1', 'groups' => [$group1, $group2]]
]);
$postsAdapter = new MockAdapter([
'1' => $post = (object) ['id' => '1', 'user' => $user]
]);
$api = new JsonApi('http://example.com');
$relationships = [];
$api->resource('posts', $postsAdapter, function (Type $schema) use (&$relationships) {
$relationships[] = $schema->hasOne('user');
});
$api->resource('users', $usersAdapter, function (Type $schema) use (&$relationships) {
$relationships[] = $schema->hasMany('groups')->includable();
});
$api->resource('groups', $groupsAdapter, function (Type $schema) {
$schema->attribute('name');
});
$request = $this->buildRequest('GET', '/posts/1')
->withQueryParams(['include' => 'user,user.groups']);
$response = $api->handle($request);
$this->assertEquals([$relationships[0]], $post->load[0]);
$this->assertEquals($relationships, $post->load[1]);
$this->assertArraySubset(
[
'relationships' => [
'user' => [
'data' => ['type' => 'users', 'id' => '1']
]
]
],
json_decode($response->getBody(), true)['data']
);
$included = json_decode($response->getBody(), true)['included'];
// $this->assertContains(
// [
// 'type' => 'users',
// 'id' => '1',
// 'relationships' => [
// 'groups' => [
// 'data' => [
// ['type' => 'groups', 'id' => '1'],
// ['type' => 'groups', 'id' => '2']
// ]
// ]
// ],
// 'links' => [
// 'self' => 'http://example.com/users/1'
// ]
// ],
// $included
// );
$this->assertContains(
[
'type' => 'groups',
'id' => '1',
'attributes' => ['name' => 'Admin'],
'links' => [
'self' => 'http://example.com/groups/1'
]
],
$included
);
$this->assertContains(
[
'type' => 'groups',
'id' => '2',
'attributes' => ['name' => 'Mod'],
'links' => [
'self' => 'http://example.com/groups/2'
]
],
$included
);
}
}

View File

@ -11,13 +11,28 @@
namespace Tobyz\Tests\JsonApiServer\feature; namespace Tobyz\Tests\JsonApiServer\feature;
use Psr\Http\Message\ServerRequestInterface;
use Tobyz\JsonApiServer\JsonApi;
use Tobyz\JsonApiServer\Schema\Type;
use Tobyz\Tests\JsonApiServer\AbstractTestCase; use Tobyz\Tests\JsonApiServer\AbstractTestCase;
use Tobyz\Tests\JsonApiServer\MockAdapter;
class FieldFiltersTest extends AbstractTestCase class FieldFiltersTest extends AbstractTestCase
{ {
/**
* @var JsonApi
*/
private $api;
/**
* @var MockAdapter
*/
private $adapter;
public function setUp(): void public function setUp(): void
{ {
$this->api = new JsonApi('http://example.com');
$this->adapter = new MockAdapter();
} }
public function test_resources_can_be_filtered_by_id() public function test_resources_can_be_filtered_by_id()
@ -53,6 +68,28 @@ class FieldFiltersTest extends AbstractTestCase
// to_one, to_many... // to_one, to_many...
public function test_types_can_have_custom_filters() public function test_types_can_have_custom_filters()
{
$called = false;
$this->api->resource('users', $this->adapter, function (Type $type) use (&$called) {
$type->filter('name', function (...$args) use (&$called) {
$this->assertSame($this->adapter->query, $args[0]);
$this->assertEquals('value', $args[1]);
$this->assertInstanceOf(ServerRequestInterface::class, $args[2]);
$called = true;
});
});
$this->api->handle(
$this->buildRequest('GET', '/users')
->withQueryParams(['filter' => ['name' => 'value']])
);
$this->assertTrue($called);
}
public function test_types_can_have_a_default_filter()
{ {
$this->markTestIncomplete(); $this->markTestIncomplete();
} }

View File

@ -45,6 +45,11 @@ class RelationshipInclusionTest extends AbstractTestCase
$this->markTestIncomplete(); $this->markTestIncomplete();
} }
public function test_to_one_relationships_can_be_not_loadable()
{
$this->markTestIncomplete();
}
// to_many... // to_many...
public function test_multiple_relationships_can_be_included() public function test_multiple_relationships_can_be_included()

View File

@ -30,30 +30,64 @@ class ScopesTest extends AbstractTestCase
*/ */
private $adapter; private $adapter;
private $scopeWasCalled = false;
public function setUp(): void public function setUp(): void
{ {
$this->api = new JsonApi('http://example.com');
$this->adapter = new MockAdapter(); $this->adapter = new MockAdapter();
$this->scopeWasCalled = false;
$this->api = new JsonApi('http://example.com');
$this->api->resource('users', $this->adapter, function (Type $type) {
$type->updatable();
$type->deletable();
$type->scope(function (...$args) {
$this->assertSame($this->adapter->query, $args[0]);
$this->assertInstanceOf(ServerRequestInterface::class, $args[1]);
$this->scopeWasCalled = true;
});
});
} }
public function test_scopes_are_applied_to_the_resource_listing_query() public function test_scopes_are_applied_to_the_resource_listing_query()
{ {
$this->markTestIncomplete(); $this->api->handle(
$this->buildRequest('GET', '/users')
);
$this->assertTrue($this->scopeWasCalled);
} }
public function test_scopes_are_applied_to_the_show_resource_query() public function test_scopes_are_applied_to_the_show_resource_query()
{ {
$this->markTestIncomplete(); $this->api->handle(
$this->buildRequest('GET', '/users/1')
);
$this->assertTrue($this->scopeWasCalled);
} }
public function test_scopes_are_applied_to_the_update_resource_query() public function test_scopes_are_applied_to_the_update_resource_query()
{ {
$this->markTestIncomplete(); $this->api->handle(
$this->buildRequest('PATCH', '/users/1')
->withParsedBody([
'data' => [
'type' => 'users',
'id' => '1'
]
])
);
$this->assertTrue($this->scopeWasCalled);
} }
public function test_scopes_are_applied_to_the_delete_resource_query() public function test_scopes_are_applied_to_the_delete_resource_query()
{ {
$this->markTestIncomplete(); $this->api->handle(
$this->buildRequest('DELETE', '/users/1')
);
$this->assertTrue($this->scopeWasCalled);
} }
} }