Passed
Push — master ( 8bd912...d93388 )
by Alan
06:58 queued 02:20
created

src/GraphQl/Resolver/Stage/ReadStage.php (3 issues)

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\Stage;
15
16
use ApiPlatform\Core\Api\IriConverterInterface;
17
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
18
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
19
use ApiPlatform\Core\Exception\ItemNotFoundException;
20
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
21
use ApiPlatform\Core\GraphQl\Serializer\SerializerContextBuilderInterface;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23
use ApiPlatform\Core\Util\ClassInfoTrait;
24
use GraphQL\Error\Error;
25
use GraphQL\Type\Definition\ResolveInfo;
26
27
/**
28
 * Read stage of GraphQL resolvers.
29
 *
30
 * @experimental
31
 *
32
 * @author Alan Poulain <[email protected]>
33
 */
34
final class ReadStage implements ReadStageInterface
35
{
36
    use ClassInfoTrait;
37
38
    private $resourceMetadataFactory;
39
    private $iriConverter;
40
    private $collectionDataProvider;
41
    private $subresourceDataProvider;
42
    private $serializerContextBuilder;
43
44
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, IriConverterInterface $iriConverter, ContextAwareCollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, SerializerContextBuilderInterface $serializerContextBuilder)
45
    {
46
        $this->resourceMetadataFactory = $resourceMetadataFactory;
47
        $this->iriConverter = $iriConverter;
48
        $this->collectionDataProvider = $collectionDataProvider;
49
        $this->subresourceDataProvider = $subresourceDataProvider;
50
        $this->serializerContextBuilder = $serializerContextBuilder;
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function __invoke(?string $resourceClass, ?string $rootClass, string $operationName, array $context)
57
    {
58
        $resourceMetadata = $resourceClass ? $this->resourceMetadataFactory->create($resourceClass) : null;
59
        if ($resourceMetadata && !$resourceMetadata->getGraphqlAttribute($operationName, 'read', true, true)) {
60
            return $context['is_collection'] ? [] : null;
61
        }
62
63
        $args = $context['args'];
64
        /** @var ResolveInfo $info */
65
        $info = $context['info'];
66
67
        $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operationName, $context, true);
0 ignored issues
show
It seems like $resourceClass can also be of type null; however, parameter $resourceClass of ApiPlatform\Core\GraphQl...lderInterface::create() 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

67
        $normalizationContext = $this->serializerContextBuilder->create(/** @scrutinizer ignore-type */ $resourceClass, $operationName, $context, true);
Loading history...
68
69
        if (!$context['is_collection']) {
70
            $identifier = $this->getIdentifier($context);
71
            $item = $this->getItem($identifier, $normalizationContext);
72
73
            if ($identifier && $context['is_mutation']) {
74
                if (null === $item) {
75
                    throw Error::createLocatedError(sprintf('Item "%s" not found.', $args['input']['id']), $info->fieldNodes, $info->path);
76
                }
77
78
                if ($resourceClass !== $this->getObjectClass($item)) {
79
                    throw Error::createLocatedError(sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $resourceMetadata->getShortName()), $info->fieldNodes, $info->path);
80
                }
81
            }
82
83
            return $item;
84
        }
85
86
        if (null === $rootClass) {
87
            return [];
88
        }
89
90
        $normalizationContext['filters'] = $this->getNormalizedFilters($args);
91
92
        $source = $context['source'];
93
        if (isset($source[$rootProperty = $info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY])) {
94
            $rootResolvedFields = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY];
95
            $subresourceCollection = $this->getSubresource($rootClass, $rootResolvedFields, $rootProperty, $resourceClass, $normalizationContext, $operationName);
0 ignored issues
show
It seems like $resourceClass can also be of type null; however, parameter $subresourceClass of ApiPlatform\Core\GraphQl...Stage::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

95
            $subresourceCollection = $this->getSubresource($rootClass, $rootResolvedFields, $rootProperty, /** @scrutinizer ignore-type */ $resourceClass, $normalizationContext, $operationName);
Loading history...
96
            if (!is_iterable($subresourceCollection)) {
97
                throw new \UnexpectedValueException('Expected subresource collection to be iterable');
98
            }
99
100
            return $subresourceCollection;
101
        }
102
103
        return $this->collectionDataProvider->getCollection($resourceClass, $operationName, $normalizationContext);
0 ignored issues
show
It seems like $resourceClass can also be of type null; however, parameter $resourceClass of ApiPlatform\Core\DataPro...erface::getCollection() 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

103
        return $this->collectionDataProvider->getCollection(/** @scrutinizer ignore-type */ $resourceClass, $operationName, $normalizationContext);
Loading history...
104
    }
105
106
    private function getIdentifier(array $context): ?string
107
    {
108
        $args = $context['args'];
109
110
        if ($context['is_mutation']) {
111
            return $args['input']['id'] ?? null;
112
        }
113
114
        return $args['id'] ?? null;
115
    }
116
117
    /**
118
     * @return object|null
119
     */
120
    private function getItem(?string $identifier, array $normalizationContext)
121
    {
122
        if (null === $identifier) {
123
            return null;
124
        }
125
126
        try {
127
            $item = $this->iriConverter->getItemFromIri($identifier, $normalizationContext);
128
        } catch (ItemNotFoundException $e) {
129
            return null;
130
        }
131
132
        return $item;
133
    }
134
135
    private function getNormalizedFilters(array $args): array
136
    {
137
        $filters = $args;
138
139
        foreach ($filters as $name => $value) {
140
            if (\is_array($value)) {
141
                if (strpos($name, '_list')) {
142
                    $name = substr($name, 0, \strlen($name) - \strlen('_list'));
143
                }
144
                $filters[$name] = $this->getNormalizedFilters($value);
145
            }
146
147
            if (\is_string($name) && strpos($name, '_')) {
148
                // Gives a chance to relations/nested fields.
149
                $filters[str_replace('_', '.', $name)] = $value;
150
            }
151
        }
152
153
        return $filters;
154
    }
155
156
    /**
157
     * @return iterable|object|null
158
     */
159
    private function getSubresource(string $rootClass, array $rootResolvedFields, string $rootProperty, string $subresourceClass, array $normalizationContext, string $operationName)
160
    {
161
        $resolvedIdentifiers = [];
162
        $rootIdentifiers = array_keys($rootResolvedFields);
163
        foreach ($rootIdentifiers as $rootIdentifier) {
164
            $resolvedIdentifiers[] = [$rootIdentifier, $rootClass];
165
        }
166
167
        return $this->subresourceDataProvider->getSubresource($subresourceClass, $rootResolvedFields, $normalizationContext + [
168
            'property' => $rootProperty,
169
            'identifiers' => $resolvedIdentifiers,
170
            'collection' => true,
171
        ], $operationName);
172
    }
173
}
174