Skip to content

Commit 0a313c6

Browse files
committed
add flag metadata + few fixes + headers/status
1 parent 4cd5e41 commit 0a313c6

30 files changed

+501
-206
lines changed

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@
111111
"symfony/deprecation-contracts": "^3.1",
112112
"symfony/http-foundation": "^6.4 || ^7.0",
113113
"symfony/http-kernel": "^6.4 || ^7.0",
114-
"symfony/json-streamer": "^7.3",
115114
"symfony/property-access": "^6.4 || ^7.0",
116115
"symfony/property-info": "^6.4 || ^7.1",
117116
"symfony/serializer": "^6.4 || ^7.0",
@@ -176,13 +175,14 @@
176175
"symfony/expression-language": "^6.4 || ^7.0",
177176
"symfony/finder": "^6.4 || ^7.0",
178177
"symfony/form": "^6.4 || ^7.0",
179-
"symfony/framework-bundle": "^6.4 || ^7.0",
178+
"symfony/framework-bundle": "7.4.x-dev",
180179
"symfony/http-client": "^6.4 || ^7.0",
181180
"symfony/intl": "^6.4 || ^7.0",
181+
"symfony/json-streamer": "7.4.x-dev",
182182
"symfony/maker-bundle": "^1.24",
183183
"symfony/mercure-bundle": "*",
184184
"symfony/messenger": "^6.4 || ^7.0",
185-
"symfony/object-mapper": "^7.3",
185+
"symfony/object-mapper": "7.4.x-dev",
186186
"symfony/routing": "^6.4 || ^7.0",
187187
"symfony/security-bundle": "^6.4 || ^7.0",
188188
"symfony/security-core": "^6.4 || ^7.0",

src/Hydra/Collection.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
<?php
22

3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
312
declare(strict_types=1);
413

514
namespace ApiPlatform\Hydra;

src/Hydra/CollectionId.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
<?php
22

3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
312
declare(strict_types=1);
413

514
namespace ApiPlatform\Hydra;

src/Hydra/State/JsonStreamerProcessor.php

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,107 @@
1919
use ApiPlatform\Hydra\PartialCollectionView;
2020
use ApiPlatform\Metadata\CollectionOperationInterface;
2121
use ApiPlatform\Metadata\Error;
22+
use ApiPlatform\Metadata\HttpOperation;
23+
use ApiPlatform\Metadata\IriConverterInterface;
2224
use ApiPlatform\Metadata\Operation;
25+
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
2326
use ApiPlatform\Metadata\QueryParameterInterface;
27+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2428
use ApiPlatform\Metadata\UrlGeneratorInterface;
2529
use ApiPlatform\Metadata\Util\IriHelper;
2630
use ApiPlatform\State\Pagination\PaginatorInterface;
2731
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
2832
use ApiPlatform\State\ProcessorInterface;
33+
use ApiPlatform\State\Util\HttpResponseHeadersTrait;
34+
use ApiPlatform\State\Util\HttpResponseStatusTrait;
2935
use Symfony\Component\HttpFoundation\Response;
3036
use Symfony\Component\HttpFoundation\StreamedResponse;
3137
use Symfony\Component\JsonStreamer\StreamWriterInterface;
3238
use Symfony\Component\TypeInfo\Type;
3339

