Completed
Push — master ( e9112a...6cc2de )
by Adrien
02:31
created

Types::getPartialInput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Doctrine;
6
7
use Doctrine\Common\Annotations\AnnotationRegistry;
8
use Doctrine\ORM\EntityManager;
9
use GraphQL\Doctrine\Definition\EntityIDType;
10
use GraphQL\Doctrine\Factory\InputTypeFactory;
11
use GraphQL\Doctrine\Factory\ObjectTypeFactory;
12
use GraphQL\Doctrine\Factory\PartialInputTypeFactory;
13
use GraphQL\Type\Definition\InputObjectType;
14
use GraphQL\Type\Definition\Type;
15
16
/**
17
 * Registry of types to manage all GraphQL types
18
 *
19
 * This is the entry point for the library.
20
 */
21
class Types
22
{
23
    /**
24
     * @var array mapping of type name to type instances
25
     */
26
    private $types = [];
27
28
    /**
29
     * @var ObjectTypeFactory
30
     */
31
    private $objectTypeFactory;
32
33
    /**
34
     * @var InputTypeFactory
35
     */
36
    private $inputTypeFactory;
37
38
    /**
39
     * @var PartialInputTypeFactory
40
     */
41
    private $partialInputTypeFactory;
42
43
    /**
44
     * @var EntityManager
45
     */
46
    private $entityManager;
47
48 21
    public function __construct(EntityManager $entityManager, array $customTypeMapping = [])
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $entityManager. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
49
    {
50 21
        $this->types = $this->getPhpToGraphQLMapping();
51 21
        $this->entityManager = $entityManager;
52 21
        $this->objectTypeFactory = new ObjectTypeFactory($this, $entityManager);
53 21
        $this->inputTypeFactory = new InputTypeFactory($this, $entityManager);
54 21
        $this->partialInputTypeFactory = new PartialInputTypeFactory($this, $entityManager);
55
56 21
        $entityManager->getConfiguration()->newDefaultAnnotationDriver();
57 21
        AnnotationRegistry::registerLoader('class_exists');
58
59 21
        foreach ($customTypeMapping as $phpType => $graphQLType) {
60 21
            $instance = $this->createInstance($graphQLType);
61 21
            $this->registerInstance($phpType, $instance);
62 21
            $this->registerInstance($graphQLType, $instance);
63
        }
64 21
    }
65
66
    /**
67
     * Always return the same instance of `Type` for the given class name
68
     *
69
     * All entity getter methods will be exposed, unless specified otherwise
70
     * with annotations.
71
     *
72
     * @param string $className the class name of either a scalar type (`PostStatus::class`), or an entity (`Post::class`)
73
     *
74
     * @return Type
75
     */
76 19
    public function get(string $className): Type
77
    {
78 19
        $key = $this->normalizedClassName($className);
79
80 19
        if (!isset($this->types[$key])) {
81 14
            $instance = $this->createInstance($className);
82 13
            $this->registerInstance($key, $instance);
83
        }
84
85 18
        return $this->types[$key];
86
    }
87
88
    /**
89
     * Returns an input type for the given entity
90
     *
91
     * This would typically be used in mutations to create new entities.
92
     *
93
     * All entity setter methods will be exposed, unless specified otherwise
94
     * with annotations.
95
     *
96
     * @param string $className the class name of an entity (`Post::class`)
97
     *
98
     * @return InputObjectType
99
     */
100 5 View Code Duplication
    public function getInput(string $className): InputObjectType
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
    {
102 5
        $this->throwIfNotEntity($className);
103 5
        $key = Utils::getInputTypeName($className);
104
105 5
        if (!isset($this->types[$key])) {
106 5
            $instance = $this->inputTypeFactory->create($className);
107 5
            $this->registerInstance($key, $instance);
108
        }
109
110 5
        return $this->types[$key];
111
    }
112
113
    /**
114
     * Returns a partial input type for the given entity
115
     *
116
     * This would typically be used in mutations to update existing entities.
117
     *
118
     * All entity setter methods will be exposed, unless specified otherwise
119
     * with annotations. But they will all be marked as optional and without
120
     * default values. So this allow the API client to specify only some fields
121
     * to be updated, and not necessarily all of them at once.
122
     *
123
     * @param string $className the class name of an entity (`Post::class`)
124
     *
125
     * @return InputObjectType
126
     */
127 1
    public function getPartialInput(string $className): InputObjectType
128
    {
129 1
        $this->throwIfNotEntity($className);
130 1
        $key = Utils::getPartialInputTypeName($className);
131
132 1
        if (!isset($this->types[$key])) {
133 1
            $instance = $this->partialInputTypeFactory->create($className);
134 1
            $this->registerInstance($key, $instance);
135
        }
136
137 1
        return $this->types[$key];
138
    }
139
140
    /**
141
     * Returns an special ID type for the given entity
142
     *
143
     * This is mostly useful for internal usage when a getter has an entity
144
     * as parameter. This type will automatically load the entity from DB, so
145
     * the resolve functions can use a real instance of entity instead of an ID.
146
     * But this can also be used to build your own schema and thus avoid
147
     * manually fetching objects from database for simple cases.
148
     *
149
     * @param string $className the class name of an entity (`Post::class`)
150
     *
151
     * @return EntityIDType
152
     */
153 3 View Code Duplication
    public function getId(string $className): EntityIDType
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
154
    {
155 3
        $this->throwIfNotEntity($className);
156 3
        $key = Utils::getIDTypeName($className);
157
158 3
        if (!isset($this->types[$key])) {
159 3
            $instance = new EntityIDType($this->entityManager, $className);
160 3
            $this->registerInstance($key, $instance);
161
        }
162
163 3
        return $this->types[$key];
164
    }
165
166
    /**
167
     * Register the given type in our internal registry
168
     *
169
     * @param string $key
170
     * @param Type $instance
171
     */
172 21
    private function registerInstance(string $key, Type $instance): void
173
    {
174 21
        $this->types[$key] = $instance;
175 21
        $this->types[$instance->name] = $instance;
176 21
    }
177
178
    /**
179
     * Create an instance of either a custom, scalar or ObjectType
180
     *
181
     * @param string $className
182
     *
183
     * @return Type
184
     */
185 21
    private function createInstance(string $className): Type
186
    {
187 21
        if (is_a($className, Type::class, true)) {
188 21
            return new $className();
189
        }
190
191 12
        $this->throwIfNotEntity($className);
192
193 11
        return $this->objectTypeFactory->create($className);
194
    }
195
196
    /**
197
     * Checks if a className is a valid doctrine entity
198
     *
199
     * @param string $className
200
     *
201
     * @return bool
202
     */
203 16
    public function isEntity(string $className): bool
204
    {
205 16
        return class_exists($className) && !$this->entityManager->getMetadataFactory()->isTransient($className);
206
    }
207
208
    /**
209
     * Returns the list of native GraphQL types
210
     *
211
     * @return array
212
     */
213 21
    private function getPhpToGraphQLMapping(): array
214
    {
215
        return [
216 21
            'id' => Type::id(),
217 21
            'bool' => Type::boolean(),
218 21
            'int' => Type::int(),
219 21
            'float' => Type::float(),
220 21
            'string' => Type::string(),
221
        ];
222
    }
223
224
    /**
225
     * Throw an exception if the class name is not Doctrine entity
226
     *
227
     * @param string $className
228
     *
229
     * @throws \UnexpectedValueException
230
     */
231 16
    private function throwIfNotEntity(string $className): void
232
    {
233 16
        if (!$this->isEntity($className)) {
234 1
            throw new \UnexpectedValueException('Given class name `' . $className . '` is not a Doctrine entity. Either register a custom GraphQL type for `' . $className . '` when instantiating `' . self::class . '`, or change the usage of that class to something else.');
235
        }
236 15
    }
237
238
    /**
239
     * Remove the leading `\` that may exists in FQCN
240
     *
241
     * @param string $className
242
     *
243
     * @return string
244
     */
245 19
    private function normalizedClassName(string $className): string
246
    {
247 19
        return ltrim($className, '\\');
248
    }
249
}
250