Skip to content

Commit aa2754d

Browse files
committed
Expose scope, filter, and sort operations on ResourceType
1 parent 416a9c8 commit aa2754d

File tree

2 files changed

+110
-87
lines changed

2 files changed

+110
-87
lines changed

src/Endpoint/Index.php

Lines changed: 32 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@
1717
use JsonApiPhp\JsonApi\Link\PrevLink;
1818
use Psr\Http\Message\ResponseInterface;
1919
use Psr\Http\Message\ServerRequestInterface as Request;
20-
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
2120
use Tobyz\JsonApiServer\Context;
2221
use Tobyz\JsonApiServer\Exception\BadRequestException;
2322
use Tobyz\JsonApiServer\Exception\ForbiddenException;
2423
use Tobyz\JsonApiServer\ResourceType;
25-
use Tobyz\JsonApiServer\Schema\Attribute;
26-
use Tobyz\JsonApiServer\Schema\Meta;
2724
use Tobyz\JsonApiServer\Serializer;
2825

2926
use function Tobyz\JsonApiServer\evaluate;
@@ -48,15 +45,22 @@ public function handle(Context $context, ResourceType $resourceType): ResponseIn
4845

4946
$query = $adapter->query();
5047

51-
$resourceType->scope($query, $context);
48+
$resourceType->applyScopes($query, $context);
5249

5350
$include = $this->getInclude($context, $resourceType);
5451

5552
[$offset, $limit] = $this->paginate($resourceType, $query, $context);
56-
$this->sort($resourceType, $query, $context);
53+
54+
if ($sortString = $context->getRequest()->getQueryParams()['sort'] ?? $schema->getDefaultSort()) {
55+
$resourceType->applySort($query, $sortString, $context);
56+
}
5757

5858
if ($filter = $context->getRequest()->getQueryParams()['filter'] ?? null) {
59-
$resourceType->filter($query, $filter, $context);
59+
if (! is_array($filter)) {
60+
throw (new BadRequestException('filter must be an array'))->setSourceParameter('filter');
61+
}
62+
63+
$resourceType->applyFilters($query, $filter, $context);
6064
}
6165

6266
run_callbacks($schema->getListeners('listing'), [$query, $context]);
@@ -74,21 +78,36 @@ public function handle(Context $context, ResourceType $resourceType): ResponseIn
7478

7579
[$primary, $included] = $serializer->serialize();
7680

77-
$meta = array_values(array_map(function (Meta $meta) use ($context) {
78-
return new Structure\Meta($meta->getName(), $meta->getValue()($context));
79-
}, $context->getMeta()));
81+
$paginationLinks = $this->buildPaginationLinks(
82+
$resourceType,
83+
$context->getRequest(),
84+
$offset,
85+
$limit,
86+
count($models),
87+
$total
88+
);
89+
90+
$meta = [
91+
new Structure\Meta('offset', $offset),
92+
new Structure\Meta('limit', $limit),
93+
];
94+
95+
if ($total !== null) {
96+
$meta[] = new Structure\Meta('total', $total);
97+
}
98+
99+
foreach ($context->getMeta() as $item) {
100+
$meta[] = new Structure\Meta($item->getName(), $item->getValue()($context));
101+
}
80102

81103
return json_api_response(
82104
new Structure\CompoundDocument(
83105
new Structure\PaginatedCollection(
84-
new Structure\Pagination(...$this->buildPaginationLinks($resourceType, $context->getRequest(), $offset, $limit, count($models), $total)),
106+
new Structure\Pagination(...$paginationLinks),
85107
new Structure\ResourceCollection(...$primary)
86108
),
87109
new Structure\Included(...$included),
88110
new Structure\Link\SelfLink($this->buildUrl($context->getRequest())),
89-
new Structure\Meta('offset', $offset),
90-
new Structure\Meta('limit', $limit),
91-
...($total !== null ? [new Structure\Meta('total', $total)] : []),
92111
...$meta
93112
)
94113
);
@@ -145,55 +164,6 @@ private function buildPaginationLinks(ResourceType $resourceType, Request $reque
145164
return $paginationLinks;
146165
}
147166

