Test Failed
Pull Request — main (#140)
by Daniel
15:33
created

CollectionApiEventListener::onPreSerialize()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 11
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;
17
use ApiPlatform\Metadata\ApiResource;
18
use ApiPlatform\Metadata\CollectionOperationInterface;
19
use ApiPlatform\Metadata\HttpOperation;
20
use ApiPlatform\Metadata\Operation;
21
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
22
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
23
use ApiPlatform\State\ProviderInterface;
24
use ApiPlatform\State\UriVariablesResolverTrait;
25
use ApiPlatform\Util\AttributesExtractor;
26
use ApiPlatform\Util\RequestParser;
27
use Silverback\ApiComponentsBundle\Entity\Component\Collection;
28
use Silverback\ApiComponentsBundle\Exception\OutOfBoundsException;
29
use Silverback\ApiComponentsBundle\Serializer\SerializeFormatResolver;
30
use Silverback\ApiComponentsBundle\Utility\ApiResourceRouteFinder;
31
use Symfony\Component\HttpFoundation\RequestStack;
32
use Symfony\Component\HttpKernel\Event\ViewEvent;
33
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
34
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
35
36
/**
37
 * @author Daniel West <[email protected]>
38
 */
39
class CollectionApiEventListener
40
{
41
    use UriVariablesResolverTrait;
42
43
    private ApiResourceRouteFinder $resourceRouteFinder;
44
    private ProviderInterface $provider;
45
    private RequestStack $requestStack;
46
    private SerializerContextBuilderInterface $serializerContextBuilder;
47
    private NormalizerInterface $itemNormalizer;
48
    private SerializeFormatResolver $serializeFormatResolver;
49
    private string $itemsPerPageParameterName;
50
    private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory;
51
52
    public function __construct(
53
        ApiResourceRouteFinder $resourceRouteFinder,
54
        ProviderInterface $provider,
55
        RequestStack $requestStack,
56
        SerializerContextBuilderInterface $serializerContextBuilder,
57
        NormalizerInterface $itemNormalizer,
58
        SerializeFormatResolver $serializeFormatResolver,
59
        ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
60
        string $itemsPerPageParameterName
61
    ) {
62
        $this->resourceRouteFinder = $resourceRouteFinder;
63
        $this->provider = $provider;
64
        $this->requestStack = $requestStack;
65
        $this->serializerContextBuilder = $serializerContextBuilder;
66
        $this->itemNormalizer = $itemNormalizer;
67
        $this->serializeFormatResolver = $serializeFormatResolver;
68
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
69
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
70
    }
71
72
    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

72
    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...
73
    {
74
        return $data instanceof Collection && Collection::class === $to;
75
    }
76
77
    public function onPreSerialize(ViewEvent $event)
78
    {
79
        $request = $event->getRequest();
80
        $data = $request->attributes->get('data');
81
        if (
82
            empty($data) ||
83
            !$data instanceof Collection
84
        ) {
85
            return;
86
        }
87
        $this->transform($data);
88
    }
89
90
    private function transform(Collection $object): Collection
91
    {
92
        $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

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