AnnotationReader::getClassAnnotations()   B
last analyzed

Complexity

Conditions 7
Paths 14

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 25
c 0
b 0
f 0
rs 8.8333
cc 7
nc 14
nop 2
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers;
5
6
7
use Doctrine\Common\Annotations\AnnotationException;
8
use Doctrine\Common\Annotations\Reader;
9
use function in_array;
10
use ReflectionClass;
11
use ReflectionMethod;
12
use function strpos;
13
use function substr;
14
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest;
15
use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException;
16
use TheCodingMachine\GraphQL\Controllers\Annotations\ExtendType;
17
use TheCodingMachine\GraphQL\Controllers\Annotations\Factory;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\FailWith;
19
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
20
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
21
use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField;
22
use TheCodingMachine\GraphQL\Controllers\Annotations\Type;
23
24
class AnnotationReader
25
{
26
    /**
27
     * @var Reader
28
     */
29
    private $reader;
30
31
    // In this mode, no exceptions will be thrown for incorrect annotations (unless the name of the annotation we are looking for is part of the docblock)
32
    const LAX_MODE = 'LAX_MODE';
33
    // In this mode, exceptions will be thrown for any incorrect annotations.
34
    const STRICT_MODE = 'STRICT_MODE';
35
36
    /**
37
     * Classes in those namespaces MUST have valid annotations (otherwise, an error is thrown).
38
     *
39
     * @var string[]
40
     */
41
    private $strictNamespaces;
42
43
    /**
44
     * If true, no exceptions will be thrown for incorrect annotations in code coming from the "vendor/" directory.
45
     *
46
     * @var string
47
     */
48
    private $mode;
49
50
    /**
51
     * AnnotationReader constructor.
52
     * @param Reader $reader
53
     * @param string $mode One of self::LAX_MODE or self::STRICT_MODE
54
     * @param array $strictNamespaces
55
     */
56
    public function __construct(Reader $reader, string $mode = self::STRICT_MODE, array $strictNamespaces = [])
57
    {
58
        $this->reader = $reader;
59
        if (!in_array($mode, [self::LAX_MODE, self::STRICT_MODE], true)) {
60
            throw new \InvalidArgumentException('The mode passed must be one of AnnotationReader::LAX_MODE, AnnotationReader::STRICT_MODE');
61
        }
62
        $this->mode = $mode;
63
        $this->strictNamespaces = $strictNamespaces;
64
    }
65
66
    public function getTypeAnnotation(ReflectionClass $refClass): ?Type
67
    {
68
        try {
69
            /** @var Type|null $type */
70
            $type = $this->getClassAnnotation($refClass, Type::class);
71
            if ($type !== null && $type->isSelfType()) {
72
                $type->setClass($refClass->getName());
73
            }
74
        } catch (ClassNotFoundException $e) {
75
            throw ClassNotFoundException::wrapException($e, $refClass->getName());
76
        }
77
        return $type;
78
    }
79
80
    public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
81
    {
82
        try {
83
            /** @var ExtendType|null $extendType */
84
            $extendType = $this->getClassAnnotation($refClass, ExtendType::class);
85
        } catch (ClassNotFoundException $e) {
86
            throw ClassNotFoundException::wrapExceptionForExtendTag($e, $refClass->getName());
87
        }
88
        return $extendType;
89
    }
90
91
    public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest
92
    {
93
        /** @var AbstractRequest|null $queryAnnotation */
94
        $queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationName);
95
        return $queryAnnotation;
96
    }
97
98
    public function getLoggedAnnotation(ReflectionMethod $refMethod): ?Logged
99
    {
100
        /** @var Logged|null $loggedAnnotation */
101
        $loggedAnnotation = $this->getMethodAnnotation($refMethod, Logged::class);
102
        return $loggedAnnotation;
103
    }
104
105
    public function getRightAnnotation(ReflectionMethod $refMethod): ?Right
106
    {
107
        /** @var Right|null $rightAnnotation */
108
        $rightAnnotation = $this->getMethodAnnotation($refMethod, Right::class);
109
        return $rightAnnotation;
110
    }
111
112
    public function getFailWithAnnotation(ReflectionMethod $refMethod): ?FailWith
113
    {
114
        /** @var FailWith|null $failWithAnnotation */
115
        $failWithAnnotation = $this->getMethodAnnotation($refMethod, FailWith::class);
116
        return $failWithAnnotation;
117
    }
118
119
    /**
120
     * @return SourceField[]
121
     */
122
    public function getSourceFields(ReflectionClass $refClass): array