148-
private function sort(ResourceType $resourceType, $query, Context $context): void
149-
{
150-
$schema = $resourceType->getSchema();
151-
152-
if (! $sort = $context->getRequest()->getQueryParams()['sort'] ?? $schema->getDefaultSort()) {
153-
return;
154-
}
155-
156-
$adapter = $resourceType->getAdapter();
157-
$sorts = $schema->getSorts();
158-
$fields = $schema->getFields();
159-
160-
foreach ($this->parseSort($sort) as $name => $direction) {
161-
if (isset($sorts[$name]) && evaluate($sorts[$name]->getVisible(), [$context])) {
162-
$sorts[$name]->getCallback()($query, $direction, $context);
163-
continue;
164-
}
165-
166-
if (
167-
isset($fields[$name])
168-
&& $fields[$name] instanceof Attribute
169-
&& evaluate($fields[$name]->getSortable(), [$context])
170-
) {
171-
$adapter->sortByAttribute($query, $fields[$name], $direction);
172-
continue;
173-
}
174-
175-
throw (new BadRequestException("Invalid sort field [$name]"))->setSourceParameter('sort');
176-
}
177-
}
178-
179-
private function parseSort(string $string): array
180-
{
181-
$sort = [];
182-
183-
foreach (explode(',', $string) as $field) {
184-
if ($field[0] === '-') {
185-
$field = substr($field, 1);
186-
$direction = 'desc';
187-
} else {
188-
$direction = 'asc';
189-
}
190-
191-
$sort[$field] = $direction;
192-
}
193-
194-
return $sort;
195-
}
196-
197167
private function paginate(ResourceType $resourceType, $query, Context $context): array
198168
{
199169
$schema = $resourceType->getSchema();

src/ResourceType.php

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Tobyz\JsonApiServer;
1313

14-
use ReflectionClass;
1514
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
1615
use Tobyz\JsonApiServer\Exception\BadRequestException;
1716
use Tobyz\JsonApiServer\Schema\Attribute;
@@ -55,75 +54,129 @@ public function getSchema(): Type
5554
return $this->schema;
5655
}
5756

58-
public function scope($query, Context $context)
57+
/**
58+
* Apply the resource type's scopes to a query.
59+
*/
60+
public function applyScopes($query, Context $context): void
5961
{
60-
run_callbacks($this->getSchema()->getListeners('scope'), [$query, $context]);
62+
run_callbacks(
63+
$this->getSchema()->getListeners('scope'),
64+
[$query, $context]
65+
);
6166
}
6267