40+
/**
41+
* @implements ProcessorInterface<mixed,mixed>
42+
*/
3443
final class JsonStreamerProcessor implements ProcessorInterface
3544
{
45+
use HttpResponseHeadersTrait;
46+
use HttpResponseStatusTrait;
47+
48+
/**
49+
* @param ProcessorInterface<mixed,mixed> $processor
50+
* @param StreamWriterInterface<array<string,mixed>> $jsonStreamer
51+
*/
3652
public function __construct(
3753
private readonly ProcessorInterface $processor,
3854
private readonly StreamWriterInterface $jsonStreamer,
55+
?IriConverterInterface $iriConverter = null,
56+
?ResourceClassResolverInterface $resourceClassResolver = null,
57+
?OperationMetadataFactoryInterface $operationMetadataFactory = null,
3958
private readonly string $pageParameterName = 'page',
4059
private readonly string $enabledParameterName = 'pagination',
41-
private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH
60+
private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH,
4261
) {
62+
$this->resourceClassResolver = $resourceClassResolver;
63+
$this->iriConverter = $iriConverter;
64+
$this->operationMetadataFactory = $operationMetadataFactory;
4365
}
4466

67+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
68+
{
69+
if (!$operation->getJsonStream() || !($request = $context['request'] ?? null)) {
70+
return $this->processor->process($data, $operation, $uriVariables, $context);
71+
}
72+
73+
// TODO: remove this before merging
74+
if ($request->query->has('skip_json_stream')) {
75+
return $this->processor->process($data, $operation, $uriVariables, $context);
76+
}
77+
78+
if ($operation instanceof Error || $data instanceof Response || !$operation instanceof HttpOperation) {
79+
return $this->processor->process($data, $operation, $uriVariables, $context);
80+
}
81+
82+
if ($operation instanceof CollectionOperationInterface) {
83+
$requestUri = $request->getRequestUri() ?? '';
84+
$collection = new Collection();
85+
$collection->member = $data;
86+
$collection->view = $this->getView($data, $requestUri, $operation);
87+
88+
if ($operation->getParameters()) {
89+
$collection->search = $this->getSearch($operation, $requestUri);
90+
}
91+
92+
if ($data instanceof PaginatorInterface) {
93+
$collection->totalItems = $data->getTotalItems();
94+
}
95+
96+
if (\is_array($data) || ($data instanceof \Countable && !$data instanceof PartialPaginatorInterface)) {
97+
$collection->totalItems = \count($data);
98+
}
99+
100+
$data = $this->jsonStreamer->write(
101+
$collection,
102+
Type::generic(Type::object($collection::class), Type::object($operation->getClass())),
103+
['data' => $data, 'operation' => $operation],
104+
);
105+
} else {
106+
$data = $this->jsonStreamer->write($data, Type::object($operation->getClass()), [
107+
'data' => $data,
108+
'operation' => $operation,
109+
]);
110+
}
111+
112+
/** @var iterable<string> $data */
113+
$response = new StreamedResponse(
114+
$data,
115+
$this->getStatus($request, $operation, $context),
116+
$this->getHeaders($request, $operation, $context)
117+
);
118+
119+
return $this->processor->process($response, $operation, $uriVariables, $context);
120+
}
121+
122+
// TODO: These come from our Hydra collection normalizer, try to share the logic
45123
private function getSearch(Operation $operation, string $requestUri): IriTemplate
46124
{
47125
/** @var list<IriTemplateMapping> */
@@ -67,6 +145,7 @@ private function getSearch(Operation $operation, string $requestUri): IriTemplat
67145
}
68146

69147
$parts = parse_url($requestUri);
148+
70149
return new IriTemplate(
71150
variableRepresentation: 'BasicRepresentation',
72151
mapping: $mapping,
@@ -91,11 +170,11 @@ private function getView(mixed $object, string $requestUri, Operation $operation
91170
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
92171
// We should not rely on the request_uri but instead rely on the UriTemplate
93172
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
94-
$parsed = IriHelper::parseIri($requestUri ?? '/', $this->pageParameterName);
173+
$parsed = IriHelper::parseIri($requestUri, $this->pageParameterName);
95174
$appliedFilters = $parsed['parameters'];
96175
unset($appliedFilters[$this->enabledParameterName]);
97176

98-
$urlGenerationStrategy = $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy;
177+
$urlGenerationStrategy = $operation->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy;
99178
$id = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy);
100179
if (!$appliedFilters && !$paginated) {
101180
return new PartialCollectionView($id);
@@ -117,46 +196,4 @@ private function getView(mixed $object, string $requestUri, Operation $operation
117196

118197
return new PartialCollectionView($id, $first, $last, $previous, $next);
119198
}
120-
121-
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
122-
{
123-
if ($context['request']->query->has('skip_json_stream')) {
124-
return $this->processor->process($data, $operation, $uriVariables, $context);
125-
}
126-
127-
if ($operation instanceof Error || $data instanceof Response) {
128-
return $this->processor->process($data, $operation, $uriVariables, $context);
129-
}
130-
131-
if ($operation instanceof CollectionOperationInterface) {
132-
$requestUri = $context['request']->getRequestUri() ?? '';
133-
$collection = new Collection();
134-
$collection->member = $data;
135-
$collection->view = $this->getView($data, $requestUri, $operation);
136-
137-
if ($operation->getParameters()) {
138-
$collection->search = $this->getSearch($operation, $requestUri);
139-
}
140-
141-
if ($data instanceof PaginatorInterface) {
142-
$collection->totalItems = $data->getTotalItems();
143-
}
144-
145-
if (\is_array($data) || ($data instanceof \Countable && !$data instanceof PartialPaginatorInterface)) {
146-
$collection->totalItems = \count($data);
147-
}
148-
149-
$response = new StreamedResponse($this->jsonStreamer->write($collection, Type::generic(Type::object($collection::class), Type::object($operation->getClass())), [
150-
'data' => $data,
151-
'operation' => $operation,
152-
]));
153-
} else {
154-
$response = new StreamedResponse($this->jsonStreamer->write($data, Type::object($operation->getClass()), [
155-
'data' => $data,
156-
'operation' => $operation,
157-
]));
158-
}
159-
160-
return $this->processor->process($response, $operation, $uriVariables, $context);
161-
}
162199
}

src/Hydra/State/JsonStreamerProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function __construct(
2929

3030
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
3131
{
32-
if (!$operation instanceof HttpOperation || !($request = $context['request'] ?? null)) {
32+
if (!$operation instanceof HttpOperation || !$operation->getJsonStream() || !($request = $context['request'] ?? null)) {
3333
return $this->decorated?->provide($operation, $uriVariables, $context);
3434
}
3535

src/JsonLd/JsonStreamer/ReadPropertyMetadataLoader.php

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer;
15+
16+
use ApiPlatform\Metadata\UrlGeneratorInterface;
17+
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
18+
use Symfony\Component\TypeInfo\Type;
19+
use ApiPlatform\Metadata\Exception\RuntimeException;
20+
21+
final class ContextValueTransformer implements ValueTransformerInterface
22+
{
23+
public function __construct(
24+
private readonly UrlGeneratorInterface $urlGenerator,
25+
) {
26+
}
27+
28+
public function transform(mixed $value, array $options = []): mixed
29+
{
30+
if (!isset($options['operation'])) {
31+
throw new RuntimeException('Operation is not defined');
32+
}
33+
return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $options['operation']->getShortName()], $options['operation']->getUrlGenerationStrategy());
34+
}
35+
36+
public static function getStreamValueType(): Type
37+
{
38+
return Type::string();
39+
}
40+
}

src/JsonLd/JsonStreamer/IriValueTransformer.php renamed to src/JsonLd/JsonStreamer/ValueTransformer/IriValueTransformer.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
<?php
22

3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
312
declare(strict_types=1);
413

5-
namespace ApiPlatform\JsonLd\JsonStreamer;
14+
namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer;
615

716
use ApiPlatform\Hydra\Collection;
817
use ApiPlatform\Metadata\CollectionOperationInterface;
18+
use ApiPlatform\Metadata\Exception\RuntimeException;
919
use ApiPlatform\Metadata\IriConverterInterface;
1020
use ApiPlatform\Metadata\UrlGeneratorInterface;
1121
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
@@ -20,6 +30,10 @@ public function __construct(
2030

2131
public function transform(mixed $value, array $options = []): mixed
2232
{
33+
if (!isset($options['operation'])) {
34+
throw new RuntimeException('Operation is not defined');
35+
}
36+
2337
if ($options['_current_object'] instanceof Collection) {
2438
return $this->iriConverter->getIriFromResource($options['operation']->getClass(), UrlGeneratorInterface::ABS_PATH, $options['operation']);
2539
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer;
15+
16+
use ApiPlatform\Hydra\Collection;
17+
use ApiPlatform\Metadata\Exception\RuntimeException;
18+
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
19+
use Symfony\Component\TypeInfo\Type;
20+
21+
final class TypeValueTransformer implements ValueTransformerInterface
22+
{
23+
public function transform(mixed $value, array $options = []): mixed
24+
{
25+
if ($options['_current_object'] instanceof Collection) {
26+
return 'Collection';
27+
}
28+
29+
if (!isset($options['operation'])) {
30+
throw new RuntimeException('Operation is not defined');
31+
}
32+
33+
return $options['operation']->getShortName();
34+
}
35+
36+
public static function getStreamValueType(): Type
37+
{
38+
return Type::string();
39+
}
40+
}

0 commit comments

Comments
 (0)