CollectionApiEventListener::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 18
ccs 0
cts 9
cp 0
crap 2
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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