Completed
Push — v2 ( d0d63b...85e3cb )
by Daniel
04:19
created

getDataProviderContext()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 13
nc 4
nop 3
dl 0
loc 22
ccs 0
cts 14
cp 0
crap 12
rs 9.8333
c 1
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Component 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\ApiComponentBundle\DataTransformer;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\Api\OperationType;
18
use ApiPlatform\Core\Bridge\Doctrine\Orm\Paginator;
19
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
20
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
21
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
23
use ApiPlatform\Core\Util\RequestParser;
24
use Silverback\ApiComponentBundle\Action\AbstractAction;
25
use Silverback\ApiComponentBundle\Dto\Collection;
26
use Silverback\ApiComponentBundle\Entity\Component\Collection as CollectionResource;
27
use Symfony\Component\HttpFoundation\Request;
28
use Symfony\Component\HttpFoundation\RequestStack;
29
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
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
44
    public function __construct(RequestStack $requestStack, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContextAwareCollectionDataProviderInterface $dataProvider, IriConverterInterface $iriConverter, NormalizerInterface $itemNormalizer)
45
    {
46
        $this->requestStack = $requestStack;
47
        $this->resourceMetadataFactory = $resourceMetadataFactory;
48
        $this->operationPathResolver = $operationPathResolver;
49
        $this->dataProvider = $dataProvider;
50
        $this->iriConverter = $iriConverter;
51
        $this->itemNormalizer = $itemNormalizer;
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     *
57
     * @param CollectionResource $data
58
     */
59
    public function transform($data, string $to, array $context = [])
60
    {
61
        $this->transformed[] = $data->getId();
62
63
        $collection = new Collection($data);
64
65
        $request = $this->requestStack->getCurrentRequest();
66
        if (!$request) {
67
            return $collection;
68
        }
69
        $format = AbstractAction::getRequestFormat($request);
70
71
        $this->addEndpoints($collection, $format);
72
        $this->addCollection($collection, $format, $request);
73
74
        return $collection;
75
    }
76
77
    private function addCollection(Collection $object, string $format, Request $request): void
78
    {
79
        $collectionResource = $object->getResource();
80
        $filters = $this->getFilters($collectionResource, $request);
81
        $dataProviderContext = $this->getDataProviderContext($collectionResource, $request, $filters);
82
        $resourceClass = $collectionResource->getResourceClass();
83
84
        /** @var Paginator $paginator */
85
        $paginator = $this->dataProvider->getCollection($resourceClass, Request::METHOD_GET, $dataProviderContext);
86
87
        $resources = array_map(function ($object) {
88
            return $this->iriConverter->getIriFromItem($object);
89
        }, (array) $paginator->getIterator());
90
        $request->attributes->set('_resources', $request->attributes->get('_resources', []) + $resources);
91
92
        $endpoints = $object->getEndpoints();
93
        $forcedContext = [
94
            'resource_class' => $resourceClass,
95
            'request_uri' => $endpoints ? $endpoints->get('get') : null,
96
            'jsonld_has_context' => false,
97
            'api_sub_level' => null,
98
            'subresource_operation_name' => 'get',
99
        ];
100
        $normalizerContext = array_merge([], $forcedContext);
101
        $normalizedCollection = $this->itemNormalizer->normalize(
102
            $paginator,
103
            $format,
104
            $normalizerContext
105
        );
106
        if (\is_array($normalizedCollection)) {
107
            $object->setCollection($normalizedCollection);
108
        }
109
    }
110
111
    private function getDataProviderContext(CollectionResource $collectionResource, Request $request, ?array $filters): array
112
    {
113
        $itemsPerPage = $collectionResource->getPerPage();
114
        $isPaginated = (bool) $itemsPerPage;
115
        $dataProviderContext = null === $filters ? [] : ['filters' => $filters];
0 ignored issues
show
introduced by
The condition null === $filters is always false.
Loading history...
116
        if ($isPaginated) {
117
            // perPage is configured in dependency injection for client controlled per page parameter.
118
            // we should really be reading this config parameter into the class in case a user chooses
119
            // a different pagination querystring parameter
120
            $dataProviderContext['filters'] = $dataProviderContext['filters'] ?? [];
121
            $dataProviderContext['filters'] = array_merge($dataProviderContext['filters'], [
122
                'pagination' => true,
123
                'perPage' => $itemsPerPage,
124
                '_page' => 1,
125
            ]);
126
            $request->attributes->set('_api_pagination', [
127
                'pagination' => 'true',
128
                'perPage' => $itemsPerPage,
129
            ]);
130
        }
131
132
        return $dataProviderContext;
133
    }
134
135
    private function getFilters(CollectionResource $object, Request $request): ?array
136
    {
137
        if (null === $filters = $request->attributes->get('_api_filters')) {
138
            $setDefaultQuery = !$request->server->get('QUERY_STRING') && $defaultQueryString = $object->getDefaultQueryParameters();
139
            if ($setDefaultQuery) {
140
                $request->server->set('QUERY_STRING', http_build_query($defaultQueryString));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $defaultQueryString does not seem to be defined for all execution paths leading up to this point.
Loading history...
141
            }
142
            $queryString = RequestParser::getQueryString($request);
143
            $filters = $queryString ? RequestParser::parseRequestParams($queryString) : null;
144
            if ($setDefaultQuery) {
145
                $request->server->set('QUERY_STRING', '');
146
            }
147
        }
148
149
        return $filters;
150
    }
151
152
    private function addEndpoints(Collection $object, string $format): void
153
    {
154
        $resourceMetadata = $this->resourceMetadataFactory->create($object->getResource()->getResourceClass());
155
        $collectionOperations = array_change_key_case($resourceMetadata->getCollectionOperations(), CASE_LOWER);
0 ignored issues
show
Bug introduced by
It seems like $resourceMetadata->getCollectionOperations() can also be of type null; however, parameter $input of array_change_key_case() does only seem to accept array, 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

155
        $collectionOperations = array_change_key_case(/** @scrutinizer ignore-type */ $resourceMetadata->getCollectionOperations(), CASE_LOWER);
Loading history...
156
        if ($collectionOperations && ($shortName = $resourceMetadata->getShortName())) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $collectionOperations of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
157
            $baseRoute = trim($resourceMetadata->getAttribute('route_prefix', ''), ' /');
158
            $methods = array_map(static function ($str) { return strtolower($str); }, [Request::METHOD_GET, Request::METHOD_POST]);
159
            foreach ($methods as $method) {
160
                if (!\array_key_exists($method, $collectionOperations)) {
161
                    continue;
162
                }
163
                $path = $baseRoute .
164
                    $this->operationPathResolver->resolveOperationPath(
165
                        $shortName,
166
                        $collectionOperations[$method],
167
                        OperationType::COLLECTION,
168
                        $method
0 ignored issues
show
Unused Code introduced by
The call to ApiPlatform\Core\PathRes...:resolveOperationPath() has too many arguments starting with $method. ( Ignorable by Annotation )

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

168
                    $this->operationPathResolver->/** @scrutinizer ignore-call */ 
169
                                                  resolveOperationPath(

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...
169
                    );
170
                $finalPath = preg_replace('/{_format}$/', $format, $path);
171
                $object->addEndpoint($method, $finalPath);
172
            }
173
        }
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function supportsTransformation($data, string $to, array $context = []): bool
180
    {
181
        return $data instanceof CollectionResource &&
182
            Collection::class === $to &&
183
            !\in_array($data->getId(), $this->transformed, true);
184
    }
185
}
186