wip
This commit is contained in:
parent
8a4a09bfeb
commit
5d76c0f45a
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Adapter;
|
||||
|
||||
use Closure;
|
||||
|
|
@ -151,20 +160,20 @@ interface AdapterInterface
|
|||
*
|
||||
* @param $model
|
||||
* @param HasOne $relationship
|
||||
* @param array $fields
|
||||
* @param bool $linkage
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getHasOne($model, HasOne $relationship, array $fields = null);
|
||||
public function getHasOne($model, HasOne $relationship, bool $linkage);
|
||||
|
||||
/**
|
||||
* Get a list of models for a has-many relationship for the model.
|
||||
*
|
||||
* @param $model
|
||||
* @param HasMany $relationship
|
||||
* @param array $fields
|
||||
* @param bool $linkage
|
||||
* @return array
|
||||
*/
|
||||
public function getHasMany($model, HasMany $relationship, array $fields = null): array;
|
||||
public function getHasMany($model, HasMany $relationship, bool $linkage): array;
|
||||
|
||||
/**
|
||||
* Apply an attribute value to the model.
|
||||
|
|
@ -219,9 +228,11 @@ interface AdapterInterface
|
|||
* @param array $relationships
|
||||
* @param Closure $scope Should be called to give the deepest relationship
|
||||
* an opportunity to scope the query that will fetch related resources
|
||||
* @param bool $linkage true if we just need the IDs of the related
|
||||
* resources and not their full data
|
||||
* @return mixed
|
||||
*/
|
||||
public function load(array $models, array $relationships, Closure $scope): void;
|
||||
public function load(array $models, array $relationships, Closure $scope, bool $linkage): void;
|
||||
|
||||
/**
|
||||
* Load information about the IDs of related resources onto a collection
|
||||
|
|
@ -231,5 +242,5 @@ interface AdapterInterface
|
|||
* @param Relationship $relationship
|
||||
* @return mixed
|
||||
*/
|
||||
public function loadIds(array $models, Relationship $relationship): void;
|
||||
// public function loadIds(array $models, Relationship $relationship): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Adapter;
|
||||
|
||||
use Closure;
|
||||
|
|
@ -69,12 +78,15 @@ class EloquentAdapter implements AdapterInterface
|
|||
return $model->{$this->getAttributeProperty($attribute)};
|
||||
}
|
||||
|
||||
public function getHasOne($model, HasOne $relationship, array $fields = null)
|
||||
public function getHasOne($model, HasOne $relationship, bool $linkage)
|
||||
{
|
||||
$relation = $this->getEloquentRelation($model, $relationship);
|
||||
|
||||
// comment
|
||||
if ($fields === ['id'] && $relation instanceof BelongsTo) {
|
||||
// If it's a belongs-to relationship and we only need to get the ID,
|
||||
// then we don't have to actually load the relation because the ID is
|
||||
// stored in a column directly on the model. We will mock up a related
|
||||
// model with the value of the ID filled.
|
||||
if ($linkage && $relation instanceof BelongsTo) {
|
||||
if ($key = $model->{$relation->getForeignKeyName()}) {
|
||||
$related = $relation->getRelated();
|
||||
|
||||
|
|
@ -87,7 +99,7 @@ class EloquentAdapter implements AdapterInterface
|
|||
return $this->getRelationValue($model, $relationship);
|
||||
}
|
||||
|
||||
public function getHasMany($model, HasMany $relationship, array $fields = null): array
|
||||
public function getHasMany($model, HasMany $relationship, bool $linkage): array
|
||||
{
|
||||
$collection = $this->getRelationValue($model, $relationship);
|
||||
|
||||
|
|
@ -188,38 +200,42 @@ class EloquentAdapter implements AdapterInterface
|
|||
$query->take($limit)->skip($offset);
|
||||
}
|
||||
|
||||
public function load(array $models, array $relationships, Closure $scope): void
|
||||
public function load(array $models, array $relationships, Closure $scope, bool $linkage): void
|
||||
{
|
||||
// TODO: Find the relation on the model that we're after. If it's a
|
||||
// belongs-to relation, and we only need linkage, then we won't need
|
||||
// to load anything as the related ID is store directly on the model.
|
||||
|
||||
(new Collection($models))->loadMissing([
|
||||
$this->getRelationshipPath($relationships) => $scope
|
||||
]);
|
||||
}
|
||||
|
||||
public function loadIds(array $models, Relationship $relationship): void
|
||||
{
|
||||
if (empty($models)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property = $this->getRelationshipProperty($relationship);
|
||||
$relation = $models[0]->$property();
|
||||
|
||||
// If it's a belongs-to relationship, then the ID is stored on the model
|
||||
// itself, so we don't need to load anything in advance.
|
||||
if ($relation instanceof BelongsTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
(new Collection($models))->loadMissing([
|
||||
$property => function ($query) use ($relation) {
|
||||
$query->select($relation->getRelated()->getKeyName());
|
||||
|
||||
if (! $relation instanceof BelongsToMany) {
|
||||
$query->addSelect($relation->getForeignKeyName());
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
// public function loadIds(array $models, Relationship $relationship): void
|
||||
// {
|
||||
// if (empty($models)) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// $property = $this->getRelationshipProperty($relationship);
|
||||
// $relation = $models[0]->$property();
|
||||
//
|
||||
// // If it's a belongs-to relationship, then the ID is stored on the model
|
||||
// // itself, so we don't need to load anything in advance.
|
||||
// if ($relation instanceof BelongsTo) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// (new Collection($models))->loadMissing([
|
||||
// $property => function ($query) use ($relation) {
|
||||
// $query->select($relation->getRelated()->getKeyName());
|
||||
//
|
||||
// if (! $relation instanceof BelongsToMany) {
|
||||
// $query->addSelect($relation->getForeignKeyName());
|
||||
// }
|
||||
// }
|
||||
// ]);
|
||||
// }
|
||||
|
||||
private function getAttributeProperty(Attribute $attribute): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use JsonApiPhp\JsonApi\Error;
|
||||
|
||||
interface ErrorProviderInterface
|
||||
{
|
||||
/**
|
||||
* Get JSON:API error objects that represent this error.
|
||||
*
|
||||
* @return Error[]
|
||||
*/
|
||||
public function getJsonApiErrors(): array;
|
||||
|
||||
/**
|
||||
* Get the most generally applicable HTTP error code for this error.
|
||||
*/
|
||||
public function getJsonApiStatus(): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use JsonApiPhp\JsonApi\Error;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException as DomainExceptionAlias;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use JsonApiPhp\JsonApi\Error;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use JsonApiPhp\JsonApi\Error;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Exception;
|
||||
|
||||
use JsonApiPhp\JsonApi\Error;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
|
@ -9,13 +18,18 @@ use function Tobyz\JsonApiServer\run_callbacks;
|
|||
|
||||
trait FindsResources
|
||||
{
|
||||
/**
|
||||
* Find a resource within the API after applying scopes for the resource type.
|
||||
*
|
||||
* @throws ResourceNotFoundException if the resource is not found.
|
||||
*/
|
||||
private function findResource(Request $request, ResourceType $resource, string $id)
|
||||
{
|
||||
$adapter = $resource->getAdapter();
|
||||
|
||||
$query = $adapter->query();
|
||||
|
||||
run_callbacks($resource->getSchema()->getScopes(), [$query, $request, $id]);
|
||||
run_callbacks($resource->getSchema()->getListeners('scope'), [$query, $request, $id]);
|
||||
|
||||
$model = $adapter->find($query, $id);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,27 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Schema\Relationship;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
|
||||
/**
|
||||
* @property JsonApi $api
|
||||
* @property ResourceType $resource
|
||||
*/
|
||||
trait IncludesData
|
||||
{
|
||||
private function getInclude(Request $request): array
|
||||
|
|
@ -32,10 +44,9 @@ trait IncludesData
|
|||
$tree = [];
|
||||
|
||||
foreach (explode(',', $include) as $path) {
|
||||
$keys = explode('.', $path);
|
||||
$array = &$tree;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
foreach (explode('.', $path) as $key) {
|
||||
if (! isset($array[$key])) {
|
||||
$array[$key] = [];
|
||||
}
|
||||
|
|
@ -52,7 +63,8 @@ trait IncludesData
|
|||
$fields = $resource->getSchema()->getFields();
|
||||
|
||||
foreach ($include as $name => $nested) {
|
||||
if (! isset($fields[$name])
|
||||
if (
|
||||
! isset($fields[$name])
|
||||
|| ! $fields[$name] instanceof Relationship
|
||||
|| ! $fields[$name]->isIncludable()
|
||||
) {
|
||||
|
|
@ -69,69 +81,42 @@ trait IncludesData
|
|||
}
|
||||
}
|
||||
|
||||
private function buildRelationshipTrails(ResourceType $resource, array $include): array
|
||||
{
|
||||
$fields = $resource->getSchema()->getFields();
|
||||
$trails = [];
|
||||
|
||||
foreach ($include as $name => $nested) {
|
||||
$relationship = $fields[$name];
|
||||
|
||||
if ($relationship->getLoadable()) {
|
||||
$trails[] = [$relationship];
|
||||
}
|
||||
|
||||
if ($type = $fields[$name]->getType()) {
|
||||
$relatedResource = $this->api->getResource($type);
|
||||
|
||||
$trails = array_merge(
|
||||
$trails,
|
||||
array_map(
|
||||
function ($trail) use ($relationship) {
|
||||
return array_merge([$relationship], $trail);
|
||||
},
|
||||
$this->buildRelationshipTrails($relatedResource, $nested)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $trails;
|
||||
}
|
||||
|
||||
private function loadRelationships(array $models, array $include, Request $request)
|
||||
{
|
||||
$adapter = $this->resource->getAdapter();
|
||||
$fields = $this->resource->getSchema()->getFields();
|
||||
$this->loadRelationshipsAtLevel($models, [], $this->resource, $include, $request);
|
||||
}
|
||||
|
||||
$trails = $this->buildRelationshipTrails($this->resource, $include);
|
||||
$loaded = [];
|
||||
|
||||
foreach ($trails as $relationships) {
|
||||
$relationship = end($relationships);
|
||||
|
||||
if (($load = $relationship->getLoadable()) instanceof Closure) {
|
||||
$load($models, $relationships, false, $request);
|
||||
} else {
|
||||
$scope = function ($query) use ($request, $relationship) {
|
||||
run_callbacks($relationship->getScopes(), [$query, $request]);
|
||||
};
|
||||
|
||||
$adapter->load($models, $relationships, $scope);
|
||||
}
|
||||
|
||||
$loaded[] = $relationships[0];
|
||||
}
|
||||
private function loadRelationshipsAtLevel(array $models, array $relationshipPath, ResourceType $resource, array $include, Request $request)
|
||||
{
|
||||
$adapter = $resource->getAdapter();
|
||||
$fields = $resource->getSchema()->getFields();
|
||||
|
||||
foreach ($fields as $name => $field) {
|
||||
if (! $field instanceof Relationship || ! evaluate($field->getLinkage(), [$request]) || ! $field->getLoadable() || in_array($field, $loaded)) {
|
||||
if (
|
||||
! $field instanceof Relationship
|
||||
|| (! $field->isLinkage() && ! isset($include[$name]))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($load = $field->getLoadable()) instanceof Closure) {
|
||||
$load($models, true);
|
||||
} else {
|
||||
$adapter->loadIds($models, $field);
|
||||
$nextRelationshipPath = array_merge($relationshipPath, [$field]);
|
||||
|
||||
if ($load = $field->isLoadable()) {
|
||||
if (is_callable($load)) {
|
||||
$load($models, $nextRelationshipPath, $field->isLinkage(), $request);
|
||||
} else {
|
||||
$scope = function ($query) use ($request, $field) {
|
||||
run_callbacks($field->getListeners('scope'), [$query, $request]);
|
||||
};
|
||||
|
||||
$adapter->load($models, $nextRelationshipPath, $scope, $field->isLinkage());
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($include[$name]) && is_string($type = $field->getType())) {
|
||||
$relatedResource = $this->api->getResource($type);
|
||||
|
||||
$this->loadRelationshipsAtLevel($models, $nextRelationshipPath, $relatedResource, $include[$name] ?? [], $request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,61 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler\Concerns;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\get_value;
|
||||
use function Tobyz\JsonApiServer\has_value;
|
||||
use function Tobyz\JsonApiServer\set_value;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\UnprocessableEntityException;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||
use Tobyz\JsonApiServer\Schema\HasOne;
|
||||
use Tobyz\JsonApiServer\Schema\Relationship;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\get_value;
|
||||
use function Tobyz\JsonApiServer\has_value;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
use function Tobyz\JsonApiServer\set_value;
|
||||
|
||||
/**
|
||||
* @property JsonApi $api
|
||||
* @property ResourceType $resource
|
||||
*/
|
||||
trait SavesData
|
||||
{
|
||||
use FindsResources;
|
||||
|
||||
private function parseData($body): array
|
||||
/**
|
||||
* Parse and validate a JSON:API document's `data` member.
|
||||
*
|
||||
* @throws BadRequestException if the `data` member is invalid.
|
||||
*/
|
||||
private function parseData($body, $model = null): array
|
||||
{
|
||||
if (! is_array($body) && ! is_object($body)) {
|
||||
throw new BadRequestException;
|
||||
}
|
||||
|
||||
$body = (array) $body;
|
||||
|
||||
if (! isset($body['data'])) {
|
||||
throw new BadRequestException('Root data attribute missing');
|
||||
if (! isset($body['data']) || ! is_array($body['data'])) {
|
||||
throw new BadRequestException('data must be an object');
|
||||
}
|
||||
|
||||
if (! isset($body['data']['type']) || $body['data']['type'] !== $this->resource->getType()) {
|
||||
throw new BadRequestException('data.type does not match the resource type');
|
||||
}
|
||||
|
||||
if ($model) {
|
||||
$id = $this->resource->getAdapter()->getId($model);
|
||||
if (! isset($body['data']['id']) || $body['data']['id'] !== $id) {
|
||||
throw new BadRequestException('data.id does not match the resource ID');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($body['data']['attributes']) && ! is_array($body['data']['attributes'])) {
|
||||
|
|
@ -45,7 +72,12 @@ trait SavesData
|
|||
);
|
||||
}
|
||||
|
||||
private function getModelForIdentifier(Request $request, $identifier, array $validTypes = null)
|
||||
/**
|
||||
* Get the model corresponding to the given identifier.
|
||||
*
|
||||
* @throws BadRequestException if the identifier is invalid.
|
||||
*/
|
||||
private function getModelForIdentifier(Request $request, array $identifier, array $validTypes = null)
|
||||
{
|
||||
if (! isset($identifier['type'])) {
|
||||
throw new BadRequestException('type not specified');
|
||||
|
|
@ -64,12 +96,20 @@ trait SavesData
|
|||
return $this->findResource($request, $resource, $identifier['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the fields contained within a data object are valid.
|
||||
*/
|
||||
private function validateFields(array $data, $model, Request $request)
|
||||
{
|
||||
$this->assertFieldsExist($data);
|
||||
$this->assertFieldsWritable($data, $model, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the fields contained within a data object exist in the schema.
|
||||
*
|
||||
* @throws BadRequestException if a field is unknown.
|
||||
*/
|
||||
private function assertFieldsExist(array $data)
|
||||
{
|
||||
$fields = $this->resource->getSchema()->getFields();
|
||||
|
|
@ -83,15 +123,23 @@ trait SavesData
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the fields contained within a data object are writable.
|
||||
*
|
||||
* @throws BadRequestException if a field is not writable.
|
||||
*/
|
||||
private function assertFieldsWritable(array $data, $model, Request $request)
|
||||
{
|
||||
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||
if (has_value($data, $field) && ! evaluate($field->getWritable(), [$model, $request])) {
|
||||
if (has_value($data, $field) && ! evaluate($field->isWritable(), [$model, $request])) {
|
||||
throw new BadRequestException("Field [{$field->getName()}] is not writable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace relationship linkage within a data object with models.
|
||||
*/
|
||||
private function loadRelatedResources(array &$data, Request $request)
|
||||
{
|
||||
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||
|
|
@ -102,7 +150,7 @@ trait SavesData
|
|||
$value = get_value($data, $field);
|
||||
|
||||
if (isset($value['data'])) {
|
||||
$allowedTypes = $field->getAllowedTypes();
|
||||
$allowedTypes = (array) $field->getType();
|
||||
|
||||
if ($field instanceof HasOne) {
|
||||
set_value($data, $field, $this->getModelForIdentifier($request, $value['data'], $allowedTypes));
|
||||
|
|
@ -117,12 +165,17 @@ trait SavesData
|
|||
}
|
||||
}
|
||||
|
||||
private function assertDataValid(array $data, $model, Request $request, bool $all): void
|
||||
/**
|
||||
* Assert that the field values within a data object pass validation.
|
||||
*
|
||||
* @throws UnprocessableEntityException if any fields do not pass validation.
|
||||
*/
|
||||
private function assertDataValid(array $data, $model, Request $request, bool $validateAll): void
|
||||
{
|
||||
$failures = [];
|
||||
|
||||
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||
if (! $all && ! has_value($data, $field)) {
|
||||
if (! $validateAll && ! has_value($data, $field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +194,9 @@ trait SavesData
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field values from a data object to the model instance.
|
||||
*/
|
||||
private function setValues(array $data, $model, Request $request)
|
||||
{
|
||||
$adapter = $this->resource->getAdapter();
|
||||
|
|
@ -169,21 +225,30 @@ trait SavesData
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the model and its fields.
|
||||
*/
|
||||
private function save(array $data, $model, Request $request)
|
||||
{
|
||||
$this->saveModel($model, $request);
|
||||
$this->saveFields($data, $model, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the model.
|
||||
*/
|
||||
private function saveModel($model, Request $request)
|
||||
{
|
||||
if ($saver = $this->resource->getSchema()->getSaver()) {
|
||||
$saver($model, $request);
|
||||
if ($saveCallback = $this->resource->getSchema()->getSaveCallback()) {
|
||||
$saveCallback($model, $request);
|
||||
} else {
|
||||
$this->resource->getAdapter()->save($model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any fields that were not saved with the model.
|
||||
*/
|
||||
private function saveFields(array $data, $model, Request $request)
|
||||
{
|
||||
$adapter = $this->resource->getAdapter();
|
||||
|
|
@ -195,8 +260,8 @@ trait SavesData
|
|||
|
||||
$value = get_value($data, $field);
|
||||
|
||||
if ($saver = $field->getSaver()) {
|
||||
$saver($model, $value, $request);
|
||||
if ($saveCallback = $field->getSaveCallback()) {
|
||||
$saveCallback($model, $value, $request);
|
||||
} elseif ($field instanceof HasMany) {
|
||||
$adapter->saveHasMany($model, $field, $value);
|
||||
}
|
||||
|
|
@ -205,6 +270,9 @@ trait SavesData
|
|||
$this->runSavedCallbacks($data, $model, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run field saved listeners.
|
||||
*/
|
||||
private function runSavedCallbacks(array $data, $model, Request $request)
|
||||
{
|
||||
foreach ($this->resource->getSchema()->getFields() as $field) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\has_value;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
use function Tobyz\JsonApiServer\set_value;
|
||||
|
||||
|
|
@ -26,11 +35,16 @@ class Create implements RequestHandlerInterface
|
|||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to create a resource.
|
||||
*
|
||||
* @throws ForbiddenException if the resource is not creatable.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
if (! evaluate($schema->getCreatable(), [$request])) {
|
||||
if (! evaluate($schema->isCreatable(), [$request])) {
|
||||
throw new ForbiddenException;
|
||||
}
|
||||
|
||||
|
|
@ -56,9 +70,9 @@ class Create implements RequestHandlerInterface
|
|||
|
||||
private function createModel(Request $request)
|
||||
{
|
||||
$creator = $this->resource->getSchema()->getCreator();
|
||||
$createModel = $this->resource->getSchema()->getCreateModelCallback();
|
||||
|
||||
return $creator ? $creator($request) : $this->resource->getAdapter()->create();
|
||||
return $createModel ? $createModel($request) : $this->resource->getAdapter()->create();
|
||||
}
|
||||
|
||||
private function fillDefaultValues(array &$data, Request $request)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
|
||||
class Delete implements RequestHandlerInterface
|
||||
{
|
||||
|
|
@ -22,18 +31,23 @@ class Delete implements RequestHandlerInterface
|
|||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to delete a resource.
|
||||
*
|
||||
* @throws ForbiddenException if the resource is not deletable.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
if (! evaluate($schema->getDeletable(), [$this->model, $request])) {
|
||||
if (! evaluate($schema->isDeletable(), [$this->model, $request])) {
|
||||
throw new ForbiddenException;
|
||||
}
|
||||
|
||||
run_callbacks($schema->getListeners('deleting'), [$this->model, $request]);
|
||||
|
||||
if ($deleter = $this->resource->getSchema()->getDelete()) {
|
||||
$deleter($this->model, $request);
|
||||
if ($deleteCallback = $schema->getDeleteCallback()) {
|
||||
$deleteCallback($this->model, $request);
|
||||
} else {
|
||||
$this->resource->getAdapter()->delete($this->model);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler;
|
||||
|
||||
use Closure;
|
||||
use JsonApiPhp\JsonApi as Structure;
|
||||
use JsonApiPhp\JsonApi\Link\LastLink;
|
||||
use JsonApiPhp\JsonApi\Link\NextLink;
|
||||
use JsonApiPhp\JsonApi\Link\PrevLink;
|
||||
use JsonApiPhp\JsonApi\Meta;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\JsonApiResponse;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||
use Tobyz\JsonApiServer\Schema\HasMany;
|
||||
use Tobyz\JsonApiServer\Schema\HasOne;
|
||||
use Tobyz\JsonApiServer\Serializer;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
|
||||
class Index implements RequestHandlerInterface
|
||||
{
|
||||
|
|
@ -33,35 +41,81 @@ class Index implements RequestHandlerInterface
|
|||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to show a resource listing.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$include = $this->getInclude($request);
|
||||
|
||||
$request = $this->extractQueryParams($request);
|
||||
|
||||
$adapter = $this->resource->getAdapter();
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
run_callbacks($schema->getListeners('listing'), [&$request]);
|
||||
|
||||
$query = $adapter->query();
|
||||
|
||||
foreach ($schema->getScopes() as $scope) {
|
||||
$request = $scope($query, $request, null) ?: $request;
|
||||
run_callbacks($schema->getListeners('scope'), [$query, $request, null]);
|
||||
|
||||
$include = $this->getInclude($request);
|
||||
|
||||
$this->filter($query, $request);
|
||||
$this->sort($query, $request);
|
||||
|
||||
$total = $schema->isCountable() ? $adapter->count($query) : null;
|
||||
|
||||
[$offset, $limit] = $this->paginate($query, $request);
|
||||
|
||||
$models = $adapter->get($query);
|
||||
|
||||
$this->loadRelationships($models, $include, $request);
|
||||
|
||||
run_callbacks($schema->getListeners('listed'), [$models, $request]);
|
||||
|
||||
$serializer = new Serializer($this->api, $request);
|
||||
|
||||
foreach ($models as $model) {
|
||||
$serializer->add($this->resource, $model, $include);
|
||||
}
|
||||
|
||||
if ($filter = $request->getAttribute('jsonApiFilter')) {
|
||||
$this->filter($query, $filter, $request);
|
||||
return new JsonApiResponse(
|
||||
new Structure\CompoundDocument(
|
||||
new Structure\PaginatedCollection(
|
||||
new Structure\Pagination(...$this->buildPaginationLinks($request, $offset, $limit, count($models), $total)),
|
||||
new Structure\ResourceCollection(...$serializer->primary())
|
||||
),
|
||||
new Structure\Included(...$serializer->included()),
|
||||
new Structure\Link\SelfLink($this->buildUrl($request)),
|
||||
new Structure\Meta('offset', $offset),
|
||||
new Structure\Meta('limit', $limit),
|
||||
...($total !== null ? [new Structure\Meta('total', $total)] : [])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildUrl(Request $request, array $overrideParams = []): string
|
||||
{
|
||||
[$selfUrl] = explode('?', $request->getUri(), 2);
|
||||
|
||||
$queryParams = array_replace_recursive($request->getQueryParams(), $overrideParams);
|
||||
|
||||
if (isset($queryParams['page']['offset']) && $queryParams['page']['offset'] <= 0) {
|
||||
unset($queryParams['page']['offset']);
|
||||
}
|
||||
|
||||
$offset = $request->getAttribute('jsonApiOffset');
|
||||
$limit = $request->getAttribute('jsonApiLimit');
|
||||
$total = null;
|
||||
if (isset($queryParams['filter'])) {
|
||||
foreach ($queryParams['filter'] as $k => &$v) {
|
||||
$v = $v === null ? '' : $v;
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = http_build_query($queryParams);
|
||||
|
||||
return $selfUrl.($queryString ? '?'.$queryString : '');
|
||||
}
|
||||
|
||||
private function buildPaginationLinks(Request $request, int $offset, ?int $limit, int $count, ?int $total)
|
||||
{
|
||||
$paginationLinks = [];
|
||||
$members = [
|
||||
new Structure\Link\SelfLink($this->buildUrl($request)),
|
||||
new Structure\Meta('offset', $offset),
|
||||
new Structure\Meta('limit', $limit),
|
||||
];
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
if ($offset > 0) {
|
||||
$paginationLinks[] = new Structure\Link\FirstLink($this->buildUrl($request, ['page' => ['offset' => 0]]));
|
||||
|
|
@ -77,165 +131,53 @@ class Index implements RequestHandlerInterface
|
|||
$paginationLinks[] = new PrevLink($this->buildUrl($request, $params));
|
||||
}
|
||||
|
||||
if ($schema->isCountable() && $schema->getPaginate()) {
|
||||
$total = $adapter->count($query);
|
||||
|
||||
$members[] = new Meta('total', $total);
|
||||
|
||||
if ($offset + $limit < $total) {
|
||||
$paginationLinks[] = new LastLink($this->buildUrl($request, ['page' => ['offset' => floor(($total - 1) / $limit) * $limit]]));
|
||||
}
|
||||
if ($schema->isCountable() && $schema->getPerPage() && $offset + $limit < $total) {
|
||||
$paginationLinks[] = new 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) === $limit && $total === null) || $offset + $limit < $total) {
|
||||
if (($total === null && $count === $limit) || $offset + $limit < $total) {
|
||||
$paginationLinks[] = new NextLink($this->buildUrl($request, ['page' => ['offset' => $offset + $limit]]));
|
||||
}
|
||||
|
||||
$this->loadRelationships($models, $include, $request);
|
||||
|
||||
$serializer = new Serializer($this->api, $request);
|
||||
|
||||
foreach ($models as $model) {
|
||||
$serializer->add($this->resource, $model, $include);
|
||||
}
|
||||
|
||||
return new JsonApiResponse(
|
||||
new Structure\CompoundDocument(
|
||||
new Structure\PaginatedCollection(
|
||||
new Structure\Pagination(...$paginationLinks),
|
||||
new Structure\ResourceCollection(...$serializer->primary())
|
||||
),
|
||||
new Structure\Included(...$serializer->included()),
|
||||
...$members
|
||||
)
|
||||
);
|
||||
return $paginationLinks;
|
||||
}
|
||||
|
||||
private function buildUrl(Request $request, array $overrideParams = []): string
|
||||
{
|
||||
[$selfUrl] = explode('?', $request->getUri(), 2);
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$queryParams = array_replace_recursive($queryParams, $overrideParams);
|
||||
|
||||
if (isset($queryParams['page']['offset']) && $queryParams['page']['offset'] <= 0) {
|
||||
unset($queryParams['page']['offset']);
|
||||
}
|
||||
|
||||
if (isset($queryParams['filter'])) {
|
||||
foreach ($queryParams['filter'] as $k => &$v) {
|
||||
if ($v === null) {
|
||||
$v = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = http_build_query($queryParams);
|
||||
|
||||
return $selfUrl.($queryString ? '?'.$queryString : '');
|
||||
}
|
||||
|
||||
private function extractQueryParams(Request $request): Request
|
||||
private function sort($query, Request $request)
|
||||
{
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$limit = $this->resource->getSchema()->getPaginate();
|
||||
|
||||
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]');
|
||||
}
|
||||
|
||||
$limit = min($this->resource->getSchema()->getLimit(), $limit);
|
||||
if (! $sort = $request->getQueryParams()['sort'] ?? $schema->getDefaultSort()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
|
||||
if (isset($queryParams['page']['offset'])) {
|
||||
$offset = $queryParams['page']['offset'];
|
||||
|
||||
if ((! is_int($offset) && ! ctype_digit($offset)) || $offset < 0) {
|
||||
throw new BadRequestException('page[offset] must be a non-negative integer', 'page[offset]');
|
||||
}
|
||||
}
|
||||
|
||||
$request = $request
|
||||
->withAttribute('jsonApiLimit', $limit)
|
||||
->withAttribute('jsonApiOffset', $offset);
|
||||
|
||||
$sort = $queryParams['sort'] ?? $this->resource->getSchema()->getDefaultSort();
|
||||
|
||||
if ($sort) {
|
||||
$sort = $this->parseSort($sort);
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($sort as $name => $direction) {
|
||||
if (! isset($fields[$name])
|
||||
|| ! $fields[$name] instanceof Attribute
|
||||
|| ! $fields[$name]->getSortable()
|
||||
) {
|
||||
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');
|
||||
}
|
||||
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filter as $name => $value) {
|
||||
if ($name !== 'id' && (! isset($fields[$name]) || ! $fields[$name]->getFilterable())) {
|
||||
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();
|
||||
$sortFields = $schema->getSortFields();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($sort as $name => $direction) {
|
||||
$attribute = $schema->getFields()[$name];
|
||||
|
||||
if (($sorter = $attribute->getSortable()) instanceof Closure) {
|
||||
$sorter($query, $direction, $request);
|
||||
} else {
|
||||
$adapter->sortByAttribute($query, $attribute, $direction);
|
||||
foreach ($this->parseSort($sort) as $name => $direction) {
|
||||
if (isset($sortFields[$name])) {
|
||||
$sortFields[$name]($query, $direction, $request);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($fields[$name])
|
||||
&& $fields[$name] instanceof Attribute
|
||||
&& $fields[$name]->isSortable()
|
||||
) {
|
||||
$adapter->sortByAttribute($query, $fields[$name], $direction);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new BadRequestException("Invalid sort field [$name]", 'sort');
|
||||
}
|
||||
}
|
||||
|
||||
private function parseSort(string $string): array
|
||||
{
|
||||
$sort = [];
|
||||
$fields = explode(',', $string);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
foreach (explode(',', $string) as $field) {
|
||||
if ($field[0] === '-') {
|
||||
$field = substr($field, 1);
|
||||
$direction = 'desc';
|
||||
|
|
@ -251,18 +193,51 @@ class Index implements RequestHandlerInterface
|
|||
|
||||
private function paginate($query, Request $request)
|
||||
{
|
||||
$limit = $request->getAttribute('jsonApiLimit');
|
||||
$offset = $request->getAttribute('jsonApiOffset');
|
||||
$schema = $this->resource->getSchema();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$limit = $schema->getPerPage();
|
||||
|
||||
if (isset($queryParams['page']['limit'])) {
|
||||
$limit = $queryParams['page']['limit'];
|
||||
|
||||
if (! ctype_digit(strval($limit)) || $limit < 1) {
|
||||
throw new BadRequestException('page[limit] must be a positive integer', 'page[limit]');
|
||||
}
|
||||
|
||||
$limit = min($schema->getLimit(), $limit);
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
|
||||
if (isset($queryParams['page']['offset'])) {
|
||||
$offset = $queryParams['page']['offset'];
|
||||
|
||||
if (! ctype_digit(strval($offset)) || $offset < 0) {
|
||||
throw new BadRequestException('page[offset] must be a non-negative integer', 'page[offset]');
|
||||
}
|
||||
}
|
||||
|
||||
if ($limit || $offset) {
|
||||
$this->resource->getAdapter()->paginate($query, $limit, $offset);
|
||||
}
|
||||
|
||||
return [$offset, $limit];
|
||||
}
|
||||
|
||||
private function filter($query, $filter, Request $request)
|
||||
private function filter($query, Request $request)
|
||||
{
|
||||
if (! $filter = $request->getQueryParams()['filter'] ?? null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_array($filter)) {
|
||||
throw new BadRequestException('filter must be an array', 'filter');
|
||||
}
|
||||
|
||||
$schema = $this->resource->getSchema();
|
||||
$adapter = $this->resource->getAdapter();
|
||||
$filters = $schema->getFilters();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filter as $name => $value) {
|
||||
if ($name === 'id') {
|
||||
|
|
@ -270,19 +245,25 @@ class Index implements RequestHandlerInterface
|
|||
continue;
|
||||
}
|
||||
|
||||
$field = $schema->getFields()[$name];
|
||||
|
||||
if (($filter = $field->getFilterable()) instanceof Closure) {
|
||||
$filter($query, $value, $request);
|
||||
} elseif ($field instanceof Attribute) {
|
||||
$adapter->filterByAttribute($query, $field, $value);
|
||||
} elseif ($field instanceof HasOne) {
|
||||
$value = explode(',', $value);
|
||||
$adapter->filterByHasOne($query, $field, $value);
|
||||
} elseif ($field instanceof HasMany) {
|
||||
$value = explode(',', $value);
|
||||
$adapter->filterByHasMany($query, $field, $value);
|
||||
if (isset($filters[$name])) {
|
||||
$filters[$name]($query, $value, $request);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($fields[$name]) && $fields[$name]->isFilterable()) {
|
||||
if ($fields[$name] instanceof Attribute) {
|
||||
$adapter->filterByAttribute($query, $fields[$name], $value);
|
||||
} elseif ($fields[$name] instanceof HasOne) {
|
||||
$value = explode(',', $value);
|
||||
$adapter->filterByHasOne($query, $fields[$name], $value);
|
||||
} elseif ($fields[$name] instanceof HasMany) {
|
||||
$value = explode(',', $value);
|
||||
$adapter->filterByHasMany($query, $fields[$name], $value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new BadRequestException("Invalid filter [$name]", "filter[$name]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler;
|
||||
|
||||
use JsonApiPhp\JsonApi\CompoundDocument;
|
||||
|
|
@ -11,6 +20,7 @@ use Tobyz\JsonApiServer\JsonApi;
|
|||
use Tobyz\JsonApiServer\JsonApiResponse;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Serializer;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
|
||||
class Show implements RequestHandlerInterface
|
||||
{
|
||||
|
|
@ -27,15 +37,19 @@ class Show implements RequestHandlerInterface
|
|||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to show a resource.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$include = $this->getInclude($request);
|
||||
|
||||
$this->loadRelationships([$this->model], $include, $request);
|
||||
|
||||
$serializer = new Serializer($this->api, $request);
|
||||
run_callbacks($this->resource->getSchema()->getListeners('show'), [$this->model, $request]);
|
||||
|
||||
$serializer->add($this->resource, $this->model, $include, true);
|
||||
$serializer = new Serializer($this->api, $request);
|
||||
$serializer->addSingle($this->resource, $this->model, $include);
|
||||
|
||||
return new JsonApiResponse(
|
||||
new CompoundDocument(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\JsonApi;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
use function Tobyz\JsonApiServer\run_callbacks;
|
||||
|
||||
class Update implements RequestHandlerInterface
|
||||
|
|
@ -26,15 +35,20 @@ class Update implements RequestHandlerInterface
|
|||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to update a resource.
|
||||
*
|
||||
* @throws ForbiddenException if the resource is not updatable.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$schema = $this->resource->getSchema();
|
||||
|
||||
if (! evaluate($schema->getUpdatable(), [$this->model, $request])) {
|
||||
if (! evaluate($schema->isUpdatable(), [$this->model, $request])) {
|
||||
throw new ForbiddenException;
|
||||
}
|
||||
|
||||
$data = $this->parseData($request->getParsedBody());
|
||||
$data = $this->parseData($request->getParsedBody(), $this->model);
|
||||
|
||||
$this->validateFields($data, $this->model, $request);
|
||||
$this->loadRelatedResources($data, $request);
|
||||
|
|
@ -47,6 +61,7 @@ class Update implements RequestHandlerInterface
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Http;
|
||||
|
||||
class MediaTypes
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use Closure;
|
||||
use JsonApiPhp\JsonApi\ErrorDocument;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\InternalServerErrorException;
|
||||
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
||||
|
|
@ -31,16 +40,27 @@ final class JsonApi implements RequestHandlerInterface
|
|||
$this->baseUrl = $baseUrl;
|
||||
}
|
||||
|
||||
public function resource(string $type, $adapter, Closure $buildSchema = null): void
|
||||
/**
|
||||
* Define a new resource type.
|
||||
*/
|
||||
public function resource(string $type, AdapterInterface $adapter, callable $buildSchema = null): void
|
||||
{
|
||||
$this->resources[$type] = new ResourceType($type, $adapter, $buildSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get defined resource types.
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
return $this->resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resource type.
|
||||
*
|
||||
* @throws ResourceNotFoundException if the resource type has not been defined.
|
||||
*/
|
||||
public function getResource(string $type): ResourceType
|
||||
{
|
||||
if (! isset($this->resources[$type])) {
|
||||
|
|
@ -50,6 +70,15 @@ final class JsonApi implements RequestHandlerInterface
|
|||
return $this->resources[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request.
|
||||
*
|
||||
* @throws UnsupportedMediaTypeException if the request Content-Type header is invalid
|
||||
* @throws NotAcceptableException if the request Accept header is invalid
|
||||
* @throws MethodNotAllowedException if the request method is invalid
|
||||
* @throws BadRequestException if the request URI is invalid
|
||||
* @throws NotImplementedException
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$this->validateRequest($request);
|
||||
|
|
@ -112,11 +141,7 @@ final class JsonApi implements RequestHandlerInterface
|
|||
|
||||
$mediaTypes = new MediaTypes($header);
|
||||
|
||||
if ($mediaTypes->containsExactly('*/*')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($mediaTypes->containsExactly(self::CONTENT_TYPE)) {
|
||||
if ($mediaTypes->containsExactly('*/*') || $mediaTypes->containsExactly(self::CONTENT_TYPE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +197,12 @@ final class JsonApi implements RequestHandlerInterface
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an exception into a JSON:API error document response.
|
||||
*
|
||||
* If the exception is not an instance of ErrorProviderInterface, an
|
||||
* Internal Server Error response will be produced.
|
||||
*/
|
||||
public function error($e)
|
||||
{
|
||||
if (! $e instanceof ErrorProviderInterface) {
|
||||
|
|
@ -188,6 +219,9 @@ final class JsonApi implements RequestHandlerInterface
|
|||
return new JsonApiResponse($data, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for the API.
|
||||
*/
|
||||
public function getBaseUrl(): string
|
||||
{
|
||||
return $this->baseUrl;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use Closure;
|
||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||
use Tobyz\JsonApiServer\Schema\Type;
|
||||
|
||||
|
|
@ -13,7 +21,7 @@ final class ResourceType
|
|||
private $buildSchema;
|
||||
private $schema;
|
||||
|
||||
public function __construct(string $type, AdapterInterface $adapter, Closure $buildSchema = null)
|
||||
public function __construct(string $type, AdapterInterface $adapter, callable $buildSchema = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->adapter = $adapter;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Closure;
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
final class Attribute extends Field
|
||||
{
|
||||
private $sortable = false;
|
||||
|
||||
public function sortable(Closure $callback = null)
|
||||
public function getLocation(): string
|
||||
{
|
||||
$this->sortable = $callback ?: true;
|
||||
return 'attributes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow this attribute to be used for sorting the resource listing.
|
||||
*/
|
||||
public function sortable()
|
||||
{
|
||||
$this->sortable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow this attribute to be used for sorting the resource listing.
|
||||
*/
|
||||
public function notSortable()
|
||||
{
|
||||
$this->sortable = false;
|
||||
|
|
@ -22,13 +40,8 @@ final class Attribute extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getSortable()
|
||||
public function isSortable(): bool
|
||||
{
|
||||
return $this->sortable;
|
||||
}
|
||||
|
||||
public function getLocation(): string
|
||||
{
|
||||
return 'attributes';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ namespace Tobyz\JsonApiServer\Schema\Concerns;
|
|||
|
||||
trait HasListeners
|
||||
{
|
||||
private $listeners = [];
|
||||
protected $listeners = [];
|
||||
|
||||
public function getListeners(string $event)
|
||||
public function getListeners(string $event): array
|
||||
{
|
||||
return $this->listeners[$event] ?? [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
use Closure;
|
||||
use function Tobyz\JsonApiServer\negate;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
||||
use function Tobyz\JsonApiServer\negate;
|
||||
use function Tobyz\JsonApiServer\wrap;
|
||||
|
||||
abstract class Field
|
||||
|
|
@ -16,10 +24,10 @@ abstract class Field
|
|||
private $visible = true;
|
||||
private $single = false;
|
||||
private $writable = false;
|
||||
private $getter;
|
||||
private $setter;
|
||||
private $saver;
|
||||
private $default;
|
||||
private $getCallback;
|
||||
private $setCallback;
|
||||
private $saveCallback;
|
||||
private $defaultCallback;
|
||||
private $filterable = false;
|
||||
|
||||
public function __construct(string $name)
|
||||
|
|
@ -27,8 +35,15 @@ abstract class Field
|
|||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the field within a JSON:API resource object
|
||||
* ('attributes' or 'relationships').
|
||||
*/
|
||||
abstract public function getLocation(): string;
|
||||
|
||||
/**
|
||||
* Set the model property to which this field corresponds.
|
||||
*/
|
||||
public function property(string $property)
|
||||
{
|
||||
$this->property = $property;
|
||||
|
|
@ -36,14 +51,20 @@ abstract class Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function visible(Closure $condition = null)
|
||||
/**
|
||||
* Allow this field to be seen.
|
||||
*/
|
||||
public function visible(callable $condition = null)
|
||||
{
|
||||
$this->visible = $condition ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hidden(Closure $condition = null)
|
||||
/**
|
||||
* Disallow this field to be seen.
|
||||
*/
|
||||
public function hidden(callable $condition = null)
|
||||
{
|
||||
$this->visible = $condition ? negate($condition) : false;
|
||||
|
||||
|
|
@ -51,9 +72,11 @@ abstract class Field
|
|||
}
|
||||
|
||||
/**
|
||||
* Indicates that the field should only be visible on single root resources
|
||||
* Only show this field on single root resources.
|
||||
*
|
||||
* @return $this
|
||||
* This is useful if a field requires an expensive calculation for each
|
||||
* individual resource (eg. n+1 query problem). In this case it may be
|
||||
* desirable to only have the field show when viewing a single resource.
|
||||
*/
|
||||
public function single()
|
||||
{
|
||||
|
|
@ -62,69 +85,109 @@ abstract class Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function writable(Closure $condition = null)
|
||||
/**
|
||||
* Allow this field to be written.
|
||||
*/
|
||||
public function writable(callable $condition = null)
|
||||
{
|
||||
$this->writable = $condition ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function readonly(Closure $condition = null)
|
||||
/**
|
||||
* Disallow this field to be written.
|
||||
*/
|
||||
public function readonly(callable $condition = null)
|
||||
{
|
||||
$this->writable = $condition ? negate($condition) : false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the value of this field.
|
||||
*
|
||||
* If null, the adapter will be used to get the value of this field.
|
||||
*
|
||||
* @param null|string|callable $value
|
||||
*/
|
||||
public function get($value)
|
||||
{
|
||||
$this->getter = wrap($value);
|
||||
$this->getCallback = $value === null ? null : wrap($value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set(Closure $callback)
|
||||
/**
|
||||
* Set the callback to apply a new value for this field to the model.
|
||||
*
|
||||
* If null, the adapter will be used to set the field on the model.
|
||||
*/
|
||||
public function set(?callable $callback)
|
||||
{
|
||||
$this->setter = $callback;
|
||||
$this->setCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function save(Closure $callback)
|
||||
/**
|
||||
* Set the callback to save this field to the model.
|
||||
*
|
||||
* If specified, the adapter will NOT be used to set the field on the model.
|
||||
*/
|
||||
public function save(?callable $callback)
|
||||
{
|
||||
$this->saver = $callback;
|
||||
$this->saveCallback = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function saved(Closure $callback)
|
||||
/**
|
||||
* Run a callback after this field has been saved.
|
||||
*/
|
||||
public function onSaved(callable $callback)
|
||||
{
|
||||
$this->listeners['saved'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a default value for this field to be used when creating a resource.
|
||||
*
|
||||
* @param null|string|callable $value
|
||||
*/
|
||||
public function default($value)
|
||||
{
|
||||
$this->default = wrap($value);
|
||||
$this->defaultCallback = wrap($value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function validate(Closure $callback)
|
||||
/**
|
||||
* Add a validation callback for this field.
|
||||
*/
|
||||
public function validate(callable $callback)
|
||||
{
|
||||
$this->listeners['validate'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filterable(Closure $callback = null)
|
||||
/**
|
||||
* Allow this field to be used for filtering the resource listing.
|
||||
*/
|
||||
public function filterable()
|
||||
{
|
||||
$this->filterable = $callback ?: true;
|
||||
$this->filterable = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow this field to be used for filtering the resource listing.
|
||||
*/
|
||||
public function notFilterable()
|
||||
{
|
||||
$this->filterable = false;
|
||||
|
|
@ -137,53 +200,47 @@ abstract class Field
|
|||
return $this->name;
|
||||
}
|
||||
|
||||
public function getProperty()
|
||||
public function getProperty(): ?string
|
||||
{
|
||||
return $this->property;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|Closure
|
||||
*/
|
||||
public function getVisible()
|
||||
public function isVisible()
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function getSingle()
|
||||
public function isSingle(): bool
|
||||
{
|
||||
return $this->single;
|
||||
}
|
||||
|
||||
public function getWritable()
|
||||
public function isWritable()
|
||||
{
|
||||
return $this->writable;
|
||||
}
|
||||
|
||||
public function getGetter()
|
||||
public function getGetCallback()
|
||||
{
|
||||
return $this->getter;
|
||||
return $this->getCallback;
|
||||
}
|
||||
|
||||
public function getSetter()
|
||||
public function getSetCallback(): ?callable
|
||||
{
|
||||
return $this->setter;
|
||||
return $this->setCallback;
|
||||
}
|
||||
|
||||
public function getSaver()
|
||||
public function getSaveCallback(): ?callable
|
||||
{
|
||||
return $this->saver;
|
||||
return $this->saveCallback;
|
||||
}
|
||||
|
||||
public function getDefault()
|
||||
public function getDefaultCallback()
|
||||
{
|
||||
return $this->default;
|
||||
return $this->defaultCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|Closure
|
||||
*/
|
||||
public function getFilterable()
|
||||
public function isFilterable(): bool
|
||||
{
|
||||
return $this->filterable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
final class HasMany extends Relationship
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
use Doctrine\Common\Inflector\Inflector;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Closure;
|
||||
use function Tobyz\JsonApiServer\wrap;
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
final class Meta
|
||||
{
|
||||
private $name;
|
||||
private $value;
|
||||
|
||||
public function __construct(string $name, $value)
|
||||
public function __construct(string $name, callable $value)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = wrap($value);
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
|
@ -21,7 +27,7 @@ final class Meta
|
|||
return $this->name;
|
||||
}
|
||||
|
||||
public function getValue(): Closure
|
||||
public function getValue(): callable
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Closure;
|
||||
use function Tobyz\JsonApiServer\negate;
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
abstract class Relationship extends Field
|
||||
{
|
||||
private $type;
|
||||
private $allowedTypes;
|
||||
private $linkage = false;
|
||||
private $links = true;
|
||||
private $loadable = true;
|
||||
private $includable = false;
|
||||
private $scopes = [];
|
||||
|
||||
public function getLocation(): string
|
||||
{
|
||||
return 'relationships';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resource type that this relationship is to.
|
||||
*/
|
||||
public function type($type)
|
||||
{
|
||||
$this->type = $type;
|
||||
|
|
@ -22,35 +34,52 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define this as a polymorphic relationship.
|
||||
*/
|
||||
public function polymorphic(array $types = null)
|
||||
{
|
||||
$this->type = null;
|
||||
$this->allowedTypes = $types;
|
||||
$this->type = $types;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function linkage(Closure $condition = null)
|
||||
/**
|
||||
* Show resource linkage for the relationship.
|
||||
*/
|
||||
public function linkage()
|
||||
{
|
||||
$this->linkage = $condition ?: true;
|
||||
$this->linkage = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function noLinkage(Closure $condition = null)
|
||||
/**
|
||||
* Do not show resource linkage for the relationship.
|
||||
*/
|
||||
public function noLinkage()
|
||||
{
|
||||
$this->linkage = $condition ? negate($condition) : false;
|
||||
$this->linkage = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function loadable(Closure $callback = null)
|
||||
/**
|
||||
* Allow the relationship data to be eager-loaded into the model collection.
|
||||
*
|
||||
* This is used to prevent the n+1 query problem. If null, the adapter will
|
||||
* be used to eager-load relationship data into the model collection.
|
||||
*/
|
||||
public function loadable(callable $callback = null)
|
||||
{
|
||||
$this->loadable = $callback ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not eager-load relationship data into the model collection.
|
||||
*/
|
||||
public function notLoadable()
|
||||
{
|
||||
$this->loadable = false;
|
||||
|
|
@ -58,6 +87,9 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the relationship data to be included in a compound document.
|
||||
*/
|
||||
public function includable()
|
||||
{
|
||||
$this->includable = true;
|
||||
|
|
@ -65,6 +97,9 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not allow the relationship data to be included in a compound document.
|
||||
*/
|
||||
public function notIncludable()
|
||||
{
|
||||
$this->includable = false;
|
||||
|
|
@ -72,16 +107,9 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getAllowedTypes(): ?array
|
||||
{
|
||||
return $this->allowedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show links for the relationship.
|
||||
*/
|
||||
public function links()
|
||||
{
|
||||
$this->links = true;
|
||||
|
|
@ -89,6 +117,9 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not show links for the relationship.
|
||||
*/
|
||||
public function noLinks()
|
||||
{
|
||||
$this->links = false;
|
||||
|
|
@ -96,20 +127,33 @@ abstract class Relationship extends Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getLinkage()
|
||||
/**
|
||||
* Apply a scope to the query to eager-load the relationship data.
|
||||
*/
|
||||
public function scope(callable $callback)
|
||||
{
|
||||
$this->listeners['scope'][] = $callback;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function isLinkage(): bool
|
||||
{
|
||||
return $this->linkage;
|
||||
}
|
||||
|
||||
public function hasLinks(): bool
|
||||
public function isLinks(): bool
|
||||
{
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|Closure
|
||||
* @return bool|callable
|
||||
*/
|
||||
public function getLoadable()
|
||||
public function isLoadable()
|
||||
{
|
||||
return $this->loadable;
|
||||
}
|
||||
|
|
@ -118,19 +162,4 @@ abstract class Relationship extends Field
|
|||
{
|
||||
return $this->includable;
|
||||
}
|
||||
|
||||
public function getLocation(): string
|
||||
{
|
||||
return 'relationships';
|
||||
}
|
||||
|
||||
public function scope(Closure $callback)
|
||||
{
|
||||
$this->scopes[] = $callback;
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
{
|
||||
return $this->scopes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer\Schema;
|
||||
|
||||
use Closure;
|
||||
use function Tobyz\JsonApiServer\negate;
|
||||
use Tobyz\JsonApiServer\Schema\Concerns\HasListeners;
|
||||
use function Tobyz\JsonApiServer\negate;
|
||||
|
||||
final class Type
|
||||
{
|
||||
|
|
@ -12,29 +20,49 @@ final class Type
|
|||
|
||||
private $fields = [];
|
||||
private $meta = [];
|
||||
private $paginate = 20;
|
||||
private $filters = [];
|
||||
private $sortFields = [];
|
||||
private $perPage = 20;
|
||||
private $limit = 50;
|
||||
private $countable = true;
|
||||
private $listable = true;
|
||||
private $defaultSort;
|
||||
private $defaultFilter;
|
||||
private $scopes = [];
|
||||
private $saver;
|
||||
private $saveCallback;
|
||||
private $createModelCallback;
|
||||
private $creatable = false;
|
||||
private $create;
|
||||
private $updatable = false;
|
||||
private $deletable = false;
|
||||
private $delete;
|
||||
private $deleteCallback;
|
||||
|
||||
/**
|
||||
* Add an attribute to the resource type.
|
||||
*
|
||||
* If an attribute has already been defined with this name, it will be
|
||||
* returned. Otherwise, the field will be overwritten.
|
||||
*/
|
||||
public function attribute(string $name): Attribute
|
||||
{
|
||||
return $this->field(Attribute::class, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a has-one relationship to the resource type.
|
||||
*
|
||||
* If a has-one relationship has already been defined with this name, it
|
||||
* will be returned. Otherwise, the field will be overwritten.
|
||||
*/
|
||||
public function hasOne(string $name): HasOne
|
||||
{
|
||||
return $this->field(HasOne::class, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a has-many relationship to the resource type.
|
||||
*
|
||||
* If a has-many relationship has already been defined with this name, it
|
||||
* will be returned. Otherwise, the field will be overwritten.
|
||||
*/
|
||||
public function hasMany(string $name): HasMany
|
||||
{
|
||||
return $this->field(HasMany::class, $name);
|
||||
|
|
@ -49,14 +77,17 @@ final class Type
|
|||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
public function removeField(string $name)
|
||||
/**
|
||||
* Remove a field from the resource type.
|
||||
*/
|
||||
public function removeField(string $name): void
|
||||
{
|
||||
unset($this->fields[$name]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource type's fields.
|
||||
*
|
||||
* @return Field[]
|
||||
*/
|
||||
public function getFields(): array
|
||||
|
|
@ -64,240 +95,408 @@ final class Type
|
|||
return $this->fields;
|
||||
}
|
||||
|
||||
public function meta(string $name, $value)
|
||||
/**
|
||||
* Add a meta attribute to the resource type.
|
||||
*/
|
||||
public function meta(string $name, callable $value): Meta
|
||||
{
|
||||
return $this->meta[$name] = new Meta($name, $value);
|
||||
}
|
||||
|
||||
public function removeMeta(string $name)
|
||||
/**
|
||||
* Remove a meta attribute from the resource type.
|
||||
*/
|
||||
public function removeMeta(string $name): void
|
||||
{
|
||||
unset($this->meta[$name]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource type's meta attributes.
|
||||
*
|
||||
* @return Meta[]
|
||||
*/
|
||||
public function getMeta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function filter(string $name, Closure $callback)
|
||||
/**
|
||||
* Add a filter to the resource type.
|
||||
*/
|
||||
public function filter(string $name, callable $callback): void
|
||||
{
|
||||
$this->attribute($name)
|
||||
->hidden()
|
||||
->filterable($callback);
|
||||
|
||||
return $this;
|
||||
$this->filters[$name] = $callback;
|
||||
}
|
||||
|
||||
public function paginate(int $perPage)
|
||||
/**
|
||||
* Get the resource type's filters.
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
$this->paginate = $perPage;
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function dontPaginate()
|
||||
/**
|
||||
* Add a sort field to the resource type.
|
||||
*/
|
||||
public function sort(string $name, callable $callback): void
|
||||
{
|
||||
$this->paginate = null;
|
||||
$this->sortFields[$name] = $callback;
|
||||
}
|
||||
|
||||
public function getPaginate(): ?int
|
||||
/**
|
||||
* Get the resource type's sort fields.
|
||||
*/
|
||||
public function getSortFields(): array
|
||||
{
|
||||
return $this->paginate;
|
||||
return $this->sortFields;
|
||||
}
|
||||
|
||||
public function limit(int $limit)
|
||||
/**
|
||||
* Paginate the listing of the resource type.
|
||||
*/
|
||||
public function paginate(int $perPage): void
|
||||
{
|
||||
$this->perPage = $perPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't paginate the listing of the resource type.
|
||||
*/
|
||||
public function dontPaginate(): void
|
||||
{
|
||||
$this->perPage = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of records to list per page, or null if the list should
|
||||
* not be paginated.
|
||||
*/
|
||||
public function getPerPage(): ?int
|
||||
{
|
||||
return $this->perPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the maximum number of records that can be listed.
|
||||
*/
|
||||
public function limit(int $limit): void
|
||||
{
|
||||
$this->limit = $limit;
|
||||
}
|
||||
|
||||
public function noLimit()
|
||||
/**
|
||||
* Allow unlimited records to be listed.
|
||||
*/
|
||||
public function noLimit(): void
|
||||
{
|
||||
$this->limit = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of records that can be listed, or null if there
|
||||
* is no limit.
|
||||
*/
|
||||
public function getLimit(): int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
public function countable()
|
||||
/**
|
||||
* Mark the resource type as countable.
|
||||
*/
|
||||
public function countable(): void
|
||||
{
|
||||
$this->countable = true;
|
||||
}
|
||||
|
||||
public function uncountable()
|
||||
/**
|
||||
* Mark the resource type as uncountable.
|
||||
*/
|
||||
public function uncountable(): void
|
||||
{
|
||||
$this->countable = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not the resource type is countable.
|
||||
*/
|
||||
public function isCountable(): bool
|
||||
{
|
||||
return $this->countable;
|
||||
}
|
||||
|
||||
public function scope(Closure $callback)
|
||||
/**
|
||||
* Apply a scope to the query to fetch record(s).
|
||||
*/
|
||||
public function scope(callable $callback): void
|
||||
{
|
||||
$this->scopes[] = $callback;
|
||||
$this->listeners['scope'][] = $callback;
|
||||
}
|
||||
|
||||
public function getScopes(): array
|
||||
/**
|
||||
* Run a callback before a resource is shown.
|
||||
*/
|
||||
public function onShowing(callable $callback): void
|
||||
{
|
||||
return $this->scopes;
|
||||
$this->listeners['showing'][] = $callback;
|
||||
}
|
||||
|
||||
public function create(?Closure $callback)
|
||||
/**
|
||||
* Run a callback when a resource is shown.
|
||||
*/
|
||||
public function onShown(callable $callback): void
|
||||
{
|
||||
$this->create = $callback;
|
||||
$this->listeners['shown'][] = $callback;
|
||||
}
|
||||
|
||||
public function getCreator()
|
||||
/**
|
||||
* Allow the resource type to be listed.
|
||||
*/
|
||||
public function listable(callable $condition = null): void
|
||||
{
|
||||
return $this->create;
|
||||
$this->listable = $condition ?: true;
|
||||
}
|
||||
|
||||
public function creatable(Closure $condition = null)
|
||||
/**
|
||||
* Disallow the resource type to be listed.
|
||||
*/
|
||||
public function notListable(callable $condition = null): void
|
||||
{
|
||||
$this->listable = $condition ? negate($condition) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not the resource type is allowed to be listed.
|
||||
*/
|
||||
public function isListable()
|
||||
{
|
||||
return $this->listable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback before the resource type is listed.
|
||||
*/
|
||||
public function onListing(callable $callback): void
|
||||
{
|
||||
$this->listeners['listing'][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback when the resource type is listed.
|
||||
*/
|
||||
public function onListed(callable $callback): void
|
||||
{
|
||||
$this->listeners['listed'][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to create a new model instance.
|
||||
*
|
||||
* If null, the adapter will be used to create new model instances.
|
||||
*/
|
||||
public function createModel(?callable $callback): void
|
||||
{
|
||||
$this->createModelCallback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the callback to create a new model instance.
|
||||
*/
|
||||
public function getCreateModelCallback(): ?callable
|
||||
{
|
||||
return $this->createModelCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the resource type to be created.
|
||||
*/
|
||||
public function creatable(callable $condition = null): void
|
||||
{
|
||||
$this->creatable = $condition ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notCreatable(Closure $condition = null)
|
||||
/**
|
||||
* Disallow the resource type to be created.
|
||||
*/
|
||||
public function notCreatable(callable $condition = null): void
|
||||
{
|
||||
$this->creatable = $condition ? negate($condition) : false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatable()
|
||||
/**
|
||||
* Get whether or not the resource type is allowed to be created.
|
||||
*/
|
||||
public function isCreatable()
|
||||
{
|
||||
return $this->creatable;
|
||||
}
|
||||
|
||||
public function creating(Closure $callback)
|
||||
/**
|
||||
* Run a callback before a resource is created.
|
||||
*/
|
||||
public function onCreating(callable $callback): void
|
||||
{
|
||||
$this->listeners['creating'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function created(Closure $callback)
|
||||
/**
|
||||
* Run a callback after a resource has been created.
|
||||
*/
|
||||
public function onCreated(callable $callback): void
|
||||
{
|
||||
$this->listeners['created'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updatable(Closure $condition = null)
|
||||
/**
|
||||
* Allow the resource type to be updated.
|
||||
*/
|
||||
public function updatable(callable $condition = null): void
|
||||
{
|
||||
$this->updatable = $condition ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notUpdatable(Closure $condition = null)
|
||||
/**
|
||||
* Disallow the resource type to be updated.
|
||||
*/
|
||||
public function notUpdatable(callable $condition = null): void
|
||||
{
|
||||
$this->updatable = $condition ? negate($condition) : false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatable()
|
||||
/**
|
||||
* Get whether or not the resource type is allowed to be updated.
|
||||
*/
|
||||
public function isUpdatable()
|
||||
{
|
||||
return $this->updatable;
|
||||
}
|
||||
|
||||
public function updating(Closure $callback)
|
||||
/**
|
||||
* Run a callback before a resource has been updated.
|
||||
*/
|
||||
public function onUpdating(callable $callback): void
|
||||
{
|
||||
$this->listeners['updating'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updated(Closure $callback)
|
||||
/**
|
||||
* Run a callback after a resource has been updated.
|
||||
*/
|
||||
public function onUpdated(callable $callback): void
|
||||
{
|
||||
$this->listeners['updated'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function save(?Closure $callback)
|
||||
/**
|
||||
* Set the callback to save a model instance.
|
||||
*
|
||||
* If null, the adapter will be used to save model instances.
|
||||
*/
|
||||
public function save(?callable $callback): void
|
||||
{
|
||||
$this->saver = $callback;
|
||||
|
||||
return $this;
|
||||
$this->saveCallback = $callback;
|
||||
}
|
||||
|
||||
public function getSaver()
|
||||
/**
|
||||
* Get the callback to save a model instance.
|
||||
*/
|
||||
public function getSaveCallback(): ?callable
|
||||
{
|
||||
return $this->saver;
|
||||
return $this->saveCallback;
|
||||
}
|
||||
|
||||
public function deletable(Closure $condition = null)
|
||||
/**
|
||||
* Allow the resource type to be deleted.
|
||||
*/
|
||||
public function deletable(callable $condition = null): void
|
||||
{
|
||||
$this->deletable = $condition ?: true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function notDeletable(Closure $condition = null)
|
||||
/**
|
||||
* Disallow the resource type to be deleted.
|
||||
*/
|
||||
public function notDeletable(callable $condition = null): void
|
||||
{
|
||||
$this->deletable = $condition ? negate($condition) : false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeletable()
|
||||
/**
|
||||
* Get whether or not the resource type is allowed to be deleted.
|
||||
*/
|
||||
public function isDeletable()
|
||||
{
|
||||
return $this->deletable;
|
||||
}
|
||||
|
||||
public function delete(?Closure $callback)
|
||||
/**
|
||||
* Set the callback to delete a model instance.
|
||||
*
|
||||
* If null, the adapter will be used to delete model instances.
|
||||
*/
|
||||
public function delete(?callable $callback): void
|
||||
{
|
||||
$this->delete = $callback;
|
||||
|
||||
return $this;
|
||||
$this->deleteCallback = $callback;
|
||||
}
|
||||
|
||||
public function getDelete()
|
||||
/**
|
||||
* Get the callback to delete a model instance.
|
||||
*/
|
||||
public function getDeleteCallback(): ?callable
|
||||
{
|
||||
return $this->delete;
|
||||
return $this->deleteCallback;
|
||||
}
|
||||
|
||||
public function deleting(Closure $callback)
|
||||
/**
|
||||
* Run a callback before a resource has been deleted.
|
||||
*/
|
||||
public function onDeleting(callable $callback): void
|
||||
{
|
||||
$this->listeners['deleting'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function deleted(Closure $callback)
|
||||
/**
|
||||
* Run a callback after a resource has been deleted.
|
||||
*/
|
||||
public function onDeleted(callable $callback): void
|
||||
{
|
||||
$this->listeners['deleted'][] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function defaultSort(?string $sort)
|
||||
/**
|
||||
* Set the default sort parameter value to be used if none is specified in
|
||||
* the query string.
|
||||
*/
|
||||
public function defaultSort(?string $sort): void
|
||||
{
|
||||
$this->defaultSort = $sort;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefaultSort()
|
||||
/**
|
||||
* Get the default sort parameter value to be used if none is specified in
|
||||
* the query string.
|
||||
*/
|
||||
public function getDefaultSort(): ?string
|
||||
{
|
||||
return $this->defaultSort;
|
||||
}
|
||||
|
||||
public function defaultFilter(?array $filter)
|
||||
/**
|
||||
* Set the default filter parameter value to be used if none is specified in
|
||||
* the query string.
|
||||
*/
|
||||
public function defaultFilter(?array $filter): void
|
||||
{
|
||||
$this->defaultFilter = $filter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefaultFilter()
|
||||
/**
|
||||
* Get the default filter parameter value to be used if none is specified in
|
||||
* the query string.
|
||||
*/
|
||||
public function getDefaultFilter(): ?array
|
||||
{
|
||||
return $this->defaultFilter;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use DateTime;
|
||||
|
|
@ -30,13 +39,18 @@ final class Serializer
|
|||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function add(ResourceType $resource, $model, array $include, bool $single = false)
|
||||
public function add(ResourceType $resource, $model, array $include, bool $single = false): void
|
||||
{
|
||||
$data = $this->addToMap($resource, $model, $include, $single);
|
||||
|
||||
$this->primary[] = $data['type'].':'.$data['id'];
|
||||
}
|
||||
|
||||
public function addSingle(ResourceType $resource, $model, array $include): void
|
||||
{
|
||||
$this->add($resource, $model, $include, true);
|
||||
}
|
||||
|
||||
private function addToMap(ResourceType $resource, $model, array $include, bool $single = false)
|
||||
{
|
||||
$adapter = $resource->getAdapter();
|
||||
|
|
@ -69,11 +83,11 @@ final class Serializer
|
|||
continue;
|
||||
}
|
||||
|
||||
if ($field->getSingle() && ! $single) {
|
||||
if ($field->isSingle() && ! $single) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! evaluate($field->getVisible(), [$model, $this->request])) {
|
||||
if (! evaluate($field->isVisible(), [$model, $this->request])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -81,12 +95,12 @@ final class Serializer
|
|||
$value = $this->attribute($field, $model, $adapter);
|
||||
} elseif ($field instanceof Schema\Relationship) {
|
||||
$isIncluded = isset($include[$name]);
|
||||
$isLinkage = evaluate($field->getLinkage(), [$this->request]);
|
||||
$isLinkage = evaluate($field->isLinkage(), [$this->request]);
|
||||
|
||||
if (! $isIncluded && ! $isLinkage) {
|
||||
$value = $this->emptyRelationship($field, $resourceUrl);
|
||||
} elseif ($field instanceof Schema\HasOne) {
|
||||
$value = $this->toOne($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
|
||||
$value = $this->toOne($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl, $single);
|
||||
} elseif ($field instanceof Schema\HasMany) {
|
||||
$value = $this->toMany($field, $model, $adapter, $isIncluded, $isLinkage, $include[$name] ?? [], $resourceUrl);
|
||||
}
|
||||
|
|
@ -114,7 +128,7 @@ final class Serializer
|
|||
|
||||
private function attribute(Attribute $field, $model, AdapterInterface $adapter): Structure\Attribute
|
||||
{
|
||||
if ($getter = $field->getGetter()) {
|
||||
if ($getter = $field->getGetCallback()) {
|
||||
$value = $getter($model, $this->request);
|
||||
} else {
|
||||
$value = $adapter->getAttribute($model, $field);
|
||||
|
|
@ -127,11 +141,11 @@ final class Serializer
|
|||
return new Structure\Attribute($field->getName(), $value);
|
||||
}
|
||||
|
||||
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||
private function toOne(Schema\HasOne $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl, bool $single = false)
|
||||
{
|
||||
$links = $this->getRelationshipLinks($field, $resourceUrl);
|
||||
|
||||
$value = $isIncluded ? (($getter = $field->getGetter()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field)) : ($isLinkage && $field->getLoadable() ? $adapter->getHasOne($model, $field, ['id']) : null);
|
||||
$value = $isIncluded ? (($getter = $field->getGetCallback()) ? $getter($model, $this->request) : $adapter->getHasOne($model, $field, false)) : ($isLinkage ? $adapter->getHasOne($model, $field, true) : null);
|
||||
|
||||
if (! $value) {
|
||||
return new Structure\ToNull(
|
||||
|
|
@ -141,7 +155,7 @@ final class Serializer
|
|||
}
|
||||
|
||||
if ($isIncluded) {
|
||||
$identifier = $this->addRelated($field, $value, $include);
|
||||
$identifier = $this->addRelated($field, $value, $include, $single);
|
||||
} else {
|
||||
$identifier = $this->relatedResourceIdentifier($field, $value);
|
||||
}
|
||||
|
|
@ -156,10 +170,10 @@ final class Serializer
|
|||
|
||||
private function toMany(Schema\HasMany $field, $model, AdapterInterface $adapter, bool $isIncluded, bool $isLinkage, array $include, string $resourceUrl)
|
||||
{
|
||||
if ($getter = $field->getGetter()) {
|
||||
if ($getter = $field->getGetCallback()) {
|
||||
$value = $getter($model, $this->request);
|
||||
} else {
|
||||
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field) : null;
|
||||
$value = ($isLinkage || $isIncluded) ? $adapter->getHasMany($model, $field, false) : null;
|
||||
}
|
||||
|
||||
$identifiers = [];
|
||||
|
|
@ -197,7 +211,7 @@ final class Serializer
|
|||
|
||||
private function getRelationshipLinks(Relationship $field, string $resourceUrl): array
|
||||
{
|
||||
if (! $field->hasLinks()) {
|
||||
if (! $field->isLinks()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -207,12 +221,12 @@ final class Serializer
|
|||
];
|
||||
}
|
||||
|
||||
private function addRelated(Relationship $field, $model, array $include): ResourceIdentifier
|
||||
private function addRelated(Relationship $field, $model, array $include, bool $single = false): ResourceIdentifier
|
||||
{
|
||||
$relatedResource = $field->getType() ? $this->api->getResource($field->getType()) : $this->resourceForModel($model);
|
||||
$relatedResource = is_string($field->getType()) ? $this->api->getResource($field->getType()) : $this->resourceForModel($model);
|
||||
|
||||
return $this->resourceIdentifier(
|
||||
$this->addToMap($relatedResource, $model, $include)
|
||||
$this->addToMap($relatedResource, $model, $include, $single)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +300,7 @@ final class Serializer
|
|||
{
|
||||
$type = $field->getType();
|
||||
|
||||
$relatedResource = $type ? $this->api->getResource($type) : $this->resourceForModel($model);
|
||||
$relatedResource = is_string($type) ? $this->api->getResource($type) : $this->resourceForModel($model);
|
||||
|
||||
return $this->resourceIdentifier([
|
||||
'type' => $relatedResource->getType(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of tobyz/json-api-server.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use Closure;
|
||||
|
|
@ -49,16 +58,3 @@ function set_value(array &$data, Field $field, $value)
|
|||
{
|
||||
$data[$field->getLocation()][$field->getName()] = $value;
|
||||
}
|
||||
|
||||
function array_set(array $array, $key, $value)
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
|
||||
while (count($keys) > 1) {
|
||||
$array = &$array[array_shift($keys)];
|
||||
}
|
||||
|
||||
$array[array_shift($keys)] = $value;
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue