Expose scope, filter, and sort operations on ResourceType

This commit is contained in:
Toby Zerner 2021-08-29 15:43:35 +10:00
parent 416a9c80b0
commit aa2754d458
2 changed files with 113 additions and 90 deletions

View File

@ -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();

View File

@ -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);
}
}