Skip to content

Commit 49aa96e

Browse files
committed
Introduce collections
1 parent 22d0ac0 commit 49aa96e

25 files changed

+344
-125
lines changed

src/Context.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
use Psr\Http\Message\ServerRequestInterface;
66
use Tobyz\JsonApiServer\Endpoint\EndpointInterface;
7+
use Tobyz\JsonApiServer\Resource\CollectionInterface;
78
use Tobyz\JsonApiServer\Resource\ResourceInterface;
89
use Tobyz\JsonApiServer\Schema\Field\Field;
910
use WeakMap;
1011

1112
class Context
1213
{
14+
public ?CollectionInterface $collection = null;
1315
public ?ResourceInterface $resource = null;
1416
public ?EndpointInterface $endpoint = null;
1517
public ?object $query = null;
@@ -155,6 +157,13 @@ public function withRequest(ServerRequestInterface $request): static
155157
return $new;
156158
}
157159

160+
public function withCollection(CollectionInterface $collection): static
161+
{
162+
$new = clone $this;
163+
$new->collection = $collection;
164+
return $new;
165+
}
166+
158167
public function withResource(ResourceInterface $resource): static
159168
{
160169
$new = clone $this;

src/Endpoint/Concerns/FindsResources.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ trait FindsResources
1616
*/
1717
private function findResource(Context $context, string $id)
1818
{
19-
$resource = $context->resource;
19+
$collection = $context->collection;
2020

21-
if (!$resource instanceof Findable) {
21+
if (!$collection instanceof Findable) {
2222
throw new RuntimeException(
23-
sprintf('%s must implement %s', get_class($resource), Findable::class),
23+
sprintf('%s must implement %s', get_class($collection), Findable::class),
2424
);
2525
}
2626

27-
if (!($model = $resource->find($id, $context))) {
28-
throw new ResourceNotFoundException($resource->type(), $id);
27+
if (!($model = $collection->find($id, $context))) {
28+
throw new ResourceNotFoundException($collection->name(), $id);
2929
}
3030

3131
return $model;

src/Endpoint/Concerns/IncludesData.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ private function getInclude(Context $context): array
1313
if ($includeString = $context->request->getQueryParams()['include'] ?? null) {
1414
$include = $this->parseInclude($includeString);
1515

16-
$this->validateInclude($context, [$context->resource], $include);
16+
$this->validateInclude(
17+
$context,
18+
array_map(
19+
fn($resource) => $context->resource($resource),
20+
$context->collection->resources(),
21+
),
22+
$include,
23+
);
1724

1825
return $include;
1926
}
@@ -58,7 +65,7 @@ private function validateInclude(
5865
continue;
5966
}
6067

61-
$types = $field->types;
68+
$types = $field->collections;
6269

6370
$relatedResources = $types
6471
? array_map(fn($type) => $context->api->getResource($type), $types)

src/Endpoint/Concerns/SavesData.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ private function parseData(Context $context): array
4848
throw new ForbiddenException('Client-generated IDs are not supported');
4949
}
5050

51-
if ($body['data']['type'] !== $context->resource->type()) {
52-
throw new ConflictException('data.type does not match the resource type');
51+
if (!in_array($body['data']['type'], $context->collection->resources())) {
52+
throw new ConflictException('collection does not support this resource type');
5353
}
5454

5555
if (isset($body['data']['attributes']) && !is_array($body['data']['attributes'])) {

src/Endpoint/Concerns/ShowsResources.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ private function showResource(Context $context, mixed $model): array
1515
{
1616
$serializer = new Serializer($context);
1717

18-
$serializer->addPrimary($context->resource, $model, $this->getInclude($context));
18+
$serializer->addPrimary(
19+
$context->resource($context->collection->resource($model, $context)),
20+
$model,
21+
$this->getInclude($context),
22+
);
1923

2024
[$primary, $included] = $serializer->serialize();
2125

src/Endpoint/Create.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ public function handle(Context $context): ?ResponseInterface
3737
throw new MethodNotAllowedException();
3838
}
3939

40-
$resource = $context->resource;
40+
$collection = $context->collection;
4141

42-
if (!$resource instanceof Creatable) {
42+
if (!$collection instanceof Creatable) {
4343
throw new RuntimeException(
44-
sprintf('%s must implement %s', get_class($resource), Creatable::class),
44+
sprintf('%s must implement %s', get_class($collection), Creatable::class),
4545
);
4646
}
4747

@@ -51,7 +51,9 @@ public function handle(Context $context): ?ResponseInterface
5151

5252
$data = $this->parseData($context);
5353

54-
$context = $context->withModel($model = $resource->newModel($context));
54+
$context = $context
55+
->withResource($resource = $context->resource($data['type']))
56+
->withModel($model = $collection->newModel($context));
5557

5658
$this->assertFieldsValid($context, $data);
5759
$this->fillDefaultValues($context, $data);

src/Endpoint/Delete.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,18 @@ public function handle(Context $context): ?ResponseInterface
3838
throw new MethodNotAllowedException();
3939
}
4040

41-
$resource = $context->resource;
41+
$model = $this->findResource($context, $segments[1]);
42+
43+
$context = $context->withResource(
44+
$resource = $context->resource($context->collection->resource($model, $context)),
45+
);
4246

4347
if (!$resource instanceof Deletable) {
4448
throw new RuntimeException(
4549
sprintf('%s must implement %s', get_class($resource), Deletable::class),
4650
);
4751
}
4852

49-
$model = $this->findResource($context, $segments[1]);
50-
5153
if (!$this->isVisible($context = $context->withModel($model))) {
5254
throw new ForbiddenException();
5355
}

src/Endpoint/Index.php

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,19 @@ public function handle(Context $context): ?Response
6868
throw new MethodNotAllowedException();
6969
}
7070

71-
$resource = $context->resource;
71+
$collection = $context->collection;
7272

73-
if (!$resource instanceof Listable) {
73+
if (!$collection instanceof Listable) {
7474
throw new RuntimeException(
75-
sprintf('%s must implement %s', get_class($resource), Listable::class),
75+
sprintf('%s must implement %s', get_class($collection), Listable::class),
7676
);
7777
}
7878

7979
if (!$this->isVisible($context)) {
8080
throw new ForbiddenException();
8181
}
8282

83-
$query = $resource->query($context);
83+
$query = $collection->query($context);
8484

8585
$context = $context->withQuery($query);
8686

@@ -91,24 +91,28 @@ public function handle(Context $context): ?Response
9191
$links = [];
9292

9393
if (
94-
$resource instanceof Countable &&
95-
!is_null($total = $resource->count($query, $context))
94+
$collection instanceof Countable &&
95+
!is_null($total = $collection->count($query, $context))
9696
) {
97-
$meta['page']['total'] = $resource->count($query, $context);
97+
$meta['page']['total'] = $collection->count($query, $context);
9898
}
9999

100100
if ($pagination = ($this->paginationResolver)($context)) {
101101
$pagination->apply($query);
102102
}
103103

104-
$models = $resource->results($query, $context);
104+
$models = $collection->results($query, $context);
105105

106106
$serializer = new Serializer($context);
107107

108108
$include = $this->getInclude($context);
109109

110110
foreach ($models as $model) {
111-
$serializer->addPrimary($resource, $model, $include);
111+
$serializer->addPrimary(
112+
$context->resource($collection->resource($model, $context)),
113+
$model,
114+
$include,
115+
);
112116
}
113117

114118
[$data, $included] = $serializer->serialize();
@@ -127,7 +131,7 @@ private function applySorts($query, Context $context): void
127131
return;
128132
}
129133

130-
$sorts = $context->resource->sorts();
134+
$sorts = $context->collection->sorts();
131135

132136
foreach (parse_sort_string($sortString) as [$name, $direction]) {
133137
foreach ($sorts as $field) {
@@ -152,7 +156,7 @@ private function applyFilters($query, Context $context): void
152156
}
153157

154158
try {
155-
apply_filters($query, $filters, $context->resource, $context);
159+
apply_filters($query, $filters, $context->collection, $context);
156160
} catch (BadRequestException $e) {
157161
throw $e->prependSource(['parameter' => 'filter']);
158162
}

src/Endpoint/Update.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,18 @@ public function handle(Context $context): ?ResponseInterface
3939
throw new MethodNotAllowedException();
4040
}
4141

42-
$resource = $context->resource;
42+
$model = $this->findResource($context, $segments[1]);
43+
44+
$context = $context->withResource(
45+
$resource = $context->resource($context->collection->resource($model, $context)),
46+
);
4347

4448
if (!$resource instanceof Updatable) {
4549
throw new RuntimeException(
4650
sprintf('%s must implement %s', get_class($resource), Updatable::class),
4751
);
4852
}
4953

50-
$model = $this->findResource($context, $segments[1]);
51-
5254
if (!$this->isVisible($context = $context->withModel($model))) {
5355
throw new ForbiddenException();
5456
}

src/JsonApi.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
1717
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
1818
use Tobyz\JsonApiServer\Extension\Extension;
19-
use Tobyz\JsonApiServer\Resource\Resource;
19+
use Tobyz\JsonApiServer\Resource\CollectionInterface;
2020
use Tobyz\JsonApiServer\Resource\ResourceInterface;
2121

2222
class JsonApi implements RequestHandlerInterface
@@ -30,10 +30,15 @@ class JsonApi implements RequestHandlerInterface
3030
public array $extensions = [];
3131

3232
/**
33-
* @var Resource[]
33+
* @var ResourceInterface[]
3434
*/
3535
public array $resources = [];
3636

37+
/**
38+
* @var CollectionInterface[]
39+
*/
40+
public array $collections = [];
41+
3742
public function __construct(public string $basePath = '')
3843
{
3944
$this->basePath = rtrim($this->basePath, '/');
@@ -47,12 +52,38 @@ public function extension(Extension $extension): void
4752
$this->extensions[$extension->uri()] = $extension;
4853
}
4954

55+
/**
56+
* Define a new collection.
57+
*/
58+
public function collection(CollectionInterface $collection): void
59+
{
60+
$this->collections[$collection->name()] = $collection;
61+
}
62+
5063
/**
5164
* Define a new resource.
5265
*/
5366
public function resource(ResourceInterface $resource): void
5467
{
5568
$this->resources[$resource->type()] = $resource;
69+
70+
if ($resource instanceof CollectionInterface) {
71+
$this->collection($resource);
72+
}
73+
}
74+
75+
/**
76+
* Get a collection by name.
77+
*
78+
* @throws ResourceNotFoundException if the collection has not been defined.
79+
*/
80+
public function getCollection(string $type): CollectionInterface
81+
{
82+
if (!isset($this->collections[$type])) {
83+
throw new ResourceNotFoundException($type);
84+
}
85+
86+
return $this->collections[$type];
5687
}
5788

5889
/**
@@ -88,9 +119,9 @@ public function handle(Request $request): Response
88119
if (!$response) {
89120
$segments = explode('/', trim($context->path(), '/'), 2);
90121

91-
$context = $context->withResource($this->getResource($segments[0]));
122+
$context = $context->withCollection($this->getCollection($segments[0]));
92123

93-
foreach ($context->resource->endpoints() as $endpoint) {
124+
foreach ($context->collection->endpoints() as $endpoint) {
94125
try {
95126
if ($response = $endpoint->handle($context->withEndpoint($endpoint))) {
96127
break;

0 commit comments

Comments
 (0)