Test Failed
Pull Request — main (#140)
by Daniel
04:22
created

findGetCollectionOperation()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 9
nc 5
nop 1
dl 0
loc 18
ccs 0
cts 0
cp 0
crap 42
rs 9.2222
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\DataTransformer;
15
16
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
17
use ApiPlatform\Exception\InvalidIdentifierException;
18
use ApiPlatform\Metadata\ApiResource;
19
use ApiPlatform\Metadata\CollectionOperationInterface;
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\Util\AttributesExtractor;
27
use ApiPlatform\Util\RequestParser;
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\Exception\NotFoundHttpException;
34
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
35
36
/**
37
 * @author Daniel West <[email protected]>
38
 */
39
class CollectionOutputDataTransformer implements DataTransformerInterface
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
73
    {
74
        return $data instanceof Collection && Collection::class === $to;
75
    }
76
77
    /**
78
     * @param object|Collection $object
79
     */
80
    public function transform($object, string $to, array $context = []): Collection
81
    {
82
        $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

82
        $parameters = $this->resourceRouteFinder->findByIri(/** @scrutinizer ignore-type */ $object->getResourceIri());
Loading history...
83
        $attributes = AttributesExtractor::extractAttributes($parameters);
84
        $request = $this->requestStack->getMainRequest();
85
        if (!$request) {
86
            return $object;
87
        }
88
        // Fetch the collection with computed context
89
        $resourceClass = $attributes['resource_class'];
90
91
        $getCollectionOperation = $this->findGetCollectionOperation($resourceClass);
92
        if (!$getCollectionOperation) {
93
            return $object;
94
        }
95
96
        // Build context
97
        $collectionContext = ['operation' => $getCollectionOperation];
98
99
        // Build filters
100
        $filters = [];
101
        if (($perPage = $object->getPerPage()) !== null) {
102
            $filters[$this->itemsPerPageParameterName] = $perPage;
103
        }
104
        if (($defaultQueryParams = $object->getDefaultQueryParameters()) !== null) {
105
            $filters += $defaultQueryParams;
106
        }
107
        if (null === $requestFilters = $request->attributes->get('_api_filters')) {
108
            $queryString = RequestParser::getQueryString($request);
109
            $requestFilters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
110
        }
111
        if ($requestFilters) {
112
            // not += because we want to overwrite with an empty string if provided in querystring.
113
            // e.g. a default search value could be overridden by no search value
114
            $filters = array_merge($filters, $requestFilters);
115
        }
116
        $collectionContext['filters'] = $filters;
117
118
        // Compose context for provider
119
        $collectionContext += $normalizationContext = $this->serializerContextBuilder->createFromRequest($request, true, $attributes);
120
121
        try {
122
            $uriVariables = $this->getOperationUriVariables($getCollectionOperation, $parameters, $resourceClass);
123
            $collectionData = $this->provider->provide($resourceClass, $uriVariables, $getCollectionOperation->getName(), $collectionContext);
0 ignored issues
show
Bug introduced by
$getCollectionOperation->getName() of type null|string is incompatible with the type array expected by parameter $context of ApiPlatform\State\ProviderInterface::provide(). ( Ignorable by Annotation )

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

123
            $collectionData = $this->provider->provide($resourceClass, $uriVariables, /** @scrutinizer ignore-type */ $getCollectionOperation->getName(), $collectionContext);
Loading history...
Bug introduced by
$resourceClass of type string is incompatible with the type ApiPlatform\Metadata\Operation expected by parameter $operation of ApiPlatform\State\ProviderInterface::provide(). ( Ignorable by Annotation )

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

123
            $collectionData = $this->provider->provide(/** @scrutinizer ignore-type */ $resourceClass, $uriVariables, $getCollectionOperation->getName(), $collectionContext);
Loading history...
Unused Code introduced by
The call to ApiPlatform\State\ProviderInterface::provide() has too many arguments starting with $collectionContext. ( Ignorable by Annotation )

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

123
            /** @scrutinizer ignore-call */ 
124
            $collectionData = $this->provider->provide($resourceClass, $uriVariables, $getCollectionOperation->getName(), $collectionContext);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
124
        } catch (InvalidIdentifierException $e) {
125
            throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
126
        }
127
128
        // Normalize the collection into an array
129
        // Pagination disabled
130
        if (\is_array($collectionData)) {
131
            $collection = $collectionData;
132
        } else {
133
            if (!$collectionData instanceof \Traversable) {
134
                throw new OutOfBoundsException('$collectionData should be Traversable');
135
            }
136
            $collection = iterator_count($collectionData) ? $collectionData : [];
137
        }
138
        $format = $this->serializeFormatResolver->getFormatFromRequest($request);
139
        $normalizedCollection = $this->itemNormalizer->normalize($collection, $format, $normalizationContext);
140
141
        // Update the original collection resource
142
        $object->setCollection($normalizedCollection);
143
144
        return $object;
145
    }
146
147
    private function findGetCollectionOperation(string $resourceClass): ?Operation
148
    {
149
        $metadata = $this->resourceMetadataCollectionFactory->create($resourceClass);
150
        $it = $metadata->getIterator();
151
        /** @var ApiResource $apiResource */
152
        foreach ($it as $apiResource) {
153
            $operations = $apiResource->getOperations();
154
            if ($operations) {
155
                /** @var Operation $operation */
156
                foreach ($operations as $operation) {
157
                    if ($operation instanceof CollectionOperationInterface && HttpOperation::METHOD_GET === $operation->getMethod()) {
0 ignored issues
show
Bug introduced by
The method getMethod() does not exist on ApiPlatform\Metadata\CollectionOperationInterface. It seems like you code against a sub-type of ApiPlatform\Metadata\CollectionOperationInterface such as ApiPlatform\Metadata\GetCollection. ( Ignorable by Annotation )

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

157
                    if ($operation instanceof CollectionOperationInterface && HttpOperation::METHOD_GET === $operation->/** @scrutinizer ignore-call */ getMethod()) {
Loading history...
158
                        return $operation;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $operation returns the type ApiPlatform\Metadata\CollectionOperationInterface which is incompatible with the type-hinted return ApiPlatform\Metadata\Operation|null.
Loading history...
159
                    }
160
                }
161
            }
162
        }
163
164
        return null;
165
    }
166
}
167