CollectionApiEventListener::onPreSerialize()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 7
cp 0
crap 12
rs 10
c 0
b 0
f 0
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\EventListener\Api;
15
16
use ApiPlatform\Exception\InvalidIdentifierException as LegacyInvalidIdentifierException;
17
use ApiPlatform\Metadata\ApiResource;
18
use ApiPlatform\Metadata\CollectionOperationInterface;
19
use ApiPlatform\Metadata\Exception\InvalidIdentifierException;
20
use ApiPlatform\Metadata\HttpOperation;
21
use ApiPlatform\Metadata\Operation;
22
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
23
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
24
use ApiPlatform\State\ProviderInterface;
25
use ApiPlatform\State\UriVariablesResolverTrait;
26
use ApiPlatform\State\Util\RequestParser;
27
use ApiPlatform\Util\AttributesExtractor;
28
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
29
use Silverback\ApiComponentsBundle\Exception\OutOfBoundsException;
30
use Silverback\ApiComponentsBundle\Serializer\SerializeFormatResolver;
31
use Silverback\ApiComponentsBundle\Utility\ApiResourceRouteFinder;
32
use Symfony\Component\HttpFoundation\RequestStack;
33
use Symfony\Component\HttpKernel\Event\ViewEvent;
34
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
35
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
36
37
/**
38
 * @author Daniel West <[email protected]>
39
 */
40
class CollectionApiEventListener
41
{
42
    use UriVariablesResolverTrait;
43
44
    private ApiResourceRouteFinder $resourceRouteFinder;
45
    private ProviderInterface $provider;
46
    private RequestStack $requestStack;
47
    private SerializerContextBuilderInterface $serializerContextBuilder;
48
    private NormalizerInterface $itemNormalizer;
49
    private SerializeFormatResolver $serializeFormatResolver;
50
    private string $itemsPerPageParameterName;
51
    private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory;
52
53
    public function __construct(
54
        ApiResourceRouteFinder $resourceRouteFinder,
55
        ProviderInterface $provider,
56
        RequestStack $requestStack,
57
        SerializerContextBuilderInterface $serializerContextBuilder,
58
        NormalizerInterface $itemNormalizer,
59
        SerializeFormatResolver $serializeFormatResolver,
60
        ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
61
        string $itemsPerPageParameterName
62
    ) {
63
        $this->resourceRouteFinder = $resourceRouteFinder;
64
        $this->provider = $provider;
65
        $this->requestStack = $requestStack;
66
        $this->serializerContextBuilder = $serializerContextBuilder;
67
        $this->itemNormalizer = $itemNormalizer;
68
        $this->serializeFormatResolver = $serializeFormatResolver;
69
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
70
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
71
    }
72
73
    public function supportsTransformation($data, string $to, array $context = []): bool
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

73
    public function supportsTransformation($data, string $to, /** @scrutinizer ignore-unused */ array $context = []): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
74
    {
75
        return $data instanceof Collection && Collection::class === $to;
76
    }
77
78
    public function onPreSerialize(ViewEvent $event)
79
    {
80
        $request = $event->getRequest();
81
        $data = $request->attributes->get('data');
82
        if (
83
            empty($data)
84
            || !$data instanceof Collection
85
        ) {
86
            return;
87
        }
88
        $this->transform($data);
89
    }
90
91
    private function transform(Collection $object): Collection
92
    {
93
        $parameters = $this->resourceRouteFinder->findByIri($object->getResourceIri());
0 ignored issues
show
Bug introduced by
It seems like $object->getResourceIri() can also be of type null; however, parameter $iri of Silverback\ApiComponents...outeFinder::findByIri() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
        $parameters = $this->resourceRouteFinder->findByIri(/** @scrutinizer ignore-type */ $object->getResourceIri());
Loading history...
94
        $attributes = AttributesExtractor::extractAttributes($parameters);
95
        $request = $this->requestStack->getMainRequest();
96
        if (!$request) {
97
            return $object;
98
        }
99
        // Fetch the collection with computed context
100
        $resourceClass = $attributes['resource_class'];
101
102
        $getCollectionOperation = $this->findGetCollectionOperation($resourceClass);
103
        if (!$getCollectionOperation) {
104
            return $object;
105
        }
106
107
        // Build context
108
        $collectionContext = ['operation' => $getCollectionOperation];
109
110
        // Build filters
111
        $filters = [];
112
        if (($perPage = $object->getPerPage()) !== null) {
113
            $filters[$this->itemsPerPageParameterName] = $perPage;
114
        }
115
        if (($defaultQueryParams = $object->getDefaultQueryParameters()) !== null) {
116
            $filters += $defaultQueryParams;
117
        }
118
        if (null === $requestFilters = $request->attributes->get('_api_filters')) {
119
            $queryString = RequestParser::getQueryString($request);
120
            $requestFilters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
121
        }
122
        if ($requestFilters) {
123
            // not += because we want to overwrite with an empty string if provided in querystring.
124
            // e.g. a default search value could be overridden by no search value
125
            $filters = array_merge($filters, $requestFilters);
126
        }
127
        $collectionContext['filters'] = $filters;
128
129
        // Compose context for provider
130
        $collectionContext += $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
131
132
        try {
133
            $uriVariables = $this->getOperationUriVariables($getCollectionOperation, $parameters, $resourceClass);
134
            // Operation $operation, array $uriVariables = [], array $context = []
135
            $collectionData = $this->provider->provide($getCollectionOperation, $uriVariables, $collectionContext);
136
        } catch (InvalidIdentifierException|LegacyInvalidIdentifierException $e) {
137
            throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
138
        }
139
140
        // Normalize the collection into an array
141
        // Pagination disabled
142
        if (\is_array($collectionData)) {
143
            $collection = $collectionData;
144
        } else {
145
            if (!$collectionData instanceof \Traversable) {
146
                throw new OutOfBoundsException('$collectionData should be Traversable');
147
            }
148
            $collection = iterator_count($collectionData) ? $collectionData : [];
149
        }
150
        $format = $this->serializeFormatResolver->getFormatFromRequest($request);
151
        $normalizedCollection = $this->itemNormalizer->normalize($collection, $format, $normalizationContext);
152
153
        // Update the original collection resource
154
        $object->setCollection($normalizedCollection);
155
156
        return $object;
157
    }
158
159
    private function findGetCollectionOperation(string $resourceClass): ?HttpOperation
160
    {
161
        $metadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
162
        $it = $metadata->getIterator();
163
        /** @var ApiResource $apiResource */
164
        foreach ($it as $apiResource) {
165
            $operations = $apiResource->getOperations();
166
            if ($operations) {
167
                /** @var Operation $operation */
168
                foreach ($operations as $operation) {
169
                    if (
170
                        $operation instanceof CollectionOperationInterface
171
                        && $operation instanceof HttpOperation
172
                        && HttpOperation::METHOD_GET === $operation->getMethod()
173
                    ) {
174
                        return $operation;
175
                    }
176
                }
177
            }
178
        }
179
180
        return null;
181
    }
182
}
183