Skip to content

Commit 7a47f7d

Browse files
committed
refactor
1 parent 0a313c6 commit 7a47f7d

File tree

7 files changed

+254
-199
lines changed

7 files changed

+254
-199
lines changed

src/Hydra/IriTemplate.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
namespace ApiPlatform\Hydra;
1515

1616
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
use Symfony\Component\Serializer\Annotation\SerializedName;
1718

1819
final class IriTemplate
1920
{
2021
#[StreamedName('@type')]
22+
#[SerializedName('@type')]
2123
public string $type = 'IriTemplate';
2224

2325
public function __construct(

src/Hydra/IriTemplateMapping.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414
namespace ApiPlatform\Hydra;
1515

1616
use Symfony\Component\JsonStreamer\Attribute\StreamedName;
17+
use Symfony\Component\Serializer\Annotation\SerializedName;
1718

1819
class IriTemplateMapping
1920
{
2021
#[StreamedName('@type')]
22+
#[SerializedName('@type')]
2123
public string $type = 'IriTemplateMapping';
2224

2325
public function __construct(
2426
public string $variable,
25-
public string $property,
27+
public ?string $property,
2628
public bool $required = false,
2729
) {
2830
}

src/Hydra/Serializer/CollectionFiltersNormalizer.php

Lines changed: 9 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2121
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2222
use ApiPlatform\State\Util\StateOptionsTrait;
23+
use ApiPlatform\Hydra\State\Util\SearchHelperTrait;
2324
use Psr\Container\ContainerInterface;
2425
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
2526
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
@@ -35,6 +36,7 @@ final class CollectionFiltersNormalizer implements NormalizerInterface, Normaliz
3536
{
3637
use HydraPrefixTrait;
3738
use StateOptionsTrait;
39+
use SearchHelperTrait;
3840
private ?ContainerInterface $filterLocator = null;
3941

4042
/**
@@ -105,7 +107,13 @@ public function normalize(mixed $object, ?string $format = null, array $context
105107

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

111119
return $data;
@@ -121,91 +129,6 @@ public function setNormalizer(NormalizerInterface $normalizer): void
121129
}
122130
}
123131

124-
/**
125-
* Returns the content of the Hydra search property.
126-
*
127-
* @param FilterInterface[] $filters
128-
*/
129-
private function getSearch(string $resourceClass, array $parts, array $filters, ?Parameters $parameters, string $hydraPrefix): array
130-
{
131-
$variables = [];
132-
$mapping = [];
133-
foreach ($filters as $filter) {
134-
foreach ($filter->getDescription($resourceClass) as $variable => $data) {
135-
$variables[] = $variable;
136-
$mapping[] = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $data['property'] ?? null, 'required' => $data['required'] ?? false];
137-
}
138-
}
139-
140-
foreach ($parameters ?? [] as $key => $parameter) {
141-
// Each IriTemplateMapping maps a variable used in the template to a property
142-
if (!$parameter instanceof QueryParameterInterface || false === $parameter->getHydra()) {
143-
continue;
144-
}
145-
146-
if (($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $this->getFilter($filterId))) {
147-
$filterDescription = $filter->getDescription($resourceClass);
148-
149-
foreach ($filterDescription as $variable => $description) {
150-
// // This is a practice induced by PHP and is not necessary when implementing URI template
151-
if (str_ends_with((string) $variable, '[]')) {
152-
continue;
153-
}
154-
155-
if (!($descriptionProperty = $description['property'] ?? null)) {
156-
continue;
157-
}
158-
159-
if (($prop = $parameter->getProperty()) && $descriptionProperty !== $prop) {
160-
continue;
161-
}
162-
163-
// :property is a pattern allowed when defining parameters
164-
$k = str_replace(':property', $descriptionProperty, $key);
165-
$variable = str_replace($descriptionProperty, $k, $variable);
166-
$variables[] = $variable;
167-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $descriptionProperty];
168-
if (null !== ($required = $parameter->getRequired() ?? $description['required'] ?? null)) {
169-
$m['required'] = $required;
170-
}
171-
$mapping[] = $m;
172-
}
173-
174-
if ($filterDescription) {
175-
continue;
176-
}
177-
}
178-
179-
if (str_contains($key, ':property') && $parameter->getProperties()) {
180-
$required = $parameter->getRequired();
181-
foreach ($parameter->getProperties() as $prop) {
182-
$k = str_replace(':property', $prop, $key);
183-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $k, 'property' => $prop];
184-
$variables[] = $k;
185-
if (null !== $required) {
186-
$m['required'] = $required;
187-
}
188-
$mapping[] = $m;
189-
}
190-
191-
continue;
192-
}
193-
194-
if (!($property = $parameter->getProperty())) {
195-
continue;
196-
}
197-
198-
$m = ['@type' => 'IriTemplateMapping', 'variable' => $key, 'property' => $property];
199-
$variables[] = $key;
200-
if (null !== ($required = $parameter->getRequired())) {
201-
$m['required'] = $required;
202-
}
203-
$mapping[] = $m;
204-
}
205-
206-
return ['@type' => $hydraPrefix.'IriTemplate', $hydraPrefix.'template' => \sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), $hydraPrefix.'variableRepresentation' => 'BasicRepresentation', $hydraPrefix.'mapping' => $mapping];
207-
}
208-
209132
/**
210133
* Gets a filter with a backward compatibility.
211134
*/