123
    {
124
        /** @var SourceField[] $sourceFields */
125
        $sourceFields = $this->getClassAnnotations($refClass, SourceField::class);
126
        return $sourceFields;
127
    }
128
129
    public function getFactoryAnnotation(ReflectionMethod $refMethod): ?Factory
130
    {
131
        /** @var Factory|null $factoryAnnotation */
132
        $factoryAnnotation = $this->getMethodAnnotation($refMethod, Factory::class);
133
        return $factoryAnnotation;
134
    }
135
136
    /**
137
     * Returns a class annotation. Finds in the parents if not found in the main class.
138
     *
139
     * @return object|null
140
     */
141
    private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass)
142
    {
143
        do {
144
            $type = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $type is dead and can be removed.
Loading history...
145
            try {
146
                $type = $this->reader->getClassAnnotation($refClass, $annotationClass);
147
            } catch (AnnotationException $e) {
148
                switch ($this->mode) {
149
                    case self::STRICT_MODE:
150
                        throw $e;
151
                    case self::LAX_MODE:
152
                        if ($this->isErrorImportant($annotationClass, $refClass->getDocComment(), $refClass->getName())) {
0 ignored issues
show
Bug introduced by
It seems like $refClass->getDocComment() can also be of type boolean; however, parameter $docComment of TheCodingMachine\GraphQL...der::isErrorImportant() 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

152
                        if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
153
                            throw $e;
154
                        } else {
155
                            return null;
156
                        }
157
                    default:
158
                        throw new \RuntimeException("Unexpected mode '$this->mode'."); // @codeCoverageIgnore
159
                }
160
            }
161
            if ($type !== null) {
162
                return $type;
163
            }
164
            $refClass = $refClass->getParentClass();
165
        } while ($refClass);
166
        return null;
167
    }
168
169
    /**
170
     * Returns a method annotation and handles correctly errors.
171
     *
172
     * @return object|null
173
     */
174
    private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass)
175
    {
176
        try {
177
            return $this->reader->getMethodAnnotation($refMethod, $annotationClass);
178
        } catch (AnnotationException $e) {
179
            switch ($this->mode) {
180
                case self::STRICT_MODE:
181
                    throw $e;
182
                case self::LAX_MODE:
183
                    if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment(), $refMethod->getDeclaringClass()->getName())) {
0 ignored issues
show
Bug introduced by
It seems like $refMethod->getDocComment() can also be of type boolean; however, parameter $docComment of TheCodingMachine\GraphQL...der::isErrorImportant() 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

183
                    if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refMethod->getDocComment(), $refMethod->getDeclaringClass()->getName())) {
Loading history...
184
                        throw $e;
185
                    } else {
186
                        return null;
187
                    }
188
                default:
189
                    throw new \RuntimeException("Unexpected mode '$this->mode'."); // @codeCoverageIgnore
190
            }
191
        }
192
    }
193
194
    /**
195
     * Returns true if the annotation class name is part of the docblock comment.
196
     *
197
     */
198
    private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
199
    {
200
        foreach ($this->strictNamespaces as $strictNamespace) {
201
            if (strpos($className, $strictNamespace) === 0) {
202
                return true;
203
            }
204
        }
205
        $shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);
206
        return strpos($docComment, '@'.$shortAnnotationClass) !== false;
207
    }
208
209
    /**
210
     * Returns the class annotations. Finds in the parents too.
211
     *
212
     * @return object[]
213
     */
214
    public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass): array
215
    {
216
        $toAddAnnotations = [];
217
        do {
218
            try {
219
                $allAnnotations = $this->reader->getClassAnnotations($refClass);
220
                $toAddAnnotations[] = \array_filter($allAnnotations, function($annotation) use ($annotationClass): bool {
221
                    return $annotation instanceof $annotationClass;
222
                });
223
            } catch (AnnotationException $e) {
224
                if ($this->mode === self::STRICT_MODE) {
225
                    throw $e;
226
                } elseif ($this->mode === self::LAX_MODE) {
227
                    if ($this->isErrorImportant($annotationClass, $refClass->getDocComment(), $refClass->getName())) {
0 ignored issues
show
Bug introduced by
It seems like $refClass->getDocComment() can also be of type boolean; however, parameter $docComment of TheCodingMachine\GraphQL...der::isErrorImportant() 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

227
                    if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
228
                        throw $e;
229
                    }
230
                }
231
            }
232
            $refClass = $refClass->getParentClass();
233
        } while ($refClass);
234
235
        if (!empty($toAddAnnotations)) {
236
            return array_merge(...$toAddAnnotations);
237
        } else {
238
            return [];
239
        }
240
    }
241
}
242