Passed
Pull Request — master (#7)
by Yonel Ceruto
07:59
created

ObjectFieldResolver::isGranted()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
crap 12
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\PropertyAccess\PropertyAccessor;
22
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
23
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
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\Model\ID;
29
use Ynlo\GraphQLBundle\Model\NodeInterface;
30
use Ynlo\GraphQLBundle\Type\Definition\EndpointAwareInterface;
31
use Ynlo\GraphQLBundle\Type\Definition\EndpointAwareTrait;
32
use Ynlo\GraphQLBundle\Type\Types;
33
34
/**
35
 * Default resolver for all object fields
36
 */
37
class ObjectFieldResolver implements ContainerAwareInterface, EndpointAwareInterface
38
{
39
    use ContainerAwareTrait;
40
    use EndpointAwareTrait;
41
42
    /**
43
     * @var int[]
44
     */
45
    private static $concurrentUsages = [];
46
    private static $isGrantedCache = [];
47
48
    protected $definition;
49
    protected $deferredBuffer;
50
    protected $authorizationChecker;
51
52
    public function __construct(ContainerInterface $container, Endpoint $endpoint, FieldsAwareDefinitionInterface $definition, DeferredBuffer $deferredBuffer, AuthorizationCheckerInterface $authorizationChecker)
53
    {
54
        $this->container = $container;
55
        $this->endpoint = $endpoint;
56
        $this->definition = $definition;
57
        $this->deferredBuffer = $deferredBuffer;
58
        $this->authorizationChecker = $authorizationChecker;
59
    }
60
61
    /**
62 22
     * @param mixed       $root
63
     * @param array       $args
64 22
     * @param mixed       $context
65 22
     * @param ResolveInfo $info
66 22
     *
67 22
     * @return mixed|null|string
68 22
     *
69
     * @throws \Exception
70
     */
71
    public function __invoke($root, array $args, $context, ResolveInfo $info)
72
    {
73
        $value = null;
74
        $fieldDefinition = $this->definition->getField($info->fieldName);
75
        $this->verifyConcurrentUsage($fieldDefinition);
76
        $this->denyAccessUnlessGranted($fieldDefinition);
77
78
        //when use external resolver or use a object method with arguments
79
        if (($resolver = $fieldDefinition->getResolver()) || $fieldDefinition->getArguments()) {
80 22
            $queryDefinition = new QueryDefinition();
81
            $queryDefinition->setName($fieldDefinition->getName());
82 22
            $queryDefinition->setType($fieldDefinition->getType());
83 22
            $queryDefinition->setNode($fieldDefinition->getNode());
84 22
            $queryDefinition->setArguments($fieldDefinition->getArguments());
85
            $queryDefinition->setList($fieldDefinition->isList());
86
            $queryDefinition->setRoles($fieldDefinition->getRoles());
87 22
            $queryDefinition->setMetas($fieldDefinition->getMetas());
88 19
89 19
            if ($resolver) {
90 19
                $queryDefinition->setResolver($resolver);
91 19
            } elseif ($fieldDefinition->getOriginType() === \ReflectionMethod::class) {
92 19
                $queryDefinition->setResolver($fieldDefinition->getOriginName());
93 19
            }
94 19
95
            $resolver = new ResolverExecutor($this->container, $this->endpoint, $queryDefinition);
96 19
            $value = $resolver($root, $args, $context, $info);
97
        } else {
98
            $accessor = new PropertyAccessor(true);
99
            $originName = $fieldDefinition->getOriginName() ?: $fieldDefinition->getName();
100
            $value = $accessor->getValue($root, $originName);
101 19
        }
102
103
        if (null !== $value && Types::ID === $fieldDefinition->getType()) {
104 19
            //ID are formed with base64 representation of the Types and real database ID
105 19
            //in order to create a unique and global identifier for each resource
106
            //@see https://facebook.github.io/relay/docs/graphql-object-identification.html
107 22
            if ($value instanceof ID) {
108 22
                $value = (string) $value;
109 22
            } else {
110
                $value = (string) new ID($this->definition->getName(), $value);
111
            }
112 22
        }
113
114
        if ($value instanceof Collection) {
115
            $value = $value->toArray();
116 17
        }
117 2
118
        if ($value instanceof Proxy && $value instanceof NodeInterface && !$value->__isInitialized()) {
119 16
            $this->deferredBuffer->add($value);
120
121
            return new Deferred(
122
                function () use ($value) {
123 22
                    $this->deferredBuffer->loadBuffer();
124 3
125
                    return $this->deferredBuffer->getLoadedEntity($value);
126
                }
127 22
            );
128 3
        }
129
130 3
        return $value;
131 3
    }
132 3
133
    /**
134 3
     * @param FieldDefinition $definition
135 3
     *
136
     * @throws Error
137
     */
138
    private function verifyConcurrentUsage(FieldDefinition $definition)
139 22
    {
140
        if ($maxConcurrentUsage = $definition->getMaxConcurrentUsage()) {
141
            $oid = spl_object_hash($definition);
142
            $usages = static::$concurrentUsages[$oid] ?? 1;
143
            if ($usages > $maxConcurrentUsage) {
144
                if (1 === $maxConcurrentUsage) {
145
                    $error = sprintf(
146
                        'The field "%s" can be fetched only once per query. This field can`t be used in a list.',
147 22
                        $definition->getName()
148
                    );
149 22
                } else {
150 2
                    $error = sprintf(
151 2
                        'The field "%s" can`t be fetched more than %s times per query.',
152 2
                        $definition->getName(),
153 1
                        $maxConcurrentUsage
154 1
                    );
155 1
                }
156 1
                throw new Error($error);
157
            }
158
            static::$concurrentUsages[$oid] = $usages + 1;
159
        }
160
    }
161
162
    /**
163
     * @throws Error
164
     */
165 1
    private function denyAccessUnlessGranted(FieldDefinition $fieldDefinition): void
166
    {
167 1
        if (($roles = $fieldDefinition->getRoles()) && !$this->isGranted($roles, $fieldDefinition)) {
168
            throw new Error(sprintf('Access denied to "%s" field', $fieldDefinition->getName()));
169 22
        }
170
    }
171
172
    private function isGranted(array $roles, $object = null): bool
173
    {
174
        $key = md5(json_encode($roles).'/'.spl_object_hash($object));
175
176
        if (array_key_exists($key, static::$isGrantedCache)) {
177
            return static::$isGrantedCache[$key];
178
        }
179
180
        try {
181
            $isGranted = $this->authorizationChecker->isGranted($roles, $object);
182
        } catch (AuthenticationCredentialsNotFoundException $e) {
183
            $isGranted = false;
184
        }
185
186
        return static::$isGrantedCache[$key] = $isGranted;
187
    }
188
}
189