src/Hydra/Serializer/PartialCollectionViewNormalizer.php

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use ApiPlatform\Metadata\Util\IriHelper;
2121
use ApiPlatform\State\Pagination\PaginatorInterface;
2222
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
23+
use ApiPlatform\Hydra\State\Util\PaginationHelperTrait;
24+
use ApiPlatform\Hydra\PartialCollectionView;
2325
use Symfony\Component\PropertyAccess\PropertyAccess;
2426
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2527
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -35,6 +37,7 @@
3537
final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface
3638
{
3739
use HydraPrefixTrait;
40+
use PaginationHelperTrait;
3841
private readonly PropertyAccessorInterface $propertyAccessor;
3942

4043
/**
@@ -60,21 +63,11 @@ public function normalize(mixed $object, ?string $format = null, array $context
6063
throw new UnexpectedValueException('Expected data to be an array');
6164
}
6265

63-
$currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null;
64-
if ($paginated = ($object instanceof PartialPaginatorInterface)) {
65-
if ($object instanceof PaginatorInterface) {
66-
$paginated = 1. !== $lastPage = $object->getLastPage();
67-
} else {
68-
$itemsPerPage = $object->getItemsPerPage();
69-
$pageTotalItems = (float) \count($object);
70-
}
71-
72-
$currentPage = $object->getCurrentPage();
66+
$paginated = ($object instanceof PartialPaginatorInterface);
67+
if ($paginated && $object instanceof PaginatorInterface) {
68+
$paginated = 1. !== $object->getLastPage();
7369
}
7470

75-
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
76-
// We should not rely on the request_uri but instead rely on the UriTemplate
77-
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
7871
$parsed = IriHelper::parseIri($context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName);
7972
$appliedFilters = $parsed['parameters'];
8073
unset($appliedFilters[$this->enabledParameterName]);
@@ -94,18 +87,35 @@ public function normalize(mixed $object, ?string $format = null, array $context
9487
$isPaginatedWithCursor = (bool) $cursorPaginationAttribute;
9588

9689
$hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext);
97-
$data[$hydraPrefix.'view'] = ['@id' => null, '@type' => $hydraPrefix.'PartialCollectionView'];
9890

9991
if ($isPaginatedWithCursor) {
92+
$data[$hydraPrefix.'view'] = ['@id' => null, '@type' => $hydraPrefix.'PartialCollectionView'];
93+
10094
return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy, $hydraPrefix);
10195
}
10296

103-
$data[$hydraPrefix.'view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
97+
$partialCollectionView = $this->getPartialCollectionView($object, $context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName, $this->enabledParameterName, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
98+
99+
$view = [
100+
'@id' => $partialCollectionView->id,
101+
'@type' => $hydraPrefix.'PartialCollectionView',
102+
];
103+
104+
if (null !== $partialCollectionView->first) {
105+
$view[$hydraPrefix.'first'] = $partialCollectionView->first;
106+
$view[$hydraPrefix.'last'] = $partialCollectionView->last;
107+
}
104108

105-
if ($paginated) {
106-
return $this->populateDataWithPagination($data, $parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy, $hydraPrefix);
109+
if (null !== $partialCollectionView->previous) {
110+
$view[$hydraPrefix.'previous'] = $partialCollectionView->previous;
107111
}
108112

113+
if (null !== $partialCollectionView->next) {
114+
$view[$hydraPrefix.'next'] = $partialCollectionView->next;
115+
}
116+
117+
$data[$hydraPrefix.'view'] = $view;
118+
109119
return $data;
110120
}
111121

@@ -168,22 +178,4 @@ private function populateDataWithCursorBasedPagination(array $data, array $parse
168178

169179
return $data;
170180
}
171-
172-
private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems, ?int $urlGenerationStrategy, string $hydraPrefix): array
173-
{
174-
if (null !== $lastPage) {
175-
$data[$hydraPrefix.'view'][$hydraPrefix.'first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
176-
$data[$hydraPrefix.'view'][$hydraPrefix.'last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
177-
}
178-
179-
if (1. !== $currentPage) {
180-
$data[$hydraPrefix.'view'][$hydraPrefix.'previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
181-
}
182-
183-
if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) {
184-
$data[$hydraPrefix.'view'][$hydraPrefix.'next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy);
185-
}
186-
187-
return $data;
188-
}
189181
}

src/Hydra/State/JsonStreamerProcessor.php

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2828
use ApiPlatform\Metadata\UrlGeneratorInterface;
2929
use ApiPlatform\Metadata\Util\IriHelper;
30+
use ApiPlatform\State\Util\HttpResponseStatusTrait;
3031
use ApiPlatform\State\Pagination\PaginatorInterface;
31-
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
3232
use ApiPlatform\State\ProcessorInterface;
3333
use ApiPlatform\State\Util\HttpResponseHeadersTrait;
34-
use ApiPlatform\State\Util\HttpResponseStatusTrait;
3534
use Symfony\Component\HttpFoundation\Response;
3635
use Symfony\Component\HttpFoundation\StreamedResponse;
3736
use Symfony\Component\JsonStreamer\StreamWriterInterface;
37+
use ApiPlatform\Hydra\State\Util\PaginationHelperTrait;
38+
use ApiPlatform\Hydra\State\Util\SearchHelperTrait;
3839
use Symfony\Component\TypeInfo\Type;
3940

4041
/**
@@ -44,6 +45,8 @@ final class JsonStreamerProcessor implements ProcessorInterface
4445
{
4546
use HttpResponseHeadersTrait;
4647
use HttpResponseStatusTrait;
48+
use PaginationHelperTrait;
49+
use SearchHelperTrait;
4750

4851
/**
4952
* @param ProcessorInterface<mixed,mixed> $processor
@@ -86,7 +89,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
8689
$collection->view = $this->getView($data, $requestUri, $operation);
8790

8891
if ($operation->getParameters()) {
89-
$collection->search = $this->getSearch($operation, $requestUri);
92+
$parts = parse_url($requestUri);
93+
$collection->search = $this->getSearch($parts['path'] ?? '', $operation);
9094
}
9195

9296
if ($data instanceof PaginatorInterface) {
@@ -119,81 +123,8 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
119123
return $this->processor->process($response, $operation, $uriVariables, $context);
120124
}
121125

122-
// TODO: These come from our Hydra collection normalizer, try to share the logic
123-
private function getSearch(Operation $operation, string $requestUri): IriTemplate
124-
{
125-
/** @var list<IriTemplateMapping> */
126-
$mapping = [];
127-
$keys = [];
128-
129-
foreach ($operation->getParameters() ?? [] as $key => $parameter) {
130-
if (!$parameter instanceof QueryParameterInterface || false === $parameter->getHydra()) {
131-
continue;
132-
}
133-
134-
if (!($property = $parameter->getProperty())) {
135-
continue;
136-
}
137-
138-
$keys[] = $key;
139-
$m = new IriTemplateMapping(
140-
variable: $key,
141-
property: $property,
142-
required: $parameter->getRequired() ?? false
143-
);
144-
$mapping[] = $m;
145-
}
146-
147-
$parts = parse_url($requestUri);
148-
149-
return new IriTemplate(
150-
variableRepresentation: 'BasicRepresentation',
151-
mapping: $mapping,
152-
template: \sprintf('%s{?%s}', $parts['path'] ?? '', implode(',', $keys)),
153-
);
154-
}
155-
156126
private function getView(mixed $object, string $requestUri, Operation $operation): PartialCollectionView
157127
{
158-
$currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null;
159-
if ($paginated = ($object instanceof PartialPaginatorInterface)) {
160-
if ($object instanceof PaginatorInterface) {
161-
$paginated = 1. !== $lastPage = $object->getLastPage();
162-
} else {
163-
$itemsPerPage = $object->getItemsPerPage();
164-
$pageTotalItems = (float) \count($object);
165-
}
166-
167-
$currentPage = $object->getCurrentPage();
168-
}
169-
170-
// TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer
171-
// We should not rely on the request_uri but instead rely on the UriTemplate
172-
// This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController)
173-
$parsed = IriHelper::parseIri($requestUri, $this->pageParameterName);
174-
$appliedFilters = $parsed['parameters'];
175-
unset($appliedFilters[$this->enabledParameterName]);
176-
177-
$urlGenerationStrategy = $operation->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy;
178-
$id = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy);
179-
if (!$appliedFilters && !$paginated) {
180-
return new PartialCollectionView($id);
181-
}
182-
183-
$first = $last = $previous = $next = null;
184-
if (null !== $lastPage) {
185-
$first = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy);
186-
$last = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy);
187-
}
188-
189-
if (1. !== $currentPage) {
190-
$previous = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy);
191-
}
192-
193-
if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) {
194-
$next = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy);
195-
}
196-
197-
return new PartialCollectionView($id, $first, $last, $previous, $next);
128+
return $this->getPartialCollectionView($object, $requestUri, $this->pageParameterName, $this->enabledParameterName, $operation->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy);
198129
}
199130
}

0 commit comments

Comments
 (0)