Skip to content

Commit c07a2af

Browse files
committed
feat: json streamer
1 parent 19b16a7 commit c07a2af

File tree

57 files changed

+1862
-223
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1862
-223
lines changed

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"symfony/property-info": "^6.4 || ^7.1",
117117
"symfony/serializer": "^6.4 || ^7.0",
118118
"symfony/translation-contracts": "^3.3",
119-
"symfony/type-info": "^7.3",
119+
"symfony/type-info": "^7.3 || 7.4.x-dev",
120120
"symfony/validator": "^6.4 || ^7.1",
121121
"symfony/web-link": "^6.4 || ^7.1",
122122
"willdurand/negotiation": "^3.1"
@@ -176,9 +176,10 @@
176176
"symfony/expression-language": "^6.4 || ^7.0",
177177
"symfony/finder": "^6.4 || ^7.0",
178178
"symfony/form": "^6.4 || ^7.0",
179-
"symfony/framework-bundle": "^6.4 || ^7.0",
179+
"symfony/framework-bundle": "7.4.x-dev",
180180
"symfony/http-client": "^6.4 || ^7.0",
181181
"symfony/intl": "^6.4 || ^7.0",
182+
"symfony/json-streamer": "7.4.x-dev",
182183
"symfony/maker-bundle": "^1.24",
183184
"symfony/mercure-bundle": "*",
184185
"symfony/messenger": "^6.4 || ^7.0",
@@ -212,6 +213,7 @@
212213
"symfony/uid": "To support Symfony UUID/ULID identifiers.",
213214
"symfony/messenger": "To support messenger integration.",
214215
"symfony/web-profiler-bundle": "To use the data collector.",
216+
"symfony/json-streamer": "To use the JSON Streamer component.",
215217
"webonyx/graphql-php": "To support GraphQL."
216218
},
217219
"type": "library",

src/Hydra/Collection.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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\Hydra;
15+
16+
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
18+
/**
19+
* @template T
20+
*
21+
* @internal
22+
*/
23+
class Collection
24+
{
25+
#[StreamedName('@context')]
26+
public string $context = 'VIRTUAL';
27+
28+
#[StreamedName('@id')]
29+
public string $id = 'VIRTUAL';
30+
31+
#[StreamedName('@type')]
32+
public string $type = 'Collection';
33+
34+
public float $totalItems;
35+
36+
public ?IriTemplate $search = null;
37+
public ?PartialCollectionView $view = null;
38+
39+
/**
40+
* @var list<T>
41+
*/
42+
public iterable $member;
43+
}

src/Hydra/IriTemplate.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Hydra;
15+
16+
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
use Symfony\Component\Serializer\Annotation\SerializedName;
18+
19+
final class IriTemplate
20+
{
21+
#[StreamedName('@type')]
22+
#[SerializedName('@type')]
23+
public string $type = 'IriTemplate';
24+
25+
public function __construct(
26+
public string $variableRepresentation,
27+
/** @var list<IriTemplateMapping> */
28+
public array $mapping = [],
29+
public ?string $template = null,
30+
) {
31+
}
32+
}

src/Hydra/IriTemplateMapping.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Hydra;
15+
16+
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
use Symfony\Component\Serializer\Annotation\SerializedName;
18+
19+
class IriTemplateMapping
20+
{
21+
#[StreamedName('@type')]
22+
#[SerializedName('@type')]
23+
public string $type = 'IriTemplateMapping';
24+
25+
public function __construct(
26+
public string $variable,
27+
public ?string $property,
28+
public bool $required = false,
29+
) {
30+
}
31+
}

src/Hydra/PartialCollectionView.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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\Hydra;
15+
16+
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
18+
class PartialCollectionView
19+
{
20+
#[StreamedName('@type')]
21+
public string $type = 'PartialCollectionView';
22+
23+
public function __construct(
24+
#[StreamedName('@id')]
25+
public string $id,
26+
#[StreamedName('first')]
27+
public ?string $first = null,
28+
#[StreamedName('last')]
29+
public ?string $last = null,
30+
#[StreamedName('previous')]
31+
public ?string $previous = null,
32+
#[StreamedName('next')]
33+
public ?string $next = null,
34+
) {
35+
}
36+
}

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 25 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
namespace ApiPlatform\Hydra\Serializer;
1515

