This commit is contained in:
Toby Zerner 2019-08-22 12:26:36 +09:30
parent ca9cd62711
commit 480a0fa9b6
11 changed files with 67 additions and 58 deletions

View File

@ -507,10 +507,6 @@ You should indicate to the server if the consumer is authenticated using the `au
$api->authenticated(); $api->authenticated();
``` ```
## Examples
- [Forust](https://github.com/forust/core/tree/master/schema) is forum software that uses tobyz/json-api-server to power its API.
## Contributing ## Contributing
Feel free to send pull requests or create issues if you come across problems or have great ideas. See the [Contributing Guide](https://github.com/tobyz/json-api-server/blob/master/CONTRIBUTING.md) for more information. Feel free to send pull requests or create issues if you come across problems or have great ideas. See the [Contributing Guide](https://github.com/tobyz/json-api-server/blob/master/CONTRIBUTING.md) for more information.

View File

@ -150,27 +150,20 @@ interface AdapterInterface
* *
* @param $model * @param $model
* @param HasOne $relationship * @param HasOne $relationship
* @param array $fields
* @return mixed|null * @return mixed|null
*/ */
public function getHasOne($model, HasOne $relationship); public function getHasOne($model, HasOne $relationship, array $fields = null);
/**
* 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. * Get a list of models for a has-many relationship for the model.
* *
* @param $model * @param $model
* @param HasMany $relationship * @param HasMany $relationship
* @param array $fields
* @return array * @return array
*/ */
public function getHasMany($model, HasMany $relationship): array; public function getHasMany($model, HasMany $relationship, array $fields = null): array;
/** /**
* Apply an attribute value to the model. * Apply an attribute value to the model.

View File

@ -69,28 +69,25 @@ class EloquentAdapter implements AdapterInterface
return $model->{$this->getAttributeProperty($attribute)}; return $model->{$this->getAttributeProperty($attribute)};
} }
public function getHasOneId($model, HasOne $relationship): ?string public function getHasOne($model, HasOne $relationship, array $fields = null)
{ {
$relation = $this->getRelation($model, $relationship); $relation = $this->getEloquentRelation($model, $relationship);
// If this is a belongs-to relation, we can simply return the value of // comment
// the foreign key on the model. Otherwise we will have to fetch the if ($fields === ['id'] && $relation instanceof BelongsTo) {
// full related model and return its key. if ($key = $model->{$relation->getForeignKeyName()}) {
if ($relation instanceof BelongsTo) { $related = $relation->getRelated();
return $model->{$relation->getForeignKeyName()};
return $related->newInstance()->forceFill([$related->getKeyName() => $key]);
}
return null;
} }
$related = $this->getRelationValue($model, $relationship);
return $related ? $related->getKey() : null;
}
public function getHasOne($model, HasOne $relationship)
{
return $this->getRelationValue($model, $relationship); return $this->getRelationValue($model, $relationship);
} }
public function getHasMany($model, HasMany $relationship): array public function getHasMany($model, HasMany $relationship, array $fields = null): array
{ {
$collection = $this->getRelationValue($model, $relationship); $collection = $this->getRelationValue($model, $relationship);
@ -104,7 +101,7 @@ class EloquentAdapter implements AdapterInterface
public function setHasOne($model, HasOne $relationship, $related): void public function setHasOne($model, HasOne $relationship, $related): void
{ {
$this->getRelation($model, $relationship)->associate($related); $this->getEloquentRelation($model, $relationship)->associate($related);
} }
public function save($model): void public function save($model): void
@ -114,7 +111,7 @@ class EloquentAdapter implements AdapterInterface
public function saveHasMany($model, HasMany $relationship, array $related): void public function saveHasMany($model, HasMany $relationship, array $related): void
{ {
$this->getRelation($model, $relationship)->sync(new Collection($related)); $this->getEloquentRelation($model, $relationship)->sync(new Collection($related));
} }
public function delete($model): void public function delete($model): void
@ -165,7 +162,7 @@ class EloquentAdapter implements AdapterInterface
public function filterByHasOne($query, HasOne $relationship, array $ids): void public function filterByHasOne($query, HasOne $relationship, array $ids): void
{ {
$relation = $this->getRelation($query->getModel(), $relationship); $relation = $this->getEloquentRelation($query->getModel(), $relationship);
$query->whereIn($relation->getQualifiedForeignKeyName(), $ids); $query->whereIn($relation->getQualifiedForeignKeyName(), $ids);
} }
@ -173,7 +170,7 @@ class EloquentAdapter implements AdapterInterface
public function filterByHasMany($query, HasMany $relationship, array $ids): void public function filterByHasMany($query, HasMany $relationship, array $ids): void
{ {
$property = $this->getRelationshipProperty($relationship); $property = $this->getRelationshipProperty($relationship);
$relation = $this->getRelation($query->getModel(), $relationship); $relation = $this->getEloquentRelation($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) {
@ -239,7 +236,7 @@ class EloquentAdapter implements AdapterInterface
return implode('.', array_map([$this, 'getRelationshipProperty'], $trail)); return implode('.', array_map([$this, 'getRelationshipProperty'], $trail));
} }
private function getRelation($model, Relationship $relationship) private function getEloquentRelation($model, Relationship $relationship)
{ {
return $model->{$this->getRelationshipProperty($relationship)}(); return $model->{$this->getRelationshipProperty($relationship)}();
} }

View File

@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use function Tobyz\JsonApiServer\evaluate; use function Tobyz\JsonApiServer\evaluate;
use function Tobyz\JsonApiServer\get_value; use function Tobyz\JsonApiServer\get_value;
use function Tobyz\JsonApiServer\has_value; use function Tobyz\JsonApiServer\has_value;
use function Tobyz\JsonApiServer\set_value;
use Tobyz\JsonApiServer\Exception\BadRequestException; use Tobyz\JsonApiServer\Exception\BadRequestException;
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException; use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
use function Tobyz\JsonApiServer\run_callbacks; use function Tobyz\JsonApiServer\run_callbacks;
@ -44,7 +45,7 @@ trait SavesData
); );
} }
private function getModelForIdentifier(Request $request, $identifier) private function getModelForIdentifier(Request $request, $identifier, array $validTypes = null)
{ {
if (! isset($identifier['type'])) { if (! isset($identifier['type'])) {
throw new BadRequestException('type not specified'); throw new BadRequestException('type not specified');
@ -54,6 +55,10 @@ trait SavesData
throw new BadRequestException('id not specified'); throw new BadRequestException('id not specified');
} }
if ($validTypes !== null && ! in_array($identifier['type'], $validTypes)) {
throw new BadRequestException("type [{$identifier['type']}] not allowed");
}
$resource = $this->api->getResource($identifier['type']); $resource = $this->api->getResource($identifier['type']);
return $this->findResource($request, $resource, $identifier['id']); return $this->findResource($request, $resource, $identifier['id']);
@ -94,16 +99,20 @@ trait SavesData
continue; continue;
} }
$value = &get_value($data, $field); $value = get_value($data, $field);
if (isset($value['data'])) { if (isset($value['data'])) {
$allowedTypes = $field->getAllowedTypes();
if ($field instanceof HasOne) { if ($field instanceof HasOne) {
$value = $this->getModelForIdentifier($request, $value['data']); set_value($data, $field, $this->getModelForIdentifier($request, $value['data'], $allowedTypes));
} elseif ($field instanceof HasMany) { } elseif ($field instanceof HasMany) {
$value = array_map(function ($identifier) use ($request) { set_value($data, $field, array_map(function ($identifier) use ($request, $allowedTypes) {
return $this->getModelForIdentifier($request, $identifier); return $this->getModelForIdentifier($request, $identifier, $allowedTypes);
}, $value['data']); }, $value['data']));
} }
} else {
set_value($data, $field, null);
} }
} }
} }

View File

@ -43,11 +43,11 @@ class Create implements RequestHandlerInterface
$this->assertDataValid($data, $model, $request, true); $this->assertDataValid($data, $model, $request, true);
$this->setValues($data, $model, $request); $this->setValues($data, $model, $request);
run_callbacks($schema->getListeners('creating'), [$request, $model]); run_callbacks($schema->getListeners('creating'), [$model, $request]);
$this->save($data, $model, $request); $this->save($data, $model, $request);
run_callbacks($schema->getListeners('created'), [$request, $model]); run_callbacks($schema->getListeners('created'), [$model, $request]);
return (new Show($this->api, $this->resource, $model)) return (new Show($this->api, $this->resource, $model))
->handle($request) ->handle($request)

View File

@ -26,15 +26,15 @@ class Delete implements RequestHandlerInterface
{ {
$schema = $this->resource->getSchema(); $schema = $this->resource->getSchema();
if (! evaluate($schema->getDeletable(), [$request, $this->model])) { if (! evaluate($schema->getDeletable(), [$this->model, $request])) {
throw new ForbiddenException; throw new ForbiddenException;
} }
run_callbacks($schema->getListeners('deleting'), [$request, $this->model]); run_callbacks($schema->getListeners('deleting'), [$this->model, $request]);
$this->resource->getAdapter()->delete($this->model); $this->resource->getAdapter()->delete($this->model);
run_callbacks($schema->getListeners('deleted'), [$request, $this->model]); run_callbacks($schema->getListeners('deleted'), [$this->model, $request]);
return new EmptyResponse; return new EmptyResponse;
} }

View File

@ -30,7 +30,7 @@ class Update implements RequestHandlerInterface
{ {
$schema = $this->resource->getSchema(); $schema = $this->resource->getSchema();
if (! evaluate($schema->getUpdatable(), [$request, $this->model])) { if (! evaluate($schema->getUpdatable(), [$this->model, $request])) {
throw new ForbiddenException; throw new ForbiddenException;
} }
@ -41,11 +41,11 @@ class Update implements RequestHandlerInterface
$this->assertDataValid($data, $this->model, $request, false); $this->assertDataValid($data, $this->model, $request, false);
$this->setValues($data, $this->model, $request); $this->setValues($data, $this->model, $request);
run_callbacks($schema->getListeners('updating'), [$request, $this->model]); run_callbacks($schema->getListeners('updating'), [$this->model, $request]);
$this->save($data, $this->model, $request); $this->save($data, $this->model, $request);
run_callbacks($schema->getListeners('updated'), [$request, $this->model]); run_callbacks($schema->getListeners('updated'), [$this->model, $request]);
return (new Show($this->api, $this->resource, $this->model))->handle($request); return (new Show($this->api, $this->resource, $this->model))->handle($request);
} }

View File

@ -8,6 +8,7 @@ use function Tobyz\JsonApiServer\negate;
abstract class Relationship extends Field abstract class Relationship extends Field
{ {
private $type; private $type;
private $allowedTypes;
private $linkage = false; private $linkage = false;
private $links = true; private $links = true;
private $loadable = true; private $loadable = true;
@ -20,9 +21,10 @@ abstract class Relationship extends Field
return $this; return $this;
} }
public function polymorphic() public function polymorphic(array $types = null)
{ {
$this->type = null; $this->type = null;
$this->allowedTypes = $types;
return $this; return $this;
} }
@ -69,11 +71,16 @@ abstract class Relationship extends Field
return $this; return $this;
} }
public function getType() public function getType(): ?string
{ {
return $this->type; return $this->type;
} }
public function getAllowedTypes(): ?array
{
return $this->allowedTypes;
}
public function links() public function links()
{ {
$this->links = true; $this->links = true;

View File

@ -125,7 +125,7 @@ final class Serializer
{ {
$links = $this->getRelationshipLinks($field, $resourceUrl); $links = $this->getRelationshipLinks($field, $resourceUrl);
$value = $isIncluded ? (($getter = $field->getGetter()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->getLoadable() ? $adapter->getHasOneId($model, $field) : null); $value = $isIncluded ? (($getter = $field->getGetter()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->getLoadable() ? $adapter->getHasOne($model, $field, ['id']) : null);
if (! $value) { if (! $value) {
return new Structure\ToNull( return new Structure\ToNull(
@ -207,7 +207,7 @@ final class Serializer
private function resourceForModel($model) private function resourceForModel($model)
{ {
foreach ($this->api->getResources() as $resource) { foreach ($this->api->getResources() as $resource) {
if ($resource->getAdapter()->handles($model)) { if ($resource->getAdapter()->represents($model)) {
return $resource; return $resource;
} }
} }
@ -270,11 +270,13 @@ final class Serializer
private function relatedResourceIdentifier(Schema\Relationship $field, $model) private function relatedResourceIdentifier(Schema\Relationship $field, $model)
{ {
$relatedResource = $this->api->getResource($type = $field->getType()); $type = $field->getType();
$relatedResource = $type ? $this->api->getResource($type) : $this->resourceForModel($model);
return $this->resourceIdentifier([ return $this->resourceIdentifier([
'type' => $type, 'type' => $relatedResource->getType(),
'id' => is_string($model) ? $model : $relatedResource->getAdapter()->getId($model) 'id' => $relatedResource->getAdapter()->getId($model)
]); ]);
} }
} }

View File

@ -40,9 +40,9 @@ function has_value(array $data, Field $field)
return isset($data[$field->getLocation()][$field->getName()]); return isset($data[$field->getLocation()][$field->getName()]);
} }
function &get_value(array $data, Field $field) function get_value(array $data, Field $field)
{ {
return $data[$field->getLocation()][$field->getName()]; return $data[$field->getLocation()][$field->getName()] ?? null;
} }
function set_value(array &$data, Field $field, $value) function set_value(array &$data, Field $field, $value)

View File

@ -40,5 +40,10 @@ class RelationshipTypesTest extends AbstractTestCase
$this->markTestIncomplete(); $this->markTestIncomplete();
} }
public function test_polymorphic_create_update()
{
$this->markTestIncomplete();
}
// to_many... // to_many...
} }