1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Silverback API Components Bundle Project |
5
|
|
|
* |
6
|
|
|
* (c) Daniel West <[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 Silverback\ApiComponentsBundle\DataTransformer; |
15
|
|
|
|
16
|
|
|
use ApiPlatform\Core\Api\IriConverterInterface; |
17
|
|
|
use ApiPlatform\Core\Api\OperationType; |
18
|
|
|
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface; |
19
|
|
|
use ApiPlatform\Core\DataTransformer\DataTransformerInterface; |
20
|
|
|
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
21
|
|
|
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; |
22
|
|
|
use ApiPlatform\Core\Util\RequestParser; |
23
|
|
|
use Silverback\ApiComponentsBundle\Entity\Component\Collection; |
24
|
|
|
use Silverback\ApiComponentsBundle\Serializer\SerializeFormatResolver; |
25
|
|
|
use Symfony\Component\HttpFoundation\Request; |
26
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
27
|
|
|
use Symfony\Component\HttpFoundation\UrlHelper; |
28
|
|
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
29
|
|
|
use Traversable; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @author Daniel West <[email protected]> |
33
|
|
|
*/ |
34
|
|
|
class CollectionOutputDataTransformer implements DataTransformerInterface |
35
|
|
|
{ |
36
|
|
|
private array $transformed = []; |
37
|
|
|
private RequestStack $requestStack; |
38
|
|
|
private ResourceMetadataFactoryInterface $resourceMetadataFactory; |
39
|
|
|
private OperationPathResolverInterface $operationPathResolver; |
40
|
|
|
private ContextAwareCollectionDataProviderInterface $dataProvider; |
41
|
|
|
private IriConverterInterface $iriConverter; |
42
|
|
|
private NormalizerInterface $itemNormalizer; |
43
|
|
|
private SerializeFormatResolver $requestFormatResolver; |
44
|
|
|
private UrlHelper $urlHelper; |
45
|
|
|
private string $itemsPerPageParameterName; |
46
|
|
|
|
47
|
|
|
public function __construct(RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContextAwareCollectionDataProviderInterface $dataProvider, IriConverterInterface $iriConverter, NormalizerInterface $itemNormalizer, SerializeFormatResolver $requestFormatResolver, UrlHelper $urlHelper, string $itemsPerPageParameterName = 'perPage') |
48
|
|
|
{ |
49
|
|
|
$this->requestStack = $requestStack; |
50
|
|
|
$this->resourceMetadataFactory = $resourceMetadataFactory; |
51
|
|
|
$this->operationPathResolver = $operationPathResolver; |
52
|
|
|
$this->dataProvider = $dataProvider; |
53
|
|
|
$this->iriConverter = $iriConverter; |
54
|
|
|
$this->itemNormalizer = $itemNormalizer; |
55
|
|
|
$this->requestFormatResolver = $requestFormatResolver; |
56
|
|
|
$this->urlHelper = $urlHelper; |
57
|
|
|
$this->itemsPerPageParameterName = $itemsPerPageParameterName; |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* {@inheritdoc} |
62
|
|
|
* |
63
|
|
|
* @param Collection $collection |
64
|
|
|
*/ |
65
|
|
|
public function transform($collection, string $to, array $context = []) |
66
|
|
|
{ |
67
|
|
|
$this->transformed[] = $collection->getId(); |
68
|
|
|
|
69
|
|
|
$request = $this->requestStack->getMasterRequest(); |
70
|
|
|
if (!$request) { |
71
|
|
|
return $collection; |
72
|
|
|
} |
73
|
|
|
$format = $this->requestFormatResolver->getFormatFromRequest($request); |
74
|
|
|
|
75
|
|
|
$this->addEndpoints($collection, $format); |
76
|
|
|
$this->addCollection($collection, $format, $request); |
77
|
|
|
|
78
|
|
|
return $collection; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
private function addCollection(Collection $collection, string $format, Request $request): void |
82
|
|
|
{ |
83
|
|
|
$filters = $this->getFilters($collection, $request); |
84
|
|
|
$dataProviderContext = $this->getDataProviderContext($collection, $request, $filters); |
85
|
|
|
$resourceClass = $collection->getResourceClass(); |
86
|
|
|
|
87
|
|
|
$endpoints = $collection->getEndpoints(); |
88
|
|
|
$forcedContext = [ |
89
|
|
|
'resource_class' => $resourceClass, |
90
|
|
|
'request_uri' => $endpoints ? $endpoints->get('get') : null, |
91
|
|
|
'jsonld_has_context' => false, |
92
|
|
|
'api_sub_level' => null, |
93
|
|
|
'subresource_operation_name' => 'get', |
94
|
|
|
]; |
95
|
|
|
$normalizerContext = array_merge([], $forcedContext); |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* !!! WE NEED TO CHECK WHETHER THIS WILL WORK WHEN PAGINATION IS DISABLED... |
99
|
|
|
* DOES PAGINATION OBJECT GET RETURNED OR ANOTHER ITERABLE OR IS IT JUST AN ARRAY?? |
100
|
|
|
* THIS WILL NEED TO BE HANDLED !!! |
101
|
|
|
* |
102
|
|
|
* @var Traversable |
103
|
|
|
*/ |
104
|
|
|
$resourceCollection = $this->dataProvider->getCollection($resourceClass, Request::METHOD_GET, $dataProviderContext); |
105
|
|
|
|
106
|
|
|
$normalizedCollection = $this->itemNormalizer->normalize( |
107
|
|
|
$resourceCollection, |
108
|
|
|
$format, |
109
|
|
|
$normalizerContext |
110
|
|
|
); |
111
|
|
|
// IS THIS CHECK NEEDED?? |
112
|
|
|
if (\is_array($normalizedCollection)) { |
113
|
|
|
$collection->setCollection($normalizedCollection); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
$resources = array_map(function ($object) { |
117
|
|
|
return $this->iriConverter->getIriFromItem($object); |
118
|
|
|
}, (array) $resourceCollection->getIterator()); |
119
|
|
|
|
120
|
|
|
$request->attributes->set('_resources', $request->attributes->get('_resources', []) + $resources); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
private function getDataProviderContext(Collection $collection, Request $request, array $filters): array |
124
|
|
|
{ |
125
|
|
|
$itemsPerPage = $collection->getPerPage(); |
126
|
|
|
$isPaginated = (bool) $itemsPerPage; |
127
|
|
|
$dataProviderContext = ['filters' => $filters]; |
128
|
|
|
if ($isPaginated) { |
129
|
|
|
$dataProviderContext['filters'] = $dataProviderContext['filters'] ?? []; |
130
|
|
|
$dataProviderContext['filters'] = array_merge($dataProviderContext['filters'], [ |
131
|
|
|
'pagination' => true, |
132
|
|
|
$this->itemsPerPageParameterName => $itemsPerPage, |
133
|
|
|
'_page' => 1, |
134
|
|
|
]); |
135
|
|
|
$request->attributes->set('_api_pagination', [ |
136
|
|
|
'pagination' => 'true', |
137
|
|
|
$this->itemsPerPageParameterName => $itemsPerPage, |
138
|
|
|
]); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $dataProviderContext; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
private function getFilters(Collection $collection, Request $request): array |
145
|
|
|
{ |
146
|
|
|
if (null === $filters = $request->attributes->get('_api_filters')) { |
147
|
|
|
$defaultQueryString = $collection->getDefaultQueryParameters(); |
148
|
|
|
$setDefaultQuery = $defaultQueryString && !$request->server->get('QUERY_STRING'); |
149
|
|
|
if ($setDefaultQuery) { |
150
|
|
|
$request->server->set('QUERY_STRING', http_build_query($defaultQueryString)); |
151
|
|
|
} |
152
|
|
|
$queryString = RequestParser::getQueryString($request); |
153
|
|
|
$filters = $queryString ? RequestParser::parseRequestParams($queryString) : []; |
154
|
|
|
if ($setDefaultQuery) { |
155
|
|
|
$request->server->set('QUERY_STRING', ''); |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return $filters; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
private function addEndpoints(Collection $collection, string $format): void |
163
|
|
|
{ |
164
|
|
|
$resourceMetadata = $this->resourceMetadataFactory->create($collection->getResourceClass()); |
165
|
|
|
$collectionOperations = array_change_key_case($resourceMetadata->getCollectionOperations() ?? [], CASE_LOWER); |
166
|
|
|
if (!empty($collectionOperations) && ($shortName = $resourceMetadata->getShortName())) { |
167
|
|
|
$baseRoute = '/' . trim($resourceMetadata->getAttribute('route_prefix', ''), ' /'); |
168
|
|
|
$methods = array_map(static function ($str) { return strtolower($str); }, [Request::METHOD_GET, Request::METHOD_POST]); |
169
|
|
|
foreach ($methods as $method) { |
170
|
|
|
if (!\array_key_exists($method, $collectionOperations)) { |
171
|
|
|
continue; |
172
|
|
|
} |
173
|
|
|
$path = $this->operationPathResolver->resolveOperationPath( |
174
|
|
|
$shortName, |
175
|
|
|
$collectionOperations[$method], |
176
|
|
|
OperationType::COLLECTION |
177
|
|
|
); |
178
|
|
|
$finalPath = preg_replace('/{_format}$/', $format, $path); |
179
|
|
|
$absoluteUrl = $this->urlHelper->getAbsoluteUrl($baseRoute . $finalPath); |
180
|
|
|
$collection->addEndpoint($method, $absoluteUrl); |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* {@inheritdoc} |
187
|
|
|
*/ |
188
|
|
|
public function supportsTransformation($data, string $to, array $context = []): bool |
189
|
|
|
{ |
190
|
|
|
return $data instanceof Collection && |
191
|
|
|
Collection::class === $to && |
192
|
|
|
!\in_array($data->getId(), $this->transformed, true); |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|