wip
This commit is contained in:
parent
602a6cca7d
commit
9ee46b3ee0
|
|
@ -44,7 +44,5 @@ interface AdapterInterface
|
||||||
|
|
||||||
public function paginate($query, int $limit, int $offset);
|
public function paginate($query, int $limit, int $offset);
|
||||||
|
|
||||||
public function include($query, array $relationships);
|
public function load(array $models, array $relationships);
|
||||||
|
|
||||||
public function load($model, array $relationships);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ namespace Tobscure\JsonApiServer\Adapter;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Tobscure\JsonApiServer\Schema\Attribute;
|
use Tobscure\JsonApiServer\Schema\Attribute;
|
||||||
use Tobscure\JsonApiServer\Schema\HasMany;
|
use Tobscure\JsonApiServer\Schema\HasMany;
|
||||||
use Tobscure\JsonApiServer\Schema\HasOne;
|
use Tobscure\JsonApiServer\Schema\HasOne;
|
||||||
|
use Tobscure\JsonApiServer\Schema\Relationship;
|
||||||
|
|
||||||
class EloquentAdapter implements AdapterInterface
|
class EloquentAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
|
|
@ -44,6 +46,11 @@ class EloquentAdapter implements AdapterInterface
|
||||||
return $query->get()->all();
|
return $query->get()->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function count($query): int
|
||||||
|
{
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId($model): string
|
public function getId($model): string
|
||||||
{
|
{
|
||||||
return $model->getKey();
|
return $model->getKey();
|
||||||
|
|
@ -51,27 +58,48 @@ class EloquentAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function getAttribute($model, Attribute $field)
|
public function getAttribute($model, Attribute $field)
|
||||||
{
|
{
|
||||||
return $model->{$field->property};
|
return $model->{$this->getAttributeProperty($field)};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHasOneId($model, HasOne $field)
|
||||||
|
{
|
||||||
|
$relation = $model->{$this->getRelationshipProperty($field)}();
|
||||||
|
|
||||||
|
if ($relation instanceof BelongsTo) {
|
||||||
|
$related = $relation->getRelated();
|
||||||
|
|
||||||
|
$key = $model->{$relation->getForeignKeyName()};
|
||||||
|
|
||||||
|
if ($key) {
|
||||||
|
return $related->forceFill([$related->getKeyName() => $key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $model->{$this->getRelationshipProperty($field)};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasOne($model, HasOne $field)
|
public function getHasOne($model, HasOne $field)
|
||||||
{
|
{
|
||||||
return $model->{$field->property};
|
return $model->{$this->getRelationshipProperty($field)};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasMany($model, HasMany $field): array
|
public function getHasMany($model, HasMany $field): array
|
||||||
{
|
{
|
||||||
return $model->{$field->property}->all();
|
$collection = $model->{$this->getRelationshipProperty($field)};
|
||||||
|
|
||||||
|
return $collection ? $collection->all() : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyAttribute($model, Attribute $field, $value)
|
public function applyAttribute($model, Attribute $field, $value)
|
||||||
{
|
{
|
||||||
$model->{$field->property} = $value;
|
$model->{$this->getAttributeProperty($field)} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyHasOne($model, HasOne $field, $related)
|
public function applyHasOne($model, HasOne $field, $related)
|
||||||
{
|
{
|
||||||
$model->{$field->property}()->associate($related);
|
$model->{$this->getRelationshipProperty($field)}()->associate($related);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save($model)
|
public function save($model)
|
||||||
|
|
@ -81,7 +109,7 @@ class EloquentAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function saveHasMany($model, HasMany $field, array $related)
|
public function saveHasMany($model, HasMany $field, array $related)
|
||||||
{
|
{
|
||||||
$model->{$field->property}()->sync(Collection::make($related));
|
$model->{$this->getRelationshipProperty($field)}()->sync(Collection::make($related));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($model)
|
public function delete($model)
|
||||||
|
|
@ -91,12 +119,12 @@ class EloquentAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function filterByAttribute($query, Attribute $field, $value)
|
public function filterByAttribute($query, Attribute $field, $value)
|
||||||
{
|
{
|
||||||
$query->where($field->property, $value);
|
$query->where($this->getAttributeProperty($field), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function filterByHasOne($query, HasOne $field, array $ids)
|
public function filterByHasOne($query, HasOne $field, array $ids)
|
||||||
{
|
{
|
||||||
$property = $field->property;
|
$property = $this->getRelationshipProperty($field);
|
||||||
$foreignKey = $query->getModel()->{$property}()->getQualifiedForeignKey();
|
$foreignKey = $query->getModel()->{$property}()->getQualifiedForeignKey();
|
||||||
|
|
||||||
$query->whereIn($foreignKey, $ids);
|
$query->whereIn($foreignKey, $ids);
|
||||||
|
|
@ -104,7 +132,7 @@ class EloquentAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function filterByHasMany($query, HasMany $field, array $ids)
|
public function filterByHasMany($query, HasMany $field, array $ids)
|
||||||
{
|
{
|
||||||
$property = $field->property;
|
$property = $this->getRelationshipProperty($field);
|
||||||
$relation = $query->getModel()->{$property}();
|
$relation = $query->getModel()->{$property}();
|
||||||
$relatedKey = $relation->getRelated()->getQualifiedKeyName();
|
$relatedKey = $relation->getRelated()->getQualifiedKeyName();
|
||||||
|
|
||||||
|
|
@ -115,7 +143,7 @@ class EloquentAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function sortByAttribute($query, Attribute $field, string $direction)
|
public function sortByAttribute($query, Attribute $field, string $direction)
|
||||||
{
|
{
|
||||||
$query->orderBy($field->property, $direction);
|
$query->orderBy($this->getAttributeProperty($field), $direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function paginate($query, int $limit, int $offset)
|
public function paginate($query, int $limit, int $offset)
|
||||||
|
|
@ -123,20 +151,48 @@ class EloquentAdapter implements AdapterInterface
|
||||||
$query->take($limit)->skip($offset);
|
$query->take($limit)->skip($offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function include($query, array $trail)
|
public function load(array $models, array $trail)
|
||||||
{
|
{
|
||||||
$query->with($this->relationshipTrailToPath($trail));
|
(new Collection($models))->load($this->relationshipTrailToPath($trail));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load($model, array $trail)
|
public function loadIds(array $models, Relationship $relationship)
|
||||||
{
|
{
|
||||||
$model->load($this->relationshipTrailToPath($trail));
|
if (empty($models)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$property = $this->getRelationshipProperty($relationship);
|
||||||
|
$relation = $models[0]->$property();
|
||||||
|
|
||||||
|
if ($relation instanceof BelongsTo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(new Collection($models))->load([
|
||||||
|
$property => function ($query) use ($relation) {
|
||||||
|
$query->select([
|
||||||
|
$relation->getRelated()->getKeyName(),
|
||||||
|
$relation->getForeignKeyName()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAttributeProperty(Attribute $field)
|
||||||
|
{
|
||||||
|
return $field->property ?: strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $field->name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRelationshipProperty(Relationship $field)
|
||||||
|
{
|
||||||
|
return $field->property ?: $field->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function relationshipTrailToPath(array $trail)
|
private function relationshipTrailToPath(array $trail)
|
||||||
{
|
{
|
||||||
return implode('.', array_map(function ($relationship) {
|
return implode('.', array_map(function ($relationship) {
|
||||||
return $relationship->property;
|
return $this->getRelationshipProperty($relationship);
|
||||||
}, $trail));
|
}, $trail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
src/Api.php
46
src/Api.php
|
|
@ -7,7 +7,9 @@ use JsonApiPhp\JsonApi;
|
||||||
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 Tobscure\JsonApiServer\Exception\BadRequestException;
|
||||||
use Tobscure\JsonApiServer\Exception\MethodNotAllowedException;
|
use Tobscure\JsonApiServer\Exception\MethodNotAllowedException;
|
||||||
|
use Tobscure\JsonApiServer\Exception\NotImplementedException;
|
||||||
use Tobscure\JsonApiServer\Exception\ResourceNotFoundException;
|
use Tobscure\JsonApiServer\Exception\ResourceNotFoundException;
|
||||||
use Tobscure\JsonApiServer\Handler\Concerns\FindsResources;
|
use Tobscure\JsonApiServer\Handler\Concerns\FindsResources;
|
||||||
|
|
||||||
|
|
@ -51,10 +53,10 @@ class Api implements RequestHandlerInterface
|
||||||
if ($count === 1) {
|
if ($count === 1) {
|
||||||
switch ($request->getMethod()) {
|
switch ($request->getMethod()) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
return (new Handler\Index($this, $resource))->handle($request);
|
return $this->handleWithHandler($request, new Handler\Index($this, $resource));
|
||||||
|
|
||||||
case 'POST':
|
case 'POST':
|
||||||
return (new Handler\Create($this, $resource))->handle($request);
|
return $this->handleWithHandler($request, new Handler\Create($this, $resource));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new MethodNotAllowedException;
|
throw new MethodNotAllowedException;
|
||||||
|
|
@ -66,28 +68,32 @@ class Api implements RequestHandlerInterface
|
||||||
if ($count === 2) {
|
if ($count === 2) {
|
||||||
switch ($request->getMethod()) {
|
switch ($request->getMethod()) {
|
||||||
case 'PATCH':
|
case 'PATCH':
|
||||||
return (new Handler\Update($this, $resource, $model))->handle($request);
|
return $this->handleWithHandler($request, new Handler\Update($this, $resource, $model));
|
||||||
|
|
||||||
case 'GET':
|
case 'GET':
|
||||||
return (new Handler\Show($this, $resource, $model))->handle($request);
|
return $this->handleWithHandler($request, new Handler\Show($this, $resource, $model));
|
||||||
|
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
return (new Handler\Delete($resource, $model))->handle($request);
|
return $this->handleWithHandler($request, new Handler\Delete($resource, $model));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new MethodNotAllowedException;
|
throw new MethodNotAllowedException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if ($count === 3) {
|
if ($count === 3) {
|
||||||
|
throw new NotImplementedException;
|
||||||
|
|
||||||
// return $this->handleRelated($request, $resource, $model, $segments[2]);
|
// return $this->handleRelated($request, $resource, $model, $segments[2]);
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
if ($count === 4 && $segments[2] === 'relationships') {
|
||||||
|
throw new NotImplementedException;
|
||||||
|
|
||||||
// if ($count === 4 && $segments[2] === 'relationship') {
|
|
||||||
// return $this->handleRelationship($request, $resource, $model, $segments[3]);
|
// return $this->handleRelationship($request, $resource, $model, $segments[3]);
|
||||||
// }
|
}
|
||||||
|
|
||||||
throw new \RuntimeException;
|
throw new BadRequestException;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stripBasePath(string $path): string
|
private function stripBasePath(string $path): string
|
||||||
|
|
@ -103,13 +109,23 @@ class Api implements RequestHandlerInterface
|
||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function error(\Throwable $e)
|
private function handleWithHandler(Request $request, RequestHandlerInterface $handler)
|
||||||
{
|
{
|
||||||
|
$request = $request->withAttribute('jsonApiHandler', $handler);
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleError($e)
|
||||||
|
{
|
||||||
|
if (! $e instanceof ErrorProviderInterface) {
|
||||||
|
$e = new Exception\InternalServerErrorException;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $e->getJsonApiErrors();
|
||||||
|
|
||||||
$data = new JsonApi\ErrorDocument(
|
$data = new JsonApi\ErrorDocument(
|
||||||
new JsonApi\Error(
|
...$errors
|
||||||
new JsonApi\Error\Title($e->getMessage()),
|
|
||||||
new JsonApi\Error\Detail((string) $e)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return new JsonApiResponse($data);
|
return new JsonApiResponse($data);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobscure\JsonApiServer;
|
||||||
|
|
||||||
|
interface ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,41 @@
|
||||||
|
|
||||||
namespace Tobscure\JsonApiServer\Exception;
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
class BadRequestException extends \DomainException
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class BadRequestException extends \DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $sourceParameter;
|
||||||
|
|
||||||
|
public function __construct(string $message = '', string $sourceParameter = '')
|
||||||
|
{
|
||||||
|
parent::__construct($message);
|
||||||
|
|
||||||
|
$this->sourceParameter = $sourceParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
$members = [];
|
||||||
|
|
||||||
|
if ($this->message) {
|
||||||
|
$members[] = new Error\Detail($this->message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->sourceParameter) {
|
||||||
|
$members[] = new Error\SourceParameter($this->sourceParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Bad Request'),
|
||||||
|
new Error\Status('400'),
|
||||||
|
...$members
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
namespace Tobscure\JsonApiServer\Exception;
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
class ForbiddenException extends \DomainException
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class BadRequestException extends \DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Forbidden'),
|
||||||
|
new Error\Status('403')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class InternalServerErrorException extends \RuntimeException implements ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Internal Server Error'),
|
||||||
|
new Error\Status('500')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
namespace Tobscure\JsonApiServer\Exception;
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
use Exception;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class MethodNotAllowedException extends \DomainException
|
class BadRequestException extends \DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
public function __construct($message = null, $code = 405, Exception $previous = null)
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
parent::__construct($message, $code, $previous);
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Method Not Allowed'),
|
||||||
|
new Error\Status('405')
|
||||||
|
)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
|
class NotImplementedException extends \DomainException implements ErrorProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Not Implemented'),
|
||||||
|
new Error\Status('501')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,21 +2,42 @@
|
||||||
|
|
||||||
namespace Tobscure\JsonApiServer\Exception;
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
use RuntimeException;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class ResourceNotFoundException extends RuntimeException
|
class ResourceNotFoundException extends \RuntimeException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
protected $type;
|
protected $type;
|
||||||
|
protected $id;
|
||||||
|
|
||||||
public function __construct(string $type, $id = null)
|
public function __construct(string $type, string $id = null)
|
||||||
{
|
{
|
||||||
parent::__construct("Resource [$type".($id !== null ? ".$id" : '').'] not found.');
|
parent::__construct(
|
||||||
|
sprintf('Resource [%s] not found.', $type.($id !== null ? '.'.$id : ''))
|
||||||
|
);
|
||||||
|
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
|
$this->id = $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatusCode()
|
public function getJsonApiErrors(): array
|
||||||
{
|
{
|
||||||
return 404;
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Resource Not Found'),
|
||||||
|
new Error\Status('404'),
|
||||||
|
new Error\Detail($this->getMessage())
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?string
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,18 @@
|
||||||
|
|
||||||
namespace Tobscure\JsonApiServer\Exception;
|
namespace Tobscure\JsonApiServer\Exception;
|
||||||
|
|
||||||
use Exception;
|
use JsonApiPhp\JsonApi\Error;
|
||||||
|
use Tobscure\JsonApiServer\ErrorProviderInterface;
|
||||||
|
|
||||||
class UnprocessableEntityException extends \DomainException
|
class UnprocessableEntityException extends \DomainException implements ErrorProviderInterface
|
||||||
{
|
{
|
||||||
|
public function getJsonApiErrors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new Error(
|
||||||
|
new Error\Title('Unprocessable Entity'),
|
||||||
|
new Error\Status('422')
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ trait FindsResources
|
||||||
$query = $adapter->query();
|
$query = $adapter->query();
|
||||||
|
|
||||||
foreach ($resource->getSchema()->scopes as $scope) {
|
foreach ($resource->getSchema()->scopes as $scope) {
|
||||||
$scope($query, $request);
|
$scope($request, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($resource->getSchema()->singleScopes as $scope) {
|
||||||
|
$scope($request, $query, $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$model = $adapter->find($query, $id);
|
$model = $adapter->find($query, $id);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ trait IncludesData
|
||||||
return $include;
|
return $include;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->defaultInclude($this->resource);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseInclude(string $include): array
|
private function parseInclude(string $include): array
|
||||||
|
|
@ -54,7 +54,7 @@ trait IncludesData
|
||||||
|| ! $schema->fields[$name] instanceof Relationship
|
|| ! $schema->fields[$name] instanceof Relationship
|
||||||
|| ($schema->fields[$name] instanceof HasMany && ! $schema->fields[$name]->includable)
|
|| ($schema->fields[$name] instanceof HasMany && ! $schema->fields[$name]->includable)
|
||||||
) {
|
) {
|
||||||
throw new BadRequestException("Invalid include [{$path}{$name}]");
|
throw new BadRequestException("Invalid include [{$path}{$name}]", 'include');
|
||||||
}
|
}
|
||||||
|
|
||||||
$relatedResource = $this->api->getResource($schema->fields[$name]->resource);
|
$relatedResource = $this->api->getResource($schema->fields[$name]->resource);
|
||||||
|
|
@ -63,23 +63,6 @@ trait IncludesData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function defaultInclude(ResourceType $resource): array
|
|
||||||
{
|
|
||||||
$include = [];
|
|
||||||
|
|
||||||
foreach ($resource->getSchema()->fields as $name => $field) {
|
|
||||||
if (! $field instanceof Relationship || ! $field->included) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$include[$name] = $this->defaultInclude(
|
|
||||||
$this->api->getResource($field->resource)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $include;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildRelationshipTrails(ResourceType $resource, array $include): array
|
private function buildRelationshipTrails(ResourceType $resource, array $include): array
|
||||||
{
|
{
|
||||||
$schema = $resource->getSchema();
|
$schema = $resource->getSchema();
|
||||||
|
|
@ -88,7 +71,9 @@ trait IncludesData
|
||||||
foreach ($include as $name => $nested) {
|
foreach ($include as $name => $nested) {
|
||||||
$relationship = $schema->fields[$name];
|
$relationship = $schema->fields[$name];
|
||||||
|
|
||||||
|
if ($relationship->loadable) {
|
||||||
$trails[] = [$relationship];
|
$trails[] = [$relationship];
|
||||||
|
}
|
||||||
|
|
||||||
$relatedResource = $this->api->getResource($relationship->resource);
|
$relatedResource = $this->api->getResource($relationship->resource);
|
||||||
|
|
||||||
|
|
@ -105,4 +90,24 @@ trait IncludesData
|
||||||
|
|
||||||
return $trails;
|
return $trails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadRelationships(array $models, array $include, Request $request)
|
||||||
|
{
|
||||||
|
$adapter = $this->resource->getAdapter();
|
||||||
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
foreach ($schema->fields as $name => $field) {
|
||||||
|
if (! $field instanceof Relationship || ! ($field->linkage)($request) || ! $field->loadable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$adapter->loadIds($models, $field);
|
||||||
|
}
|
||||||
|
|
||||||
|
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
||||||
|
|
||||||
|
foreach ($trails as $relationships) {
|
||||||
|
$adapter->load($models, $relationships);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ trait SavesData
|
||||||
$adapter->save($model);
|
$adapter->save($model);
|
||||||
|
|
||||||
$this->saveFields($data, $model, $request);
|
$this->saveFields($data, $model, $request);
|
||||||
|
|
||||||
|
$this->runSavedCallbacks($data, $model, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parseData($body): array
|
private function parseData($body): array
|
||||||
|
|
@ -65,8 +67,12 @@ trait SavesData
|
||||||
|
|
||||||
private function getModelForIdentifier(Request $request, $identifier)
|
private function getModelForIdentifier(Request $request, $identifier)
|
||||||
{
|
{
|
||||||
if (! isset($identifier['type']) || ! isset($identifier['id'])) {
|
if (! isset($identifier['type'])) {
|
||||||
throw new BadRequestException('type/id not specified');
|
throw new BadRequestException('type not specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($identifier['id'])) {
|
||||||
|
throw new BadRequestException('id not specified');
|
||||||
}
|
}
|
||||||
|
|
||||||
$resource = $this->api->getResource($identifier['type']);
|
$resource = $this->api->getResource($identifier['type']);
|
||||||
|
|
@ -96,7 +102,7 @@ trait SavesData
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($schema->fields as $name => $field) {
|
||||||
$valueProvided = isset($data[$field->location][$name]);
|
$valueProvided = isset($data[$field->location][$name]);
|
||||||
|
|
||||||
if ($valueProvided && ! ($field->isWritable)($model, $request)) {
|
if ($valueProvided && ! ($field->isWritable)($request, $model)) {
|
||||||
throw new BadRequestException("Field [$name] is not writable");
|
throw new BadRequestException("Field [$name] is not writable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +158,7 @@ trait SavesData
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach ($field->validators as $validator) {
|
foreach ($field->validators as $validator) {
|
||||||
$validator($fail, $data[$field->location][$name], $model, $request);
|
$validator($fail, $data[$field->location][$name] ?? null, $request, $model, $field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,10 +208,25 @@ trait SavesData
|
||||||
$value = $data[$field->location][$name];
|
$value = $data[$field->location][$name];
|
||||||
|
|
||||||
if ($field->saver) {
|
if ($field->saver) {
|
||||||
($field->saver)($model, $value, $request);
|
($field->saver)($request, $model, $value);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
} elseif ($field instanceof Schema\HasMany) {
|
||||||
$adapter->saveHasMany($model, $field, $value);
|
$adapter->saveHasMany($model, $field, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function runSavedCallbacks(array $data, $model, Request $request)
|
||||||
|
{
|
||||||
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
foreach ($schema->fields as $name => $field) {
|
||||||
|
if (! isset($data[$field->location][$name])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($field->savedCallbacks as $callback) {
|
||||||
|
$callback($request, $model, $data[$field->location][$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,24 @@ class Create implements RequestHandlerInterface
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
if (! ($this->resource->getSchema()->isCreatable)($request)) {
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
if (! ($schema->isCreatable)($request)) {
|
||||||
throw new ForbiddenException('You cannot create this resource');
|
throw new ForbiddenException('You cannot create this resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
$model = $this->resource->getAdapter()->create();
|
$model = $this->resource->getAdapter()->create();
|
||||||
|
|
||||||
|
foreach ($schema->creatingCallbacks as $callback) {
|
||||||
|
$callback($request, $model);
|
||||||
|
}
|
||||||
|
|
||||||
$this->save($model, $request, true);
|
$this->save($model, $request, true);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,22 @@ class Delete implements RequestHandlerInterface
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
if (! ($this->resource->getSchema()->isDeletable)($this->model, $request)) {
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
if (! ($schema->isDeletable)($request, $this->model)) {
|
||||||
throw new ForbiddenException('You cannot delete this resource');
|
throw new ForbiddenException('You cannot delete this resource');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($schema->deletingCallbacks as $callback) {
|
||||||
|
$callback($request, $this->model);
|
||||||
|
}
|
||||||
|
|
||||||
$this->resource->getAdapter()->delete($this->model);
|
$this->resource->getAdapter()->delete($this->model);
|
||||||
|
|
||||||
|
foreach ($schema->deletedCallbacks as $callback) {
|
||||||
|
$callback($request, $this->model);
|
||||||
|
}
|
||||||
|
|
||||||
return new EmptyResponse;
|
return new EmptyResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace Tobscure\JsonApiServer\Handler;
|
namespace Tobscure\JsonApiServer\Handler;
|
||||||
|
|
||||||
use JsonApiPhp\JsonApi;
|
use JsonApiPhp\JsonApi;
|
||||||
|
use JsonApiPhp\JsonApi\Link;
|
||||||
|
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;
|
||||||
|
|
@ -30,7 +32,71 @@ class Index implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
$include = $this->getInclude($request);
|
$include = $this->getInclude($request);
|
||||||
|
|
||||||
$models = $this->getModels($include, $request);
|
$request = $this->extractQueryParams($request);
|
||||||
|
|
||||||
|
$adapter = $this->resource->getAdapter();
|
||||||
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
$query = $adapter->query();
|
||||||
|
|
||||||
|
foreach ($schema->scopes as $scope) {
|
||||||
|
$scope($request, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($schema->indexScopes as $scope) {
|
||||||
|
$scope($request, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($filter = $request->getAttribute('jsonApiFilter')) {
|
||||||
|
$this->filter($query, $filter, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = $request->getAttribute('jsonApiOffset');
|
||||||
|
$limit = $request->getAttribute('jsonApiLimit');
|
||||||
|
$total = null;
|
||||||
|
|
||||||
|
$paginationLinks = [];
|
||||||
|
$members = [
|
||||||
|
new Link\SelfLink($this->buildUrl($request))
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($offset > 0) {
|
||||||
|
$paginationLinks[] = new Link\FirstLink($this->buildUrl($request, ['page' => ['offset' => 0]]));
|
||||||
|
|
||||||
|
$prevOffset = $offset - $limit;
|
||||||
|
|
||||||
|
if ($prevOffset < 0) {
|
||||||
|
$params = ['page' => ['offset' => 0, 'limit' => $offset]];
|
||||||
|
} else {
|
||||||
|
$params = ['page' => ['offset' => max(0, $prevOffset)]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginationLinks[] = new Link\PrevLink($this->buildUrl($request, $params));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($schema->countable) {
|
||||||
|
$total = $adapter->count($query);
|
||||||
|
|
||||||
|
$members[] = new Meta('total', $total);
|
||||||
|
|
||||||
|
if ($offset + $limit < $total) {
|
||||||
|
$paginationLinks[] = new Link\LastLink($this->buildUrl($request, ['page' => ['offset' => floor(($total - 1) / $limit) * $limit]]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sort = $request->getAttribute('jsonApiSort')) {
|
||||||
|
$this->sort($query, $sort, $request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->paginate($query, $request);
|
||||||
|
|
||||||
|
$models = $adapter->get($query);
|
||||||
|
|
||||||
|
if ((count($models) && $total === null) || $offset + $limit < $total) {
|
||||||
|
$paginationLinks[] = new Link\NextLink($this->buildUrl($request, ['page' => ['offset' => $offset + $limit]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadRelationships($models, $include, $request);
|
||||||
|
|
||||||
$serializer = new Serializer($this->api, $request);
|
$serializer = new Serializer($this->api, $request);
|
||||||
|
|
||||||
|
|
@ -40,52 +106,108 @@ class Index implements RequestHandlerInterface
|
||||||
|
|
||||||
return new JsonApiResponse(
|
return new JsonApiResponse(
|
||||||
new JsonApi\CompoundDocument(
|
new JsonApi\CompoundDocument(
|
||||||
new JsonApi\ResourceCollection(...$serializer->primary()),
|
new JsonApi\PaginatedCollection(
|
||||||
new JsonApi\Included(...$serializer->included())
|
new JsonApi\Pagination(...$paginationLinks),
|
||||||
|
new JsonApi\ResourceCollection(...$serializer->primary())
|
||||||
|
),
|
||||||
|
new JsonApi\Included(...$serializer->included()),
|
||||||
|
...$members
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getModels(array $include, Request $request)
|
private function buildUrl(Request $request, array $overrideParams = []): string
|
||||||
{
|
{
|
||||||
$adapter = $this->resource->getAdapter();
|
[$selfUrl] = explode('?', $request->getUri(), 2);
|
||||||
|
$queryParams = $request->getQueryParams();
|
||||||
|
|
||||||
$query = $adapter->query();
|
$queryParams = array_replace_recursive($queryParams, $overrideParams);
|
||||||
|
|
||||||
foreach ($this->resource->getSchema()->scopes as $scope) {
|
if (isset($queryParams['page']['offset']) && $queryParams['page']['offset'] <= 0) {
|
||||||
$scope($query, $request);
|
unset($queryParams['page']['offset']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$queryString = http_build_query($queryParams);
|
||||||
|
|
||||||
|
return $selfUrl.($queryString ? '?'.$queryString : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractQueryParams(Request $request): Request
|
||||||
|
{
|
||||||
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
|
|
||||||
if (isset($queryParams['sort'])) {
|
$limit = $this->resource->getSchema()->paginate;
|
||||||
$this->sort($query, $queryParams['sort'], $request);
|
|
||||||
|
if (isset($queryParams['page']['limit'])) {
|
||||||
|
$limit = $queryParams['page']['limit'];
|
||||||
|
|
||||||
|
if ((! is_int($limit) && ! ctype_digit($limit)) || $limit < 1) {
|
||||||
|
throw new BadRequestException('page[limit] must be a positive integer', 'page[limit]');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($queryParams['filter'])) {
|
$limit = min($this->resource->getSchema()->limit, $limit);
|
||||||
$this->filter($query, $queryParams['filter'], $request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->paginate($query, $request);
|
$offset = 0;
|
||||||
|
|
||||||
$this->include($query, $include);
|
if (isset($queryParams['page']['offset'])) {
|
||||||
|
$offset = $queryParams['page']['offset'];
|
||||||
|
|
||||||
return $adapter->get($query);
|
if ((! is_int($offset) && ! ctype_digit($offset)) || $offset < 0) {
|
||||||
|
throw new BadRequestException('page[offset] must be a non-negative integer', 'page[offset]');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sort($query, string $sort, Request $request)
|
$request = $request
|
||||||
{
|
->withAttribute('jsonApiLimit', $limit)
|
||||||
$schema = $this->resource->getSchema();
|
->withAttribute('jsonApiOffset', $offset);
|
||||||
$adapter = $this->resource->getAdapter();
|
|
||||||
|
|
||||||
foreach ($this->parseSort($sort) as $name => $direction) {
|
$sort = $queryParams['sort'] ?? $this->resource->getSchema()->defaultSort;
|
||||||
|
|
||||||
|
if ($sort) {
|
||||||
|
$sort = $this->parseSort($sort);
|
||||||
|
|
||||||
|
foreach ($sort as $name => $direction) {
|
||||||
if (! isset($schema->fields[$name])
|
if (! isset($schema->fields[$name])
|
||||||
|| ! $schema->fields[$name] instanceof Schema\Attribute
|
|| ! $schema->fields[$name] instanceof Schema\Attribute
|
||||||
|| ! $schema->fields[$name]->sortable
|
|| ! $schema->fields[$name]->sortable
|
||||||
) {
|
) {
|
||||||
throw new BadRequestException("Invalid sort field [$name]");
|
throw new BadRequestException("Invalid sort field [$name]", 'sort');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$request = $request->withAttribute('jsonApiSort', $sort);
|
||||||
|
|
||||||
|
$filter = $queryParams['filter'] ?? null;
|
||||||
|
|
||||||
|
if ($filter) {
|
||||||
|
if (! is_array($filter)) {
|
||||||
|
throw new BadRequestException('filter must be an array', 'filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($filter as $name => $value) {
|
||||||
|
if (! isset($schema->fields[$name])
|
||||||
|
|| ! $schema->fields[$name]->filterable
|
||||||
|
) {
|
||||||
|
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $request->withAttribute('jsonApiFilter', $filter);
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sort($query, array $sort, Request $request)
|
||||||
|
{
|
||||||
|
$schema = $this->resource->getSchema();
|
||||||
|
$adapter = $this->resource->getAdapter();
|
||||||
|
|
||||||
|
foreach ($sort as $name => $direction) {
|
||||||
$attribute = $schema->fields[$name];
|
$attribute = $schema->fields[$name];
|
||||||
|
|
||||||
if ($attribute->sorter) {
|
if ($attribute->sorter) {
|
||||||
|
|
@ -117,19 +239,10 @@ class Index implements RequestHandlerInterface
|
||||||
|
|
||||||
private function paginate($query, Request $request)
|
private function paginate($query, Request $request)
|
||||||
{
|
{
|
||||||
$queryParams = $request->getQueryParams();
|
$limit = $request->getAttribute('jsonApiLimit');
|
||||||
|
$offset = $request->getAttribute('jsonApiOffset');
|
||||||
|
|
||||||
$maxLimit = $this->resource->getSchema()->paginate;
|
if ($limit || $offset) {
|
||||||
|
|
||||||
$limit = isset($queryParams['page']['limit']) ? min($maxLimit, (int) $queryParams['page']['limit']) : $maxLimit;
|
|
||||||
|
|
||||||
$offset = isset($queryParams['page']['offset']) ? (int) $queryParams['page']['offset'] : 0;
|
|
||||||
|
|
||||||
if ($offset < 0) {
|
|
||||||
throw new BadRequestException('page[offset] must be >=0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($limit) {
|
|
||||||
$this->resource->getAdapter()->paginate($query, $limit, $offset);
|
$this->resource->getAdapter()->paginate($query, $limit, $offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,17 +252,7 @@ class Index implements RequestHandlerInterface
|
||||||
$schema = $this->resource->getSchema();
|
$schema = $this->resource->getSchema();
|
||||||
$adapter = $this->resource->getAdapter();
|
$adapter = $this->resource->getAdapter();
|
||||||
|
|
||||||
if (! is_array($filter)) {
|
|
||||||
throw new BadRequestException('filter must be an array');
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($filter as $name => $value) {
|
foreach ($filter as $name => $value) {
|
||||||
if (! isset($schema->fields[$name])
|
|
||||||
|| ! $schema->fields[$name]->filterable
|
|
||||||
) {
|
|
||||||
throw new BadRequestException("Invalid filter [$name]");
|
|
||||||
}
|
|
||||||
|
|
||||||
$field = $schema->fields[$name];
|
$field = $schema->fields[$name];
|
||||||
|
|
||||||
if ($field->filter) {
|
if ($field->filter) {
|
||||||
|
|
@ -165,15 +268,4 @@ class Index implements RequestHandlerInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function include($query, array $include)
|
|
||||||
{
|
|
||||||
$adapter = $this->resource->getAdapter();
|
|
||||||
|
|
||||||
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
|
||||||
|
|
||||||
foreach ($trails as $relationships) {
|
|
||||||
$adapter->include($query, $relationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class Show implements RequestHandlerInterface
|
||||||
{
|
{
|
||||||
$include = $this->getInclude($request);
|
$include = $this->getInclude($request);
|
||||||
|
|
||||||
$this->load($include);
|
$this->loadRelationships([$this->model], $include, $request);
|
||||||
|
|
||||||
$serializer = new Serializer($this->api, $request);
|
$serializer = new Serializer($this->api, $request);
|
||||||
|
|
||||||
|
|
@ -43,15 +43,4 @@ class Show implements RequestHandlerInterface
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function load(array $include)
|
|
||||||
{
|
|
||||||
$adapter = $this->resource->getAdapter();
|
|
||||||
|
|
||||||
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
|
||||||
|
|
||||||
foreach ($trails as $relationships) {
|
|
||||||
$adapter->load($this->model, $relationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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 Tobscure\JsonApiServer\Api;
|
use Tobscure\JsonApiServer\Api;
|
||||||
|
use Tobscure\JsonApiServer\Exception\ForbiddenException;
|
||||||
use Tobscure\JsonApiServer\ResourceType;
|
use Tobscure\JsonApiServer\ResourceType;
|
||||||
|
|
||||||
class Update implements RequestHandlerInterface
|
class Update implements RequestHandlerInterface
|
||||||
|
|
@ -25,10 +26,22 @@ class Update implements RequestHandlerInterface
|
||||||
|
|
||||||
public function handle(Request $request): Response
|
public function handle(Request $request): Response
|
||||||
{
|
{
|
||||||
$adapter = $this->resource->getAdapter();
|
$schema = $this->resource->getSchema();
|
||||||
|
|
||||||
|
if (! ($schema->isUpdatable)($request, $this->model)) {
|
||||||
|
throw new ForbiddenException('You cannot update this resource');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($schema->updatingCallbacks as $callback) {
|
||||||
|
$callback($request, $this->model);
|
||||||
|
}
|
||||||
|
|
||||||
$this->save($this->model, $request);
|
$this->save($this->model, $request);
|
||||||
|
|
||||||
|
foreach ($schema->updatedCallbacks as $callback) {
|
||||||
|
$callback($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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,6 @@ class Attribute extends Field
|
||||||
public $sortable = false;
|
public $sortable = false;
|
||||||
public $sorter;
|
public $sorter;
|
||||||
|
|
||||||
public function __construct(string $name)
|
|
||||||
{
|
|
||||||
parent::__construct($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sortable(Closure $callback = null)
|
public function sortable(Closure $callback = null)
|
||||||
{
|
{
|
||||||
$this->sortable = true;
|
$this->sortable = true;
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,24 @@ use Closure;
|
||||||
class Builder
|
class Builder
|
||||||
{
|
{
|
||||||
public $fields = [];
|
public $fields = [];
|
||||||
|
public $meta = [];
|
||||||
public $paginate = 20;
|
public $paginate = 20;
|
||||||
|
public $limit = 50;
|
||||||
|
public $countable = true;
|
||||||
public $scopes = [];
|
public $scopes = [];
|
||||||
|
public $indexScopes = [];
|
||||||
|
public $singleScopes = [];
|
||||||
public $isVisible;
|
public $isVisible;
|
||||||
public $isCreatable;
|
public $isCreatable;
|
||||||
|
public $creatingCallbacks = [];
|
||||||
|
public $createdCallbacks = [];
|
||||||
|
public $isUpdatable;
|
||||||
|
public $updatingCallbacks = [];
|
||||||
|
public $updatedCallbacks = [];
|
||||||
public $isDeletable;
|
public $isDeletable;
|
||||||
|
public $deletingCallbacks = [];
|
||||||
|
public $deletedCallbacks = [];
|
||||||
|
public $defaultSort;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
|
@ -46,16 +59,46 @@ class Builder
|
||||||
return $field;
|
return $field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function meta(string $name, $value)
|
||||||
|
{
|
||||||
|
return $this->meta[$name] = new Meta($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
public function paginate(?int $perPage)
|
public function paginate(?int $perPage)
|
||||||
{
|
{
|
||||||
$this->paginate = $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 scope(Closure $callback)
|
public function scope(Closure $callback)
|
||||||
{
|
{
|
||||||
$this->scopes[] = $callback;
|
$this->scopes[] = $callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeIndex(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->indexScopes[] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeSingle(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->singleScopes[] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
public function creatableIf(Closure $condition)
|
public function creatableIf(Closure $condition)
|
||||||
{
|
{
|
||||||
$this->isCreatable = $condition;
|
$this->isCreatable = $condition;
|
||||||
|
|
@ -84,6 +127,54 @@ class Builder
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 deletableIf(Closure $condition)
|
public function deletableIf(Closure $condition)
|
||||||
{
|
{
|
||||||
$this->isDeletable = $condition;
|
$this->isDeletable = $condition;
|
||||||
|
|
@ -112,6 +203,21 @@ class Builder
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private function field(string $class, string $name, string $property = null)
|
||||||
{
|
{
|
||||||
if (! isset($this->fields[$name]) || ! $this->fields[$name] instanceof $class) {
|
if (! isset($this->fields[$name]) || ! $this->fields[$name] instanceof $class) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ abstract class Field
|
||||||
public $getter;
|
public $getter;
|
||||||
public $setter;
|
public $setter;
|
||||||
public $saver;
|
public $saver;
|
||||||
|
public $savedCallbacks = [];
|
||||||
public $default;
|
public $default;
|
||||||
public $validators = [];
|
public $validators = [];
|
||||||
public $filterable = false;
|
public $filterable = false;
|
||||||
|
|
@ -20,7 +21,7 @@ abstract class Field
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function __construct(string $name)
|
||||||
{
|
{
|
||||||
$this->name = $this->property = $name;
|
$this->name = $name;
|
||||||
|
|
||||||
$this->visible();
|
$this->visible();
|
||||||
$this->readonly();
|
$this->readonly();
|
||||||
|
|
@ -110,6 +111,13 @@ abstract class Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saved(Closure $callback)
|
||||||
|
{
|
||||||
|
$this->savedCallbacks[] = $callback;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function default($value)
|
public function default($value)
|
||||||
{
|
{
|
||||||
$this->default = $this->wrap($value);
|
$this->default = $this->wrap($value);
|
||||||
|
|
@ -132,7 +140,7 @@ abstract class Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function wrap($value)
|
protected function wrap($value)
|
||||||
{
|
{
|
||||||
if (! $value instanceof Closure) {
|
if (! $value instanceof Closure) {
|
||||||
$value = function () use ($value) {
|
$value = function () use ($value) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobscure\JsonApiServer\Schema;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class Meta
|
||||||
|
{
|
||||||
|
public $name;
|
||||||
|
public $value;
|
||||||
|
|
||||||
|
public function __construct(string $name, $value)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->value = $this->wrap($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function wrap($value)
|
||||||
|
{
|
||||||
|
if (! $value instanceof Closure) {
|
||||||
|
$value = function () use ($value) {
|
||||||
|
return $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,15 +4,26 @@ namespace Tobscure\JsonApiServer\Schema;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Spatie\Macroable\Macroable;
|
use Spatie\Macroable\Macroable;
|
||||||
|
use Tobscure\JsonApiServer\Handler\Show;
|
||||||
|
|
||||||
abstract class Relationship extends Field
|
abstract class Relationship extends Field
|
||||||
{
|
{
|
||||||
use Macroable;
|
use Macroable;
|
||||||
|
|
||||||
public $location = 'relationships';
|
public $location = 'relationships';
|
||||||
|
public $linkage;
|
||||||
|
public $hasLinks = true;
|
||||||
|
public $loadable = true;
|
||||||
public $included = false;
|
public $included = false;
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
parent::__construct($name);
|
||||||
|
|
||||||
|
$this->noLinkage();
|
||||||
|
}
|
||||||
|
|
||||||
public function resource($resource)
|
public function resource($resource)
|
||||||
{
|
{
|
||||||
$this->resource = $resource;
|
$this->resource = $resource;
|
||||||
|
|
@ -20,10 +31,59 @@ abstract class Relationship extends Field
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function linkageIf(Closure $condition)
|
||||||
|
{
|
||||||
|
$this->linkage = $condition;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function linkage()
|
||||||
|
{
|
||||||
|
return $this->linkageIf(function () {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function linkageIfSingle()
|
||||||
|
{
|
||||||
|
return $this->linkageIf(function ($request) {
|
||||||
|
return $request->getAttribute('jsonApiHandler') instanceof Show;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function noLinkage()
|
||||||
|
{
|
||||||
|
return $this->linkageIf(function () {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadable()
|
||||||
|
{
|
||||||
|
$this->loadable = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notLoadable()
|
||||||
|
{
|
||||||
|
$this->loadable = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function included()
|
public function included()
|
||||||
{
|
{
|
||||||
$this->included = true;
|
$this->included = true;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function noLinks()
|
||||||
|
{
|
||||||
|
$this->hasLinks = false;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,38 +37,58 @@ class Serializer
|
||||||
'type' => $resource->getType(),
|
'type' => $resource->getType(),
|
||||||
'id' => $adapter->getId($model),
|
'id' => $adapter->getId($model),
|
||||||
'fields' => [],
|
'fields' => [],
|
||||||
'links' => []
|
'links' => [],
|
||||||
|
'meta' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$resourceUrl = $this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id'];
|
||||||
|
|
||||||
|
ksort($schema->fields);
|
||||||
|
|
||||||
foreach ($schema->fields as $name => $field) {
|
foreach ($schema->fields as $name => $field) {
|
||||||
if (($field instanceof Schema\Relationship && ! isset($include[$name]))
|
if (! ($field->isVisible)($this->request, $model)) {
|
||||||
|| ! ($field->isVisible)($model, $this->request)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = $this->getValue($field, $adapter, $model);
|
|
||||||
|
|
||||||
if ($field instanceof Schema\Attribute) {
|
if ($field instanceof Schema\Attribute) {
|
||||||
$value = $this->attribute($field, $value);
|
$value = $this->attribute($field, $model, $adapter);
|
||||||
|
} elseif ($field instanceof Schema\Relationship) {
|
||||||
|
$isIncluded = isset($include[$name]);
|
||||||
|
$isLinkage = ($field->linkage)($this->request);
|
||||||
|
|
||||||
|
if (! $isIncluded && ! $isLinkage) {
|
||||||
|
$value = $this->emptyRelationship($field, $resourceUrl);
|
||||||
} elseif ($field instanceof Schema\HasOne) {
|
} elseif ($field instanceof Schema\HasOne) {
|
||||||
$value = $this->toOne($field, $value, $include[$name] ?? []);
|
$value = $this->toOne($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
} elseif ($field instanceof Schema\HasMany) {
|
||||||
$value = $this->toMany($field, $value, $include[$name] ?? []);
|
$value = $this->toMany($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['fields'][$name] = $value;
|
$data['fields'][$name] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['links']['self'] = new JsonApi\Link\SelfLink($this->api->getBaseUrl().'/'.$data['type'].'/'.$data['id']);
|
$data['links']['self'] = new JsonApi\Link\SelfLink($resourceUrl);
|
||||||
|
|
||||||
|
ksort($schema->meta);
|
||||||
|
|
||||||
|
foreach ($schema->meta as $name => $meta) {
|
||||||
|
$data['meta'][$name] = new JsonApi\Meta($meta->name, ($meta->value)($this->request, $model));
|
||||||
|
}
|
||||||
|
|
||||||
$this->merge($data);
|
$this->merge($data);
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function attribute(Schema\Attribute $field, $value): JsonApi\Attribute
|
private function attribute(Schema\Attribute $field, $model, AdapterInterface $adapter): JsonApi\Attribute
|
||||||
{
|
{
|
||||||
|
if ($field->getter) {
|
||||||
|
$value = ($field->getter)($this->request, $model);
|
||||||
|
} else {
|
||||||
|
$value = $adapter->getAttribute($model, $field);
|
||||||
|
}
|
||||||
|
|
||||||
if ($value instanceof DateTimeInterface) {
|
if ($value instanceof DateTimeInterface) {
|
||||||
$value = $value->format(DateTime::RFC3339);
|
$value = $value->format(DateTime::RFC3339);
|
||||||
}
|
}
|
||||||
|
|
@ -76,31 +96,84 @@ class Serializer
|
||||||
return new JsonApi\Attribute($field->name, $value);
|
return new JsonApi\Attribute($field->name, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function toOne(Schema\Relationship $field, $value, array $include)
|
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||||
{
|
{
|
||||||
|
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
||||||
|
|
||||||
|
if ($field->getter) {
|
||||||
|
$value = ($field->getter)($this->request, $model);
|
||||||
|
} else {
|
||||||
|
$value = $isIncluded ? $adapter->getHasOne($model, $field) : ($isLinkage && $field->loadable ? $adapter->getHasOneId($model, $field) : null);
|
||||||
|
}
|
||||||
|
|
||||||
if (! $value) {
|
if (! $value) {
|
||||||
return new JsonApi\ToNull($field->name);
|
return new JsonApi\ToNull(
|
||||||
|
$field->name,
|
||||||
|
...$links
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isIncluded) {
|
||||||
$identifier = $this->addRelated($field, $value, $include);
|
$identifier = $this->addRelated($field, $value, $include);
|
||||||
|
} else {
|
||||||
return new JsonApi\ToOne($field->name, $identifier);
|
$identifier = $this->relatedResourceIdentifier($field, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function toMany(Schema\Relationship $field, $value, array $include): JsonApi\ToMany
|
|
||||||
|
return new JsonApi\ToOne(
|
||||||
|
$field->name,
|
||||||
|
$identifier,
|
||||||
|
...$links
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||||
{
|
{
|
||||||
|
if ($field->getter) {
|
||||||
|
$value = ($field->getter)($this->request, $model);
|
||||||
|
} else {
|
||||||
|
$value = $isLinkage ? $adapter->getHasMany($model, $field) : null;
|
||||||
|
}
|
||||||
|
|
||||||
$identifiers = [];
|
$identifiers = [];
|
||||||
|
|
||||||
|
if ($isIncluded) {
|
||||||
foreach ($value as $relatedModel) {
|
foreach ($value as $relatedModel) {
|
||||||
$identifiers[] = $this->addRelated($field, $relatedModel, $include);
|
$identifiers[] = $this->addRelated($field, $relatedModel, $include);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($value as $relatedModel) {
|
||||||
|
$identifiers[] = $this->relatedResourceIdentifier($field, $relatedModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonApi\ToMany(
|
return new JsonApi\ToMany(
|
||||||
$field->name,
|
$field->name,
|
||||||
new JsonApi\ResourceIdentifierCollection(...$identifiers)
|
new JsonApi\ResourceIdentifierCollection(...$identifiers),
|
||||||
|
...$this->getRelationshipLinks($field, $resourceUrl)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function emptyRelationship(Schema\Relationship $field, string $resourceUrl): JsonApi\EmptyRelationship
|
||||||
|
{
|
||||||
|
return new JsonApi\EmptyRelationship(
|
||||||
|
$field->name,
|
||||||
|
...$this->getRelationshipLinks($field, $resourceUrl)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRelationshipLinks(Schema\Relationship $field, string $resourceUrl): array
|
||||||
|
{
|
||||||
|
if (! $field->hasLinks) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
new JsonApi\Link\SelfLink($resourceUrl.'/relationships/'.$field->name),
|
||||||
|
new JsonApi\Link\RelatedLink($resourceUrl.'/'.$field->name)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function addRelated(Schema\Relationship $field, $model, array $include): JsonApi\ResourceIdentifier
|
private function addRelated(Schema\Relationship $field, $model, array $include): JsonApi\ResourceIdentifier
|
||||||
{
|
{
|
||||||
$relatedResource = $this->api->getResource($field->resource);
|
$relatedResource = $this->api->getResource($field->resource);
|
||||||
|
|
@ -110,19 +183,6 @@ class Serializer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getValue(Schema\Field $field, AdapterInterface $adapter, $model)
|
|
||||||
{
|
|
||||||
if ($field->getter) {
|
|
||||||
return ($field->getter)($model, $this->request);
|
|
||||||
} elseif ($field instanceof Schema\Attribute) {
|
|
||||||
return $adapter->getAttribute($model, $field);
|
|
||||||
} elseif ($field instanceof Schema\HasOne) {
|
|
||||||
return $adapter->getHasOne($model, $field);
|
|
||||||
} elseif ($field instanceof Schema\HasMany) {
|
|
||||||
return $adapter->getHasMany($model, $field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function merge($data): void
|
private function merge($data): void
|
||||||
{
|
{
|
||||||
$key = $data['type'].':'.$data['id'];
|
$key = $data['type'].':'.$data['id'];
|
||||||
|
|
@ -130,6 +190,7 @@ class Serializer
|
||||||
if (isset($this->map[$key])) {
|
if (isset($this->map[$key])) {
|
||||||
$this->map[$key]['fields'] = array_merge($this->map[$key]['fields'], $data['fields']);
|
$this->map[$key]['fields'] = array_merge($this->map[$key]['fields'], $data['fields']);
|
||||||
$this->map[$key]['links'] = array_merge($this->map[$key]['links'], $data['links']);
|
$this->map[$key]['links'] = array_merge($this->map[$key]['links'], $data['links']);
|
||||||
|
$this->map[$key]['meta'] = array_merge($this->map[$key]['meta'], $data['meta']);
|
||||||
} else {
|
} else {
|
||||||
$this->map[$key] = $data;
|
$this->map[$key] = $data;
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +223,8 @@ class Serializer
|
||||||
$data['type'],
|
$data['type'],
|
||||||
$data['id'],
|
$data['id'],
|
||||||
...array_values($data['fields']),
|
...array_values($data['fields']),
|
||||||
...array_values($data['links'])
|
...array_values($data['links']),
|
||||||
|
...array_values($data['meta'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,4 +235,14 @@ class Serializer
|
||||||
$data['id']
|
$data['id']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function relatedResourceIdentifier(Schema\Relationship $field, $model)
|
||||||
|
{
|
||||||
|
$relatedResource = $this->api->getResource($field->resource);
|
||||||
|
|
||||||
|
return $this->resourceIdentifier([
|
||||||
|
'type' => $field->resource,
|
||||||
|
'id' => $relatedResource->getAdapter()->getId($model)
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tobscure\JsonApiServer;
|
||||||
|
|
||||||
|
interface StatusProviderInterface
|
||||||
|
{
|
||||||
|
public function getJsonApiStatus(): array;
|
||||||
|
}
|
||||||
|
|
@ -118,14 +118,14 @@ class CreateTest extends AbstractTestCase
|
||||||
$schema->attribute('writable1')->writable();
|
$schema->attribute('writable1')->writable();
|
||||||
|
|
||||||
$schema->attribute('writable2')->writableIf(function ($arg1, $arg2) use ($adapter, $request) {
|
$schema->attribute('writable2')->writableIf(function ($arg1, $arg2) use ($adapter, $request) {
|
||||||
$this->assertEquals($adapter->createdModel, $arg1);
|
$this->assertEquals($request, $arg1);
|
||||||
$this->assertEquals($request, $arg2);
|
$this->assertEquals($adapter->createdModel, $arg2);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$schema->attribute('writable3')->readonlyIf(function ($arg1, $arg2) use ($adapter, $request) {
|
$schema->attribute('writable3')->readonlyIf(function ($arg1, $arg2) use ($adapter, $request) {
|
||||||
$this->assertEquals($adapter->createdModel, $arg1);
|
$this->assertEquals($request, $arg1);
|
||||||
$this->assertEquals($request, $arg2);
|
$this->assertEquals($adapter->createdModel, $arg2);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace Tobscure\Tests\JsonApiServer;
|
||||||
|
|
||||||
use Tobscure\JsonApiServer\Adapter\AdapterInterface;
|
use Tobscure\JsonApiServer\Adapter\AdapterInterface;
|
||||||
use Tobscure\JsonApiServer\Schema\Attribute;
|
use Tobscure\JsonApiServer\Schema\Attribute;
|
||||||
|
use Tobscure\JsonApiServer\Schema\Field;
|
||||||
use Tobscure\JsonApiServer\Schema\HasMany;
|
use Tobscure\JsonApiServer\Schema\HasMany;
|
||||||
use Tobscure\JsonApiServer\Schema\HasOne;
|
use Tobscure\JsonApiServer\Schema\HasOne;
|
||||||
|
|
||||||
|
|
@ -44,27 +45,27 @@ class MockAdapter implements AdapterInterface
|
||||||
|
|
||||||
public function getAttribute($model, Attribute $attribute)
|
public function getAttribute($model, Attribute $attribute)
|
||||||
{
|
{
|
||||||
return $model->{$attribute->property} ?? 'default';
|
return $model->{$this->getProperty($attribute)} ?? 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasOne($model, HasOne $relationship)
|
public function getHasOne($model, HasOne $relationship)
|
||||||
{
|
{
|
||||||
return $model->{$relationship->property} ?? null;
|
return $model->{$this->getProperty($relationship)} ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHasMany($model, HasMany $relationship): array
|
public function getHasMany($model, HasMany $relationship): array
|
||||||
{
|
{
|
||||||
return $model->{$relationship->property} ?? [];
|
return $model->{$this->getProperty($relationship)} ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyAttribute($model, Attribute $attribute, $value)
|
public function applyAttribute($model, Attribute $attribute, $value)
|
||||||
{
|
{
|
||||||
$model->{$attribute->property} = $value;
|
$model->{$this->getProperty($attribute)} = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyHasOne($model, HasOne $relationship, $related)
|
public function applyHasOne($model, HasOne $relationship, $related)
|
||||||
{
|
{
|
||||||
$model->{$relationship->property} = $related;
|
$model->{$this->getProperty($relationship)} = $related;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save($model)
|
public function save($model)
|
||||||
|
|
@ -120,4 +121,9 @@ class MockAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
$model->load[] = $relationships;
|
$model->load[] = $relationships;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getProperty(Field $field)
|
||||||
|
{
|
||||||
|
return $field->property ?: $field->name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,8 +93,8 @@ class ShowTest extends AbstractTestCase
|
||||||
$api->resource('users', $adapter, function (Builder $schema) use ($model, $request) {
|
$api->resource('users', $adapter, function (Builder $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->assertEquals($model, $arg1);
|
$this->assertEquals($request, $arg1);
|
||||||
$this->assertEquals($request, $arg2);
|
$this->assertEquals($model, $arg2);
|
||||||
return 'value1';
|
return 'value1';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -127,14 +127,14 @@ class ShowTest extends AbstractTestCase
|
||||||
$schema->attribute('visible2')->visible();
|
$schema->attribute('visible2')->visible();
|
||||||
|
|
||||||
$schema->attribute('visible3')->visibleIf(function ($arg1, $arg2) use ($model, $request) {
|
$schema->attribute('visible3')->visibleIf(function ($arg1, $arg2) use ($model, $request) {
|
||||||
$this->assertEquals($model, $arg1);
|
$this->assertEquals($request, $arg1);
|
||||||
$this->assertEquals($request, $arg2);
|
$this->assertEquals($model, $arg2);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$schema->attribute('visible4')->hiddenIf(function ($arg1, $arg2) use ($model, $request) {
|
$schema->attribute('visible4')->hiddenIf(function ($arg1, $arg2) use ($model, $request) {
|
||||||
$this->assertEquals($model, $arg1);
|
$this->assertEquals($request, $arg1);
|
||||||
$this->assertEquals($request, $arg2);
|
$this->assertEquals($model, $arg2);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue