Passed
Push — master ( 5cd32c...1a1aa9 )
by Rafael
05:46
created

ObjectFieldResolver::verifyConcurrentUsage()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.3035

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 11
cts 15
cp 0.7332
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 15
nc 4
nop 1
crap 4.3035
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Resolver;
12
13
use Doctrine\Common\Collections\Collection;
14
use Doctrine\Common\Persistence\Proxy;
15
use GraphQL\Deferred;
16
use GraphQL\Error\Error;
17
use GraphQL\Type\Definition\ResolveInfo;
18
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
19
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22
use Symfony\Component\PropertyAccess\PropertyAccessor;
23
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
24
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
25
use Ynlo\GraphQLBundle\Definition\FieldsAwareDefinitionInterface;
26
use Ynlo\GraphQLBundle\Definition\QueryDefinition;
27
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
28
use Ynlo\GraphQLBundle\Events\GraphQLEvents;
29
use Ynlo\GraphQLBundle\Events\GraphQLFieldEvent;
30
use Ynlo\GraphQLBundle\Events\GraphQLFieldInfo;
31
use Ynlo\GraphQLBundle\Model\NodeInterface;
32
use Ynlo\GraphQLBundle\Type\Definition\EndpointAwareInterface;
33
use Ynlo\GraphQLBundle\Type\Definition\EndpointAwareTrait;
34
use Ynlo\GraphQLBundle\Type\Types;
35
use Ynlo\GraphQLBundle\Util\IDEncoder;
36
37
/**
38
 * Default resolver for all object fields
39
 */
40
class ObjectFieldResolver implements ContainerAwareInterface, EndpointAwareInterface
41
{
42
    use ContainerAwareTrait;
43
    use EndpointAwareTrait;
44
45
    /**
46
     * @var int[]
47
     */
48
    private static $concurrentUsages = [];
49
50
    protected $definition;
51
    protected $deferredBuffer;
52
    protected $authorizationChecker;
53
54 22
    public function __construct(ContainerInterface $container, Endpoint $endpoint, FieldsAwareDefinitionInterface $definition, DeferredBuffer $deferredBuffer, AuthorizationCheckerInterface $authorizationChecker)
55
    {
56 22
        $this->container = $container;
57 22
        $this->endpoint = $endpoint;
58 22
        $this->definition = $definition;
59 22
        $this->deferredBuffer = $deferredBuffer;
60 22
        $this->authorizationChecker = $authorizationChecker;
61 22
    }
62
63
    /**
64
     * @param mixed       $root
65
     * @param array       $args
66
     * @param mixed       $context
67
     * @param ResolveInfo $info
68
     *
69
     * @return mixed|null|string
70
     *
71
     * @throws \Exception
72
     */
73 22
    public function __invoke($root, array $args, $context, ResolveInfo $info)
74
    {
75 22
        $value = null;
76 22
        $fieldDefinition = $this->definition->getField($info->fieldName);
77 22
        $eventDispatcher = $this->container->get(EventDispatcherInterface::class);
78
79 22
        $fieldInfo = new GraphQLFieldInfo($this->definition, $fieldDefinition, $info);
80 22
        $event = new GraphQLFieldEvent(
81 22
            $fieldInfo,
82 22
            $root,
83 22
            $args,
84 22
            $context
85
        );
86 22
        $eventDispatcher->dispatch(GraphQLEvents::PRE_READ_FIELD, $event);
87
88 22
        if ($event->isPropagationStopped() || $event->getValue()) {
89
            $eventDispatcher->dispatch(GraphQLEvents::POST_READ_FIELD, $event);
90
91
            return $event->getValue();
92
        }
93
94 22
        $this->verifyConcurrentUsage($fieldDefinition);
95 22
        $this->denyAccessUnlessGranted($fieldDefinition);
96
97
        //when use external resolver or use a object method with arguments
98 22
        if (($resolver = $fieldDefinition->getResolver()) || $fieldDefinition->getArguments()) {
99 19
            $queryDefinition = new QueryDefinition();
100 19
            $queryDefinition->setName($fieldDefinition->getName());
101 19
            $queryDefinition->setType($fieldDefinition->getType());
102 19
            $queryDefinition->setNode($fieldDefinition->getNode());
103 19
            $queryDefinition->setArguments($fieldDefinition->getArguments());
104 19
            $queryDefinition->setList($fieldDefinition->isList());
105 19
            $queryDefinition->setRoles($fieldDefinition->getRoles());
106 19
            $queryDefinition->setMetas($fieldDefinition->getMetas());
107
108 19
            if ($resolver) {
109 19
                $queryDefinition->setResolver($resolver);
110
            } elseif ($fieldDefinition->getOriginType() === \ReflectionMethod::class) {
111
                $queryDefinition->setResolver($fieldDefinition->getOriginName());
112
            }
113
114 19
            $resolver = new ResolverExecutor($this->container, $this->endpoint, $queryDefinition);
115 19
            $value = $resolver($root, $args, $context, $info);
116
        } else {
117 22
            $accessor = new PropertyAccessor(true);
118 22
            $originName = $fieldDefinition->getOriginName() ?: $fieldDefinition->getName();
119 22
            $value = $accessor->getValue($root, $originName);
120
        }
121
122 22
        if (null !== $value && Types::ID === $fieldDefinition->getType() && $root instanceof NodeInterface) {
123
            //ID are formed with base64 representation of the Types and real database ID
124
            //in order to create a unique and global identifier for each resource
125
            //@see https://facebook.github.io/relay/docs/graphql-object-identification.html
126 16
            $value = IDEncoder::encode($root);
127
        }
128
129 22
        if ($value instanceof Collection) {
130 3
            $value = $value->toArray();
131
        }
132
133 22
        if ($value instanceof Proxy && $value instanceof NodeInterface && !$value->__isInitialized()) {
134 6
            $this->deferredBuffer->add($value);
135
136 6
            return new Deferred(
137 6
                function () use ($value) {
138 6
                    $this->deferredBuffer->loadBuffer();
139
140 6
                    return $this->deferredBuffer->getLoadedEntity($value);
141 6
                }
142
            );
143
        }
144
145 22
        $event->setValue($value);
146 22
        $eventDispatcher->dispatch(GraphQLEvents::POST_READ_FIELD, $event);
147
148 22
        return $event->getValue();
149
    }
150
151
    /**
152
     * @param FieldDefinition $definition
153
     *
154
     * @throws Error
155
     */
156 22
    private function verifyConcurrentUsage(FieldDefinition $definition)
157
    {
158 22
        if ($maxConcurrentUsage = $definition->getMaxConcurrentUsage()) {
159 2
            $oid = spl_object_hash($definition);
160 2
            $usages = static::$concurrentUsages[$oid] ?? 1;
0 ignored issues
show
Bug introduced by
Since $concurrentUsages is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $concurrentUsages to at least protected.
Loading history...
161 2
            if ($usages > $maxConcurrentUsage) {
162 1
                if (1 === $maxConcurrentUsage) {
163 1
                    $error = sprintf(
164 1
                        'The field "%s" can be fetched only once per query. This field can`t be used in a list.',
165 1
                        $definition->getName()
166
                    );
167
                } else {
168
                    $error = sprintf(
169
                        'The field "%s" can`t be fetched more than %s times per query.',
170
                        $definition->getName(),
171
                        $maxConcurrentUsage
172
                    );
173
                }
174 1
                throw new Error($error);
175
            }
176 1
            static::$concurrentUsages[$oid] = $usages + 1;
177
        }
178 22
    }
179
180
    /**
181
     * @throws Error
182
     */
183 22
    private function denyAccessUnlessGranted(FieldDefinition $fieldDefinition): void
184
    {
185 22
        if ($fieldDefinition->hasMeta('roles')) {
186
            $roles = $fieldDefinition->getMeta('roles');
187
        } else {
188 22
            $roles = $fieldDefinition->getRoles();
189
        }
190
191 22
        if ($roles && !$this->authorizationChecker->isGranted($roles, $fieldDefinition)) {
192
            throw new Error(sprintf('Access denied to "%s" field', $fieldDefinition->getName()));
193
        }
194 22
    }
195
}
196