Expose scope, filter, and sort operations on ResourceType
This commit is contained in:
parent
416a9c80b0
commit
aa2754d458
|
|
@ -17,13 +17,10 @@ use JsonApiPhp\JsonApi\Link\NextLink;
|
|||
use JsonApiPhp\JsonApi\Link\PrevLink;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||
use Tobyz\JsonApiServer\Context;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Exception\ForbiddenException;
|
||||
use Tobyz\JsonApiServer\ResourceType;
|
||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||
use Tobyz\JsonApiServer\Schema\Meta;
|
||||
use Tobyz\JsonApiServer\Serializer;
|
||||
|
||||
use function Tobyz\JsonApiServer\evaluate;
|
||||
|
|
@ -48,15 +45,22 @@ class Index
|
|||
|
||||
$query = $adapter->query();
|
||||
|
||||
$resourceType->scope($query, $context);
|
||||
$resourceType->applyScopes($query, $context);
|
||||
|
||||
$include = $this->getInclude($context, $resourceType);
|
||||
|
||||
[$offset, $limit] = $this->paginate($resourceType, $query, $context);
|
||||
$this->sort($resourceType, $query, $context);
|
||||
|
||||
if ($sortString = $context->getRequest()->getQueryParams()['sort'] ?? $schema->getDefaultSort()) {
|
||||
$resourceType->applySort($query, $sortString, $context);
|
||||
}
|
||||
|
||||
if ($filter = $context->getRequest()->getQueryParams()['filter'] ?? null) {
|
||||
$resourceType->filter($query, $filter, $context);
|
||||
if (! is_array($filter)) {
|
||||
throw (new BadRequestException('filter must be an array'))->setSourceParameter('filter');
|
||||
}
|
||||
|
||||
$resourceType->applyFilters($query, $filter, $context);
|
||||
}
|
||||
|
||||
run_callbacks($schema->getListeners('listing'), [$query, $context]);
|
||||
|
|
@ -74,21 +78,36 @@ class Index
|
|||
|
||||
[$primary, $included] = $serializer->serialize();
|
||||
|
||||
$meta = array_values(array_map(function (Meta $meta) use ($context) {
|
||||
return new Structure\Meta($meta->getName(), $meta->getValue()($context));
|
||||
}, $context->getMeta()));
|
||||
$paginationLinks = $this->buildPaginationLinks(
|
||||
$resourceType,
|
||||
$context->getRequest(),
|
||||
$offset,
|
||||
$limit,
|
||||
count($models),
|
||||
$total
|
||||
);
|
||||
|
||||
$meta = [
|
||||
new Structure\Meta('offset', $offset),
|
||||
new Structure\Meta('limit', $limit),
|
||||
];
|
||||
|
||||
if ($total !== null) {
|
||||
$meta[] = new Structure\Meta('total', $total);
|
||||
}
|
||||
|
||||
foreach ($context->getMeta() as $item) {
|
||||
$meta[] = new Structure\Meta($item->getName(), $item->getValue()($context));
|
||||
}
|
||||
|
||||
return json_api_response(
|
||||
new Structure\CompoundDocument(
|
||||
new Structure\PaginatedCollection(
|
||||
new Structure\Pagination(...$this->buildPaginationLinks($resourceType, $context->getRequest(), $offset, $limit, count($models), $total)),
|
||||
new Structure\Pagination(...$paginationLinks),
|
||||
new Structure\ResourceCollection(...$primary)
|
||||
),
|
||||
new Structure\Included(...$included),
|
||||
new Structure\Link\SelfLink($this->buildUrl($context->getRequest())),
|
||||
new Structure\Meta('offset', $offset),
|
||||
new Structure\Meta('limit', $limit),
|
||||
...($total !== null ? [new Structure\Meta('total', $total)] : []),
|
||||
...$meta
|
||||
)
|
||||
);
|
||||
|
|
@ -145,55 +164,6 @@ class Index
|
|||
return $paginationLinks;
|
||||
}
|
||||
|
||||
private function sort(ResourceType $resourceType, $query, Context $context): void
|
||||
{
|
||||
$schema = $resourceType->getSchema();
|
||||
|
||||
if (! $sort = $context->getRequest()->getQueryParams()['sort'] ?? $schema->getDefaultSort()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adapter = $resourceType->getAdapter();
|
||||
$sorts = $schema->getSorts();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($this->parseSort($sort) as $name => $direction) {
|
||||
if (isset($sorts[$name]) && evaluate($sorts[$name]->getVisible(), [$context])) {
|
||||
$sorts[$name]->getCallback()($query, $direction, $context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($fields[$name])
|
||||
&& $fields[$name] instanceof Attribute
|
||||
&& evaluate($fields[$name]->getSortable(), [$context])
|
||||
) {
|
||||
$adapter->sortByAttribute($query, $fields[$name], $direction);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw (new BadRequestException("Invalid sort field [$name]"))->setSourceParameter('sort');
|
||||
}
|
||||
}
|
||||
|
||||
private function parseSort(string $string): array
|
||||
{
|
||||
$sort = [];
|
||||
|
||||
foreach (explode(',', $string) as $field) {
|
||||
if ($field[0] === '-') {
|
||||
$field = substr($field, 1);
|
||||
$direction = 'desc';
|
||||
} else {
|
||||
$direction = 'asc';
|
||||
}
|
||||
|
||||
$sort[$field] = $direction;
|
||||
}
|
||||
|
||||
return $sort;
|
||||
}
|
||||
|
||||
private function paginate(ResourceType $resourceType, $query, Context $context): array
|
||||
{
|
||||
$schema = $resourceType->getSchema();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
namespace Tobyz\JsonApiServer;
|
||||
|
||||
use ReflectionClass;
|
||||
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
||||
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
||||
use Tobyz\JsonApiServer\Schema\Attribute;
|
||||
|
|
@ -55,62 +54,116 @@ final class ResourceType
|
|||
return $this->schema;
|
||||
}
|
||||
|
||||
public function scope($query, Context $context)
|
||||
/**
|
||||
* Apply the resource type's scopes to a query.
|
||||
*/
|
||||
public function applyScopes($query, Context $context): void
|
||||
{
|
||||
run_callbacks($this->getSchema()->getListeners('scope'), [$query, $context]);
|
||||
run_callbacks(
|
||||
$this->getSchema()->getListeners('scope'),
|
||||
[$query, $context]
|
||||
);
|
||||
}
|
||||
|
||||
public function filter($query, $filter, Context $context): void
|
||||
/**
|
||||
* Apply the resource type's filters to a query.
|
||||
*/
|
||||
public function applySort($query, string $sortString, Context $context): void
|
||||
{
|
||||
if (! is_array($filter)) {
|
||||
throw (new BadRequestException('filter must be an array'))->setSourceParameter('filter');
|
||||
}
|
||||
|
||||
$schema = $this->getSchema();
|
||||
$adapter = $this->getAdapter();
|
||||
$filters = $schema->getFilters();
|
||||
$customSorts = $schema->getSorts();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filter as $name => $value) {
|
||||
if ($name === 'id') {
|
||||
$adapter->filterByIds($query, explode(',', $value));
|
||||
foreach ($this->parseSortString($sortString) as [$name, $direction]) {
|
||||
if (
|
||||
isset($customSorts[$name])
|
||||
&& evaluate($customSorts[$name]->getVisible(), [$context])
|
||||
) {
|
||||
$customSorts[$name]->getCallback()($query, $direction, $context);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($filters[$name]) && evaluate($filters[$name]->getVisible(), [$context])) {
|
||||
$filters[$name]->getCallback()($query, $value, $context);
|
||||
$field = $fields[$name] ?? null;
|
||||
|
||||
if (
|
||||
$field instanceof Attribute
|
||||
&& evaluate($field->getSortable(), [$context])
|
||||
) {
|
||||
$this->adapter->sortByAttribute($query, $field, $direction);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw (new BadRequestException("Invalid sort field: $name"))->setSourceParameter('sort');
|
||||
}
|
||||
}
|
||||
|
||||
private function parseSortString(string $string): array
|
||||
{
|
||||
return array_map(function ($field) {
|
||||
if ($field[0] === '-') {
|
||||
return [substr($field, 1), 'desc'];
|
||||
} else {
|
||||
return [$field, 'asc'];
|
||||
}
|
||||
}, explode(',', $string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the resource type's filters to a query.
|
||||
*/
|
||||
public function applyFilters($query, array $filters, Context $context): void
|
||||
{
|
||||
$schema = $this->getSchema();
|
||||
$customFilters = $schema->getFilters();
|
||||
$fields = $schema->getFields();
|
||||
|
||||
foreach ($filters as $name => $value) {
|
||||
if ($name === 'id') {
|
||||
$this->adapter->filterByIds($query, explode(',', $value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($customFilters[$name])
|
||||
&& evaluate($customFilters[$name]->getVisible(), [$context])
|
||||
) {
|
||||
$customFilters[$name]->getCallback()($query, $value, $context);
|
||||
continue;
|
||||
}
|
||||
|
||||
[$name, $sub] = explode('.', $name, 2) + [null, null];
|
||||
$field = $fields[$name] ?? null;
|
||||
|
||||
if (isset($fields[$name]) && evaluate($fields[$name]->getFilterable(), [$context])) {
|
||||
if ($fields[$name] instanceof Attribute && $sub === null) {
|
||||
$this->filterByAttribute($adapter, $query, $fields[$name], $value);
|
||||
if ($field && evaluate($field->getFilterable(), [$context])) {
|
||||
if ($field instanceof Attribute && $sub === null) {
|
||||
$this->filterByAttribute($query, $field, $value);
|
||||
continue;
|
||||
} elseif ($fields[$name] instanceof Relationship) {
|
||||
if (is_string($relatedType = $fields[$name]->getType())) {
|
||||
}
|
||||
|
||||
if ($field instanceof Relationship) {
|
||||
if (is_string($relatedType = $field->getType())) {
|
||||
$relatedResource = $context->getApi()->getResourceType($relatedType);
|
||||
$adapter->filterByRelationship($query, $fields[$name], function ($query) use ($relatedResource, $sub, $value, $context) {
|
||||
$relatedResource->filter($query, [($sub ?? 'id') => $value], $context);
|
||||
|
||||
$this->adapter->filterByRelationship($query, $field, function ($query) use ($relatedResource, $sub, $value, $context) {
|
||||
$relatedResource->applyFilters($query, [($sub ?? 'id') => $value], $context);
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw (new BadRequestException("Invalid filter [$name]"))->setSourceParameter("filter[$name]");
|
||||
throw (new BadRequestException("Invalid filter: $name"))->setSourceParameter("filter[$name]");
|
||||
}
|
||||
}
|
||||
|
||||
private function filterByAttribute(AdapterInterface $adapter, $query, Attribute $attribute, $value): void
|
||||
private function filterByAttribute($query, Attribute $attribute, $value): void
|
||||
{
|
||||
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
|
||||
if ($matches[1] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '>=');
|
||||
$this->adapter->filterByAttribute($query, $attribute, $value, '>=');
|
||||
}
|
||||
if ($matches[2] !== '*') {
|
||||
$adapter->filterByAttribute($query, $attribute, $value, '<=');
|
||||
$this->adapter->filterByAttribute($query, $attribute, $value, '<=');
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -118,12 +171,12 @@ final class ResourceType
|
|||
|
||||
foreach (['>=', '>', '<=', '<'] as $operator) {
|
||||
if (strpos($value, $operator) === 0) {
|
||||
$adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
|
||||
$this->adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$adapter->filterByAttribute($query, $attribute, $value);
|
||||
$this->adapter->filterByAttribute($query, $attribute, $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue