Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Completed
Pull Request — master (#750)
by Timur
22:37
created

Hydrator::hydrateValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Overblog\GraphQLBundle\Hydrator;
6
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use Doctrine\Common\Annotations\AnnotationRegistry;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Doctrine\ORM\Mapping as ORM;
11
use Doctrine\ORM\NonUniqueResultException;
12
use Exception;
13
use GraphQL\Type\Definition\InputObjectType;
14
use GraphQL\Type\Definition\ListOfType;
15
use GraphQL\Type\Definition\NonNull;
16
use GraphQL\Type\Definition\ResolveInfo;
17
use GraphQL\Type\Definition\Type;
18
use Overblog\GraphQLBundle\Config\Parser\AnnotationParser;
19
use Overblog\GraphQLBundle\Definition\ArgumentInterface;
20
use Overblog\GraphQLBundle\Hydrator\Annotation\Field;
21
use Overblog\GraphQLBundle\Hydrator\Annotation\Model;
22
use Overblog\GraphQLBundle\Hydrator\Converters\ConverterAnnotationInterface;
23
use Overblog\GraphQLBundle\Hydrator\Converters\Entity;
24
use ReflectionClass;
25
use ReflectionException;
26
use ReflectionProperty;
27
use RuntimeException;
28
use Symfony\Component\DependencyInjection\ServiceLocator;
29
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30
31
class Hydrator
32
{
33
    private const ENTITY_ANNOTATIONS = [
34
        ORM\OneToOne::class,
35
        ORM\OneToMany::class,
36
        ORM\ManyToOne::class,
37
        ORM\ManyToMany::class
38
    ];
39
40
    private static array $annotationCache = [];
41
42
    private AnnotationReader $annotationReader;
43
    private PropertyAccessorInterface $propertyAccessor;
44
    private EntityManagerInterface $em;
45
    private ServiceLocator $converters;
46
    private array $args;
47
48
    public function __construct(
49
        PropertyAccessorInterface $propertyAccessor,
50
        ServiceLocator $converters,
51
        EntityManagerInterface $entityManager
52
    ) {
53
        if (!class_exists(AnnotationReader::class) || !class_exists(AnnotationRegistry::class)) {
54
            throw new RuntimeException('In order to use graphql annotation, you need to require doctrine annotations');
55
        }
56
57
        AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Annotati...istry::registerLoader() has been deprecated: This method is deprecated and will be removed in doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

57
        /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerLoader('class_exists');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
58
59
        $this->annotationReader = new AnnotationReader();
60
        $this->propertyAccessor = $propertyAccessor;
61
        $this->converters = $converters;
62
        $this->em = $entityManager;
63
    }
64
65
    /**
66
     * @throws ORM\MappingException|NonUniqueResultException|ReflectionException|Exception
67
     */
68
    public function hydrate(ArgumentInterface $args, ResolveInfo $info): Models
69
    {
70
        $this->args = $args->getArrayCopy();
71
        $requestedField = $info->parentType->getField($info->fieldName);
72
73
        $models = new Models();
74
75
        foreach ($this->args as $argName => $input) {
76
            /** @var ListOfType|NonNull $argType */
77
            $argType = $requestedField->getArg($argName)->getType();
78
79
            /** @var InputObjectType $inputType */
80
            $inputType = $argType->getOfType();
81
82
            if (!isset($inputType->config['model'])) {
83
                continue;
84
            }
85
86
            $models->models[$argName] = $this->hydrateInputType($inputType, $input);
87
        }
88
89
        return $models;
90
    }
91
92
    /**
93
     * @param mixed $inputValues
94
     *
95
     * @return object
96
     *
97
     * @throws ORM\MappingException|ReflectionException|NonUniqueResultException
98
     */
99
    private function hydrateInputType(InputObjectType $inputType, $inputValues, object $model = null): object
100
    {
101
        if (empty($inputType->config['model'])) {
102
            return $inputValues;
103
        }
104
105
        $modelName = null !== $model ? get_class($model) : $inputType->config['model'];
106
        $modelReflection = new ReflectionClass($modelName);
107
108
        $entityAnnotation = $this->annotationReader->getClassAnnotation($modelReflection, Orm\Entity::class);
109
110
        if (null === $model) {
111
            if (null !== $entityAnnotation) {
112
                $model = $this->getEntityModel($modelName, $inputValues);
113
            } else {
114
                $model = new $modelName();
115
            }
116
        }
117
118
        $annotationMapping = $this->readAnnotationMapping($modelReflection);
119
        $fields = $inputType->getFields();
120
121
        foreach ($inputValues as $fieldName => $fieldValue) {
122
            if (empty($fields[$fieldName])) {
123
                continue;
124
            }
125
126
            $fieldObject = $fields[$fieldName];
127
            $targetField = $annotationMapping[$fieldName] ?? $fieldName;
128
129
            if ($this->propertyAccessor->isWritable($model, $targetField)) {
0 ignored issues
show
Bug introduced by
It seems like $model can also be of type integer and string; however, parameter $objectOrArray of Symfony\Component\Proper...Interface::isWritable() does only seem to accept array|object, 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

129
            if ($this->propertyAccessor->isWritable(/** @scrutinizer ignore-type */ $model, $targetField)) {
Loading history...
130
131
                $resultValue = $this->resolveConverter(
132
                   $modelName,
133
                   $targetField,
134
                   $fieldValue,
135
                   $modelReflection,
136
                   $fieldObject
137
               );
138
139
                $this->propertyAccessor->setValue(
140
                    $model,
0 ignored issues
show
Bug introduced by
It seems like $model can also be of type integer and string; however, parameter $objectOrArray of Symfony\Component\Proper...orInterface::setValue() does only seem to accept array|object, 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

140
                    /** @scrutinizer ignore-type */ $model,
Loading history...
141
                    $targetField,
142
                    $resultValue
143
                );
144
            }
145
        }
146
147
        return $model;
148
    }
149
150
    public function resolveConverter($modelName, $targetField, $fieldValue, $modelReflection, $fieldObject)
151
    {
152
        # 1. Check if converter declared explicitely -----------------------------------------------------------
153
        $converterAnnotation = $this->getPropertyAnnotation($modelName, $targetField, ConverterAnnotationInterface::class);
154
        if (null !== $converterAnnotation) {
155
            $converter = $this->converters->get($converterAnnotation::getConverterClass());
156
            $resultValue = $converter->convert($fieldValue, $converterAnnotation);
157
        }
158
159
        # ------------------------------------------------------------------------------------------------------
160
161
        # 2. Check if an entity converter can be applied automatically (single or collection)
162
        // Check if target property has a type-hint which is en Entity itself
163
        $typeHint = $modelReflection->getProperty($targetField)->getType();
164
        if (null !== $typeHint && class_exists((string) $typeHint)) {
165
            $typeAnno = $this->annotationReader->getClassAnnotation(new ReflectionClass($typeHint), ORM\Entity::class);
166
            if (null !== $typeAnno) {
167
                // use entity converter
168
                $converter = $this->converters->get(Converters\Entity::class);
169
                $a = new Converters\Entity;
170
                $a->value = (string) $typeHint;
171
                $resultValue = $converter->convert($fieldValue, $a);
172
            }
173
        }
174
175
        // Check if target property has a Doctrine annotation declared on it
176
        foreach (self::ENTITY_ANNOTATIONS as $annotationName) {
177
            /** @var ORM\OneToOne|ORM\OneToMany|ORM\ManyToOne|ORM\ManyToMany $a */
178
            $columnAnnotation = $this->getPropertyAnnotation($modelName, $targetField, $annotationName);
179
180
            if (null !== $columnAnnotation) {
181
                if (strpos($columnAnnotation->targetEntity, '\\') === false) {
182
                    // Fix namespace
183
                    $columnAnnotation->targetEntity = $modelReflection->getNamespaceName()."\\$columnAnnotation->targetEntity";
184
                }
185
186
                switch (true) {
187
                    case $columnAnnotation instanceof ORM\OneToOne:
188
                    case $columnAnnotation instanceof ORM\ManyToOne:
189
                        $converter = $this->converters->get($columnAnnotation->targetEntity);
190
                        $entity = new Entity();
191
                        $entity->value = "";
192
                        $entity->isCollection = true;
193
                        $resultValue = $converter->convert($fieldValue, $entity);
194
                        break;
195
196
                    case $columnAnnotation instanceof ORM\OneToMany:
197
                    case $columnAnnotation instanceof ORM\ManyToMany:
198
                        $converter = $this->converters->get($columnAnnotation->targetEntity);
199
                        $entity = new Entity();
200
                        $entity->value = "";
201
                        $entity->isCollection = true;
202
                        $resultValue = $converter->convert($fieldValue, $columnAnnotation);
203
                        break;
204
                }
205
206
                break;
207
            }
208
        }
209
210
        # ------------------------------------------------------------------------------------------------------
211
212
        # 3. Use default converter (single or collection)
213
        if (Type::getNullableType($fieldObject->getType()) instanceof ListOfType) {
214
            $resultValue = $this->hydrateCollectionValue($fieldObject, $fieldValue, $modelName);
215
        } else {
216
            $resultValue = $this->hydrateValue($fieldObject, $fieldValue, $resultValue);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $resultValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
217
        }
218
219
        return $resultValue;
220
    }
221
222
    /**
223
     * Returns property annotation from cache.
224
     *
225
     * @throws ReflectionException
226
     */
227
    private function getPropertyAnnotation(string $className, string $propertyName, string $annotationName): ?object
228
    {
229
        self::$annotationCache[$className][$propertyName] ??= $this->annotationReader->getPropertyAnnotations(new ReflectionProperty($className, $propertyName));
230
231
        foreach (self::$annotationCache[$className][$propertyName] as $annotation) {
232
            if ($annotation instanceof $annotationName) {
233
                return $annotation;
234
            }
235
        }
236
237
        return null;
238
    }
239
240
    /**
241
     * Returns class annotation from cache.
242
     *
243
     * @throws ReflectionException
244
     */
245
    private function getClassAnnotation(string $className, string $annotationName): ?object
246
    {
247
        static $cache = [];
248
249
        return $cache[$className][$annotationName] ??=
250
            $this->annotationReader->getClassAnnotation(new ReflectionClass($className), $annotationName);
251
    }
252
253
    /**
254
     * @param string $modelName
255
     * @param array $inputValues
256
     * @return int|mixed|string|null
257
     *
258
     * @throws ORM\MappingException|NonUniqueResultException|ReflectionException
259
     */
260
    private function getEntityModel(string $modelName, array $inputValues)
261
    {
262
        $idValue = $this->resolveIdValue($modelName, $inputValues);
263
264
        if (null === $idValue) {
265
            return new $modelName();
266
        }
267
268
        // entity
269
        $meta = $this->em->getClassMetadata($modelName);
270
        $entityIdField = $meta->getSingleIdentifierFieldName();
271
272
        $builder = $this->em->createQueryBuilder()
273
            ->select('o')
274
            ->from($modelName, 'o')
275
            ->where("o.$entityIdField = :identifier")
276
            ->setParameter('identifier', $idValue);
277
278
        $result = $builder->getQuery()->getOneOrNullResult();
279
280
        if (null === $result) {
281
            throw new Exception("Couldn't find entity");
282
        }
283
284
        return $result;
285
    }
286
287
    /**
288
     * @return array|mixed|null
289
     * @throws ReflectionException
290
     */
291
    private function resolveIdValue(string $modelName, array $inputValues)
292
    {
293
        $reflectionClass = new ReflectionClass($modelName);
294
        $modelAnnotation = $this->annotationReader->getClassAnnotation($reflectionClass, Model::class);
295
296
        $identifier = $modelAnnotation->identifier ?? 'id';
297
        $path = explode('.', $identifier);
298
299
        // If a path is provided, search the value from top argument down
300
        if (count($path) > 1) {
301
            $temp = &$this->args;
302
            foreach($path as $key) {
303
                $temp = &$temp[$key];
304
            }
305
            return $temp;
306
        }
307
308
        return $inputValues[$identifier] ?? $this->args[$identifier] ?? null;
309
    }
310
311
    /**
312
     * @param $fieldObject
313
     * @param $fieldValue
314
     * @return mixed
315
     * @throws ReflectionException
316
     */
317
    private function hydrateValue($fieldObject, $fieldValue, object $model = null)
318
    {
319
        $field = Type::getNamedType($fieldObject->getType());
320
321
        if ($field instanceof InputObjectType) {
322
            $fieldValue = $this->hydrateInputType($field, $fieldValue, $model);
323
        }
324
325
        return $fieldValue;
326
    }
327
328
    /**
329
     * @param $fieldObject
330
     * @param $fieldValue
331
     * @param string $modelName
332
     * @return array
333
     * @throws ORM\MappingException
334
     * @throws ReflectionException
335
     */
336
    private function hydrateCollectionValue($fieldObject, $fieldValue, string $modelName)
337
    {
338
        $isBuiltInTypes = Type::isBuiltInType(Type::getNamedType($fieldObject->getType()));
0 ignored issues
show
Bug introduced by
It seems like GraphQL\Type\Definition\...fieldObject->getType()) can also be of type null; however, parameter $type of GraphQL\Type\Definition\Type::isBuiltInType() does only seem to accept GraphQL\Type\Definition\Type, 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

338
        $isBuiltInTypes = Type::isBuiltInType(/** @scrutinizer ignore-type */ Type::getNamedType($fieldObject->getType()));
Loading history...
339
340
        if (true === $isBuiltInTypes) {
341
            $meta = $this->em->getClassMetadata($modelName);
342
            $entityIdField = $meta->getSingleIdentifierFieldName();
343
344
            $query = $this->em->createQuery(<<<DQL
345
            SELECT o FROM $modelName o
346
            WHERE o.$entityIdField IN (:ids)
347
            INDEX BY o.$entityIdField
348
            DQL);
349
350
            $query->setParameter('ids', $fieldValue);
351
            $entities = $query->getResult();
352
353
            if (count($entities) !== count($fieldValue)) {
354
                throw new Exception("Couldn't find all entities.");
355
            }
356
        }
357
358
        $result = [];
359
        foreach ($fieldValue as $value) {
360
            $result[] = $this->hydrateValue($fieldObject, $value, $entities[$value] ?? null);
361
        }
362
363
        return $result;
364
    }
365
366
    /**
367
     * @param mixed $value
368
     * @return mixed
369
     * @throws ReflectionException
370
     */
371
    private function convertValue($value, object $model, string $targetName)
0 ignored issues
show
Unused Code introduced by
The method convertValue() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
372
    {
373
        $reflectionClass = new ReflectionClass($model);
374
        $property = $reflectionClass->getProperty($targetName);
375
376
        /** @var ConverterAnnotationInterface $annotation */
377
        $annotation = $this->annotationReader->getPropertyAnnotation($property, ConverterAnnotationInterface::class);
378
379
        if (null !== $annotation) {
380
            $converter = $this->converters->get($annotation::getConverterClass());
381
            return $converter->convert($value, $annotation);
382
        }
383
384
        return $value;
385
    }
386
387
    private function readAnnotationMapping(ReflectionClass $reflectionClass): array
388
    {
389
        $reader = AnnotationParser::getAnnotationReader();
0 ignored issues
show
Bug introduced by
The method getAnnotationReader() does not exist on Overblog\GraphQLBundle\C...Parser\AnnotationParser. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
        /** @scrutinizer ignore-call */ 
390
        $reader = AnnotationParser::getAnnotationReader();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
390
        $properties = $reflectionClass->getProperties();
391
392
        $mapping = [];
393
        foreach ($properties as $property) {
394
            $annotation = $reader->getPropertyAnnotation($property, Field::class);
395
396
            if (isset($annotation->name)) {
397
                $mapping[$annotation->name] = $property->name;
398
            }
399
        }
400
401
        return $mapping;
402
    }
403
}
404