16+
use ApiPlatform\Hydra\IriTemplateMapping;
17+
use ApiPlatform\Hydra\State\Util\SearchHelperTrait;
1618
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
1719
use ApiPlatform\Metadata\FilterInterface;
18-
use ApiPlatform\Metadata\Parameters;
19-
use ApiPlatform\Metadata\QueryParameterInterface;
2020
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2121
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2222
use ApiPlatform\State\Util\StateOptionsTrait;
@@ -34,6 +34,7 @@
3434
final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface
3535
{
3636
use HydraPrefixTrait;
37+
use SearchHelperTrait;
3738
use StateOptionsTrait;
3839
private ?ContainerInterface $filterLocator = null;
3940

@@ -108,7 +109,13 @@ public function normalize(mixed $object, ?string $format = null, array $context
108109

109110
if ($currentFilters || ($parameters && \count($parameters))) {
110111
$hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext);
111-
$data[$hydraPrefix.'search'] = $this->getSearch($resourceClass, $requestParts, $currentFilters, $parameters, $hydraPrefix);
112+
['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']);
113+
$data[$hydraPrefix.'search'] = [
114+
'@type' => $hydraPrefix.'IriTemplate',
115+
$hydraPrefix.'template' => \sprintf('%s{?%s}', $requestParts['path'], implode(',', $keys)),
116+
$hydraPrefix.'variableRepresentation' => 'BasicRepresentation',
117+
$hydraPrefix.'mapping' => $this->convertMappingToArray($mapping),
118+
];
112119
}
113120

114121
return $data;
@@ -125,88 +132,28 @@ public function setNormalizer(NormalizerInterface $normalizer): void
125132
}
126133

127134
/**
128-
* Returns the content of the Hydra search property.
135+
* @param list<IriTemplateMapping> $mapping
129136
*
130-
* @param FilterInterface[] $filters
137+
* @return array<array<string, mixed>>
131138
*/
132-
private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array
139+
private function convertMappingToArray(array $mapping): array
133140
{
134-
$variables = [];
135-
$mapping = [];
136-
foreach ($filters as $filter) {
137-
foreach ($filter->getDescription($resourceClass) as $variable => $data) {
138-
$variables[] = $variable;
139-
$mapping[] = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $data['property'] ?? null, 'required' => $data['required'] ?? false];
140-
}
141-
}
142-
143-
foreach ($parameters ?? [] as $key => $parameter) {
144-
// Each IriTemplateMapping maps a variable used in the template to a property
145-
if (!$parameter instanceof QueryParameterInterface || false === $parameter->getHydra()) {
146-
continue;
147-
}
148-
149-
if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) {
150-
$filterDescription = $filter->getDescription($resourceClass);
151-
152-
foreach ($filterDescription as $variable => $description) {
153-
// // This is a practice induced by PHP and is not necessary when implementing URI template
154-
if (str_ends_with((string) $variable, '[]')) {
155-
continue;
156-
}
157-
158-
if (!($descriptionProperty = $description['property'] ?? null)) {
159-
continue;
160-
}
161-
162-
if (($prop = $parameter->getProperty()) && $descriptionProperty !== $prop) {
163-
continue;
164-
}
165-
166-
// :property is a pattern allowed when defining parameters
167-
$k = str_replace(':property', $descriptionProperty, $key);
168-
$variable = str_replace($descriptionProperty, $k, $variable);
169-
$variables[] = $variable;
170-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $descriptionProperty];
171-
if (null !== ($required = $parameter->getRequired() ?? $description['required'] ?? null)) {
172-
$m['required'] = $required;
173-
}
174-
$mapping[] = $m;
175-
}
176-
177-
if ($filterDescription) {
178-
continue;
179-
}
141+
$convertedMapping = [];
142+
foreach ($mapping as $m) {
143+
$converted = [
144+
'@type' => 'IriTemplateMapping',
145+
'variable' => $m->variable,
146+
'property' => $m->property,
147+
];
148+
149+
if (null !== ($r = $m->required)) {
150+
$converted['required'] = $r;
180151
}
181152

182-
if (str_contains($key, ':property') && $parameter->getProperties()) {
183-
$required = $parameter->getRequired();
184-
foreach ($parameter->getProperties() as $prop) {
185-
$k = str_replace(':property', $prop, $key);
186-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $k, 'property' => $prop];
187-
$variables[] = $k;
188-
if (null !== $required) {
189-
$m['required'] = $required;
190-
}
191-
$mapping[] = $m;
192-
}
193-
194-
continue;
195-
}
196-
197-
if (!($property = $parameter->getProperty())) {
198-
continue;
199-
}
200-
201-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $key, 'property' => $property];
202-
$variables[] = $key;
203-
if (null !== ($required = $parameter->getRequired())) {
204-
$m['required'] = $required;
205-
}
206-
$mapping[] = $m;
153+
$convertedMapping[] = $converted;
207154
}
208155

209-
return ['@type' => $hydraPrefix.'IriTemplate', $hydraPrefix.'template' => \sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), $hydraPrefix.'variableRepresentation' => 'BasicRepresentation', $hydraPrefix.'mapping' => $mapping];
156+
return $convertedMapping;
210157
}
211158

212159
/**

0 commit comments

Comments
 (0)