Completed
Push — master ( 6c2ef1...d70f56 )
by David
15s queued 10s
created

AnnotationReader::getExtendTypeAnnotation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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
        } catch (ClassNotFoundException $e) {
72
            throw ClassNotFoundException::wrapException($e, $refClass->getName());
73
        }
74
        return $type;
75
    }
76
77
    public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
78
    {
79
        try {
80
            /** @var ExtendType|null $extendType */
81
            $extendType = $this->getClassAnnotation($refClass, ExtendType::class);
82
        } catch (ClassNotFoundException $e) {
83
            throw ClassNotFoundException::wrapExceptionForExtendTag($e, $refClass->getName());
84
        }
85
        return $extendType;
86
    }
87
88
    public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest
89
    {
90
        /** @var AbstractRequest|null $queryAnnotation */
91
        $queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationName);
92
        return $queryAnnotation;
93
    }
94
95
    public function getLoggedAnnotation(ReflectionMethod $refMethod): ?Logged
96
    {
97
        /** @var Logged|null $loggedAnnotation */
98
        $loggedAnnotation = $this->getMethodAnnotation($refMethod, Logged::class);
99
        return $loggedAnnotation;
100
    }
101
102
    public function getRightAnnotation(ReflectionMethod $refMethod): ?Right
103
    {
104
        /** @var Right|null $rightAnnotation */
105
        $rightAnnotation = $this->getMethodAnnotation($refMethod, Right::class);
106
        return $rightAnnotation;
107
    }
108
109
    public function getFailWithAnnotation(ReflectionMethod $refMethod): ?FailWith
110
    {
111
        /** @var FailWith|null $failWithAnnotation */
112
        $failWithAnnotation = $this->getMethodAnnotation($refMethod, FailWith::class);
113
        return $failWithAnnotation;
114
    }
115
116
    /**
117
     * @return SourceField[]
118
     */
119
    public function getSourceFields(ReflectionClass $refClass): array
120
    {
121
        /** @var SourceField[] $sourceFields */
122
        $sourceFields = $this->getClassAnnotations($refClass, SourceField::class);
123
        return $sourceFields;
124
    }
125
126
    public function getFactoryAnnotation(ReflectionMethod $refMethod): ?Factory
127
    {
128
        /** @var Factory|null $factoryAnnotation */
129
        $factoryAnnotation = $this->getMethodAnnotation($refMethod, Factory::class);
130
        return $factoryAnnotation;
131
    }
132
133
    /**
134
     * Returns a class annotation. Finds in the parents if not found in the main class.
135
     *
136
     * @return object|null
137
     */
138
    private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass)
139
    {
140
        do {
141
            $type = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $type is dead and can be removed.
Loading history...
142
            try {
143
                $type = $this->reader->getClassAnnotation($refClass, $annotationClass);
144
            } catch (AnnotationException $e) {
145
                switch ($this->mode) {
146
                    case self::STRICT_MODE:
147
                        throw $e;
148
                    case self::LAX_MODE:
149
                        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

149
                        if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
150
                            throw $e;
151
                        } else {
152
                            return null;
153
                        }
154
                    default:
155
                        throw new \RuntimeException("Unexpected mode '$this->mode'."); // @codeCoverageIgnore
156
                }
157
            }
158
            if ($type !== null) {
159
                return $type;
160
            }
161
            $refClass = $refClass->getParentClass();
162
        } while ($refClass);
163
        return null;
164
    }
165
166
    /**
167
     * Returns a method annotation and handles correctly errors.
168
     *
169
     * @return object|null
170
     */
171
    private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass)
172
    {
173
        try {
174
            return $this->reader->getMethodAnnotation($refMethod, $annotationClass);
175
        } catch (AnnotationException $e) {
176
            switch ($this->mode) {
177
                case self::STRICT_MODE:
178
                    throw $e;
179
                case self::LAX_MODE:
180
                    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

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

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