Passed
Push — master ( e2a719...9eb666 )
by Alan
03:22
created

CollectionResolverFactory::__invoke()   D

Complexity

Conditions 19
Paths 1

Size

Total Lines 83
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 54
nc 1
nop 3
dl 0
loc 83
rs 4.5166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[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 ApiPlatform\Core\GraphQl\Resolver\Factory;
15
16
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
17
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
18
use ApiPlatform\Core\DataProvider\PaginatorInterface;
19
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
20
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
21
use ApiPlatform\Core\GraphQl\Resolver\FieldsToAttributesTrait;
22
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
23
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
24
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
25
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
26
use GraphQL\Error\Error;
27
use GraphQL\Type\Definition\ResolveInfo;
28
use Symfony\Component\HttpFoundation\RequestStack;
29
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
30
31
/**
32
 * Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation.
33
 *
34
 * @experimental
35
 *
36
 * @author Alan Poulain <[email protected]>
37
 * @author Kévin Dunglas <[email protected]>
38
 */
39
final class CollectionResolverFactory implements ResolverFactoryInterface
40
{
41
    use FieldsToAttributesTrait;
42
    use ResourceAccessCheckerTrait;
0 ignored issues
show
introduced by
The trait ApiPlatform\Core\GraphQl...ourceAccessCheckerTrait requires some properties which are not provided by ApiPlatform\Core\GraphQl...llectionResolverFactory: $fieldNodes, $path
Loading history...
43
44
    private $collectionDataProvider;
45
    private $subresourceDataProvider;
46
    private $normalizer;
47
    private $identifiersExtractor;
48
    private $resourceAccessChecker;
49
    private $requestStack;
50
    private $paginationEnabled;
51
    private $resourceMetadataFactory;
52
53
    public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false)
54
    {
55
        $this->subresourceDataProvider = $subresourceDataProvider;
56
        $this->collectionDataProvider = $collectionDataProvider;
57
        $this->normalizer = $normalizer;
58
        $this->identifiersExtractor = $identifiersExtractor;
59
        $this->resourceAccessChecker = $resourceAccessChecker;
60
        $this->requestStack = $requestStack;
61
        $this->paginationEnabled = $paginationEnabled;
62
        $this->resourceMetadataFactory = $resourceMetadataFactory;
63
    }
64
65
    public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
66
    {
67
        return function ($source, $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operationName) {
68
            if (null === $resourceClass) {
69
                return null;
70
            }
71
72
            if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) {
73
                $request->attributes->set(
74
                    '_graphql_collections_args',
75
                    [$resourceClass => $args] + $request->attributes->get('_graphql_collections_args', [])
76
                );
77
            }
78
79
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
80
            $dataProviderContext = $resourceMetadata->getGraphqlAttribute($operationName ?? 'query', 'normalization_context', [], true);
81
            $dataProviderContext['attributes'] = $this->fieldsToAttributes($info);
82
            $dataProviderContext['filters'] = $this->getNormalizedFilters($args);
83
            $dataProviderContext['graphql'] = true;
84
85
            if (isset($rootClass, $source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_KEY])) {
86
                $rootResolvedFields = $this->identifiersExtractor->getIdentifiersFromItem(unserialize($source[ItemNormalizer::ITEM_KEY]));
87
                $subresource = $this->getSubresource($rootClass, $rootResolvedFields, array_keys($rootResolvedFields), $rootProperty, $resourceClass, true, $dataProviderContext);
0 ignored issues
show
Bug introduced by
It seems like $rootProperty can also be of type null; however, parameter $rootProperty of ApiPlatform\Core\GraphQl...ctory::getSubresource() 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

87
                $subresource = $this->getSubresource($rootClass, $rootResolvedFields, array_keys($rootResolvedFields), /** @scrutinizer ignore-type */ $rootProperty, $resourceClass, true, $dataProviderContext);
Loading history...
88
                $collection = $subresource ?? [];
89
            } else {
90
                $collection = $this->collectionDataProvider->getCollection($resourceClass, null, $dataProviderContext);
91
            }
92
93
            $this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection, $operationName ?? 'query');
