diff --git a/src/Endpoint/Index.php b/src/Endpoint/Index.php index 75d46e2..c4d6449 100644 --- a/src/Endpoint/Index.php +++ b/src/Endpoint/Index.php @@ -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(); diff --git a/src/ResourceType.php b/src/ResourceType.php index f1df5d5..f0ead32 100644 --- a/src/ResourceType.php +++ b/src/ResourceType.php @@ -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); } }