63-
public function filter($query, $filter, Context $context): void
68+
/**
69+
* Apply the resource type's filters to a query.
70+
*/
71+
public function applySort($query, string $sortString, Context $context): void
6472
{
65-
if (! is_array($filter)) {
66-
throw (new BadRequestException('filter must be an array'))->setSourceParameter('filter');
73+
$schema = $this->getSchema();
74+
$customSorts = $schema->getSorts();
75+
$fields = $schema->getFields();
76+
77+
foreach ($this->parseSortString($sortString) as [$name, $direction]) {
78+
if (
79+
isset($customSorts[$name])
80+
&& evaluate($customSorts[$name]->getVisible(), [$context])
81+
) {
82+
$customSorts[$name]->getCallback()($query, $direction, $context);
83+
continue;
84+
}
85+
86+
$field = $fields[$name] ?? null;
87+
88+
if (
89+
$field instanceof Attribute
90+
&& evaluate($field->getSortable(), [$context])
91+
) {
92+
$this->adapter->sortByAttribute($query, $field, $direction);
93+
continue;
94+
}
95+
96+
throw (new BadRequestException("Invalid sort field: $name"))->setSourceParameter('sort');
6797
}
98+
}
99+
100+
private function parseSortString(string $string): array
101+
{
102+
return array_map(function ($field) {
103+
if ($field[0] === '-') {
104+
return [substr($field, 1), 'desc'];
105+
} else {
106+
return [$field, 'asc'];
107+
}
108+
}, explode(',', $string));
109+
}
68110

111+
/**
112+
* Apply the resource type's filters to a query.
113+
*/
114+
public function applyFilters($query, array $filters, Context $context): void
115+
{
69116
$schema = $this->getSchema();
70-
$adapter = $this->getAdapter();
71-
$filters = $schema->getFilters();
117+
$customFilters = $schema->getFilters();
72118
$fields = $schema->getFields();
73119

74-
foreach ($filter as $name => $value) {
120+
foreach ($filters as $name => $value) {
75121
if ($name === 'id') {
76-
$adapter->filterByIds($query, explode(',', $value));
122+
$this->adapter->filterByIds($query, explode(',', $value));
77123
continue;
78124
}
79125

80-
if (isset($filters[$name]) && evaluate($filters[$name]->getVisible(), [$context])) {
81-
$filters[$name]->getCallback()($query, $value, $context);
126+
if (
127+
isset($customFilters[$name])
128+
&& evaluate($customFilters[$name]->getVisible(), [$context])
129+
) {
130+
$customFilters[$name]->getCallback()($query, $value, $context);
82131
continue;
83132
}
84133

85134
[$name, $sub] = explode('.', $name, 2) + [null, null];
135+
$field = $fields[$name] ?? null;
86136

87-
if (isset($fields[$name]) && evaluate($fields[$name]->getFilterable(), [$context])) {
88-
if ($fields[$name] instanceof Attribute && $sub === null) {
89-
$this->filterByAttribute($adapter, $query, $fields[$name], $value);
137+
if ($field && evaluate($field->getFilterable(), [$context])) {
138+
if ($field instanceof Attribute && $sub === null) {
139+
$this->filterByAttribute($query, $field, $value);
90140
continue;
91-
} elseif ($fields[$name] instanceof Relationship) {
92-
if (is_string($relatedType = $fields[$name]->getType())) {
141+
}
142+
143+
if ($field instanceof Relationship) {
144+
if (is_string($relatedType = $field->getType())) {
93145
$relatedResource = $context->getApi()->getResourceType($relatedType);
94-
$adapter->filterByRelationship($query, $fields[$name], function ($query) use ($relatedResource, $sub, $value, $context) {
95-
$relatedResource->filter($query, [($sub ?? 'id') => $value], $context);
146+
147+
$this->adapter->filterByRelationship($query, $field, function ($query) use ($relatedResource, $sub, $value, $context) {
148+
$relatedResource->applyFilters($query, [($sub ?? 'id') => $value], $context);
96149
});
97150
}
98151
continue;
99152
}
100153
}
101154

102-
throw (new BadRequestException("Invalid filter [$name]"))->setSourceParameter("filter[$name]");
155+
throw (new BadRequestException("Invalid filter: $name"))->setSourceParameter("filter[$name]");
103156
}
104157
}
105158

106-
private function filterByAttribute(AdapterInterface $adapter, $query, Attribute $attribute, $value): void
159+
private function filterByAttribute($query, Attribute $attribute, $value): void
107160
{
108161
if (preg_match('/(.+)\.\.(.+)/', $value, $matches)) {
109162
if ($matches[1] !== '*') {
110-
$adapter->filterByAttribute($query, $attribute, $value, '>=');
163+
$this->adapter->filterByAttribute($query, $attribute, $value, '>=');
111164
}
112165
if ($matches[2] !== '*') {
113-
$adapter->filterByAttribute($query, $attribute, $value, '<=');
166+
$this->adapter->filterByAttribute($query, $attribute, $value, '<=');
114167
}
115168

116169
return;
117170
}
118171

119172
foreach (['>=', '>', '<=', '<'] as $operator) {
120173
if (strpos($value, $operator) === 0) {
121-
$adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
174+
$this->adapter->filterByAttribute($query, $attribute, substr($value, strlen($operator)), $operator);
122175

123176
return;
124177
}
125178
}
126179

127-
$adapter->filterByAttribute($query, $attribute, $value);
180+
$this->adapter->filterByAttribute($query, $attribute, $value);
128181
}
129182
}

0 commit comments

Comments
 (0)