94
95
            if (!$this->paginationEnabled) {
96
                $data = [];
97
                foreach ($collection as $index => $object) {
98
                    $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext);
99
                }
100
101
                return $data;
102
            }
103
104
            if (!($collection instanceof PaginatorInterface)) {
105
                throw Error::createLocatedError(sprintf('Collection returned by the collection data provider must implement %s', PaginatorInterface::class), $info->fieldNodes, $info->path);
106
            }
107
108
            $offset = 0;
109
            $totalItems = $collection->getTotalItems();
110
            $nbPageItems = $collection->count();
111
            if (isset($args['after'])) {
112
                $after = base64_decode($args['after'], true);
113
                if (false === $after) {
114
                    throw Error::createLocatedError(sprintf('Cursor %s is invalid', $args['after']), $info->fieldNodes, $info->path);
115
                }
116
                $offset = 1 + (int) $after;
117
            }
118
            if (isset($args['before'])) {
119
                $before = base64_decode($args['before'], true);
120
                if (false === $before) {
121
                    throw Error::createLocatedError(sprintf('Cursor %s is invalid', $args['before']), $info->fieldNodes, $info->path);
122
                }
123
                $offset = (int) $before - $nbPageItems;
124
            }
125
            if (isset($args['last']) && !isset($args['before'])) {
126
                $offset = $totalItems - $args['last'];
127
            }
128
            $offset = 0 > $offset ? 0 : $offset;
129
130
            $data = ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]];
131
132
            if ($totalItems > 0) {
133
                $data['totalCount'] = $totalItems;
134
                $data['pageInfo']['startCursor'] = base64_encode((string) $offset);
135
                $data['pageInfo']['endCursor'] = base64_encode((string) ($offset + $nbPageItems - 1));
136
                $data['pageInfo']['hasNextPage'] = $collection->getCurrentPage() !== $collection->getLastPage() && (float) $nbPageItems === $collection->getItemsPerPage();
137
                $data['pageInfo']['hasPreviousPage'] = $collection->getCurrentPage() > 1 && (float) $nbPageItems === $collection->getItemsPerPage();
138
            }
139
140
            foreach ($collection as $index => $object) {
141
                $data['edges'][$index] = [
142
                    'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $dataProviderContext),
143
                    'cursor' => base64_encode((string) ($index + $offset)),
144
                ];
145
            }
146
147
            return $data;
148
        };
149
    }
150
151
    /**
152
     * @throws ResourceClassNotSupportedException
153
     *
154
     * @return object|null
155
     */
156
    private function getSubresource(string $rootClass, array $rootResolvedFields, array $rootIdentifiers, string $rootProperty, string $subresourceClass, bool $isCollection, array $normalizationContext)
157
    {
158
        $identifiers = [];
159
        $resolvedIdentifiers = [];
160
        foreach ($rootIdentifiers as $rootIdentifier) {
161
            if (isset($rootResolvedFields[$rootIdentifier])) {
162
                $identifiers[$rootIdentifier] = $rootResolvedFields[$rootIdentifier];
163
            }
164
165
            $resolvedIdentifiers[] = [$rootIdentifier, $rootClass];
166
        }
167
168
        return $this->subresourceDataProvider->getSubresource($subresourceClass, $identifiers, $normalizationContext + [
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->subresourc...ion' => $isCollection)) also could return the type array which is incompatible with the documented return type null|object.
Loading history...
169
            'property' => $rootProperty,
170
            'identifiers' => $resolvedIdentifiers,
171
            'collection' => $isCollection,
172
        ]);
173
    }
174
175
    private function getNormalizedFilters(array $args): array
176
    {
177
        $filters = $args;
178
179
        foreach ($filters as $name => $value) {
180
            if (\is_array($value)) {
181
                if (strpos($name, '_list')) {
182
                    $name = substr($name, 0, \strlen($name) - \strlen('_list'));
183
                }
184
                $filters[$name] = $this->getNormalizedFilters($value);
185
            }
186
187
            if (\is_string($name) && strpos($name, '_')) {
188
                // Gives a chance to relations/nested fields.
189
                $filters[str_replace('_', '.', $name)] = $value;
190
            }
191
        }
192
193
        return $filters;
194
    }
195
}
196