Completed
Push — master ( 621a38...6bd0cd )
by David
14s queued 11s
created

AnnotationReader::getClassAnnotation()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 26
c 0
b 0
f 0
rs 8.6666
cc 7
nc 6
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\Factory;
17
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
18
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
19
use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField;
20
use TheCodingMachine\GraphQL\Controllers\Annotations\Type;
21
22
class AnnotationReader
23
{
24
    /**
25
     * @var Reader
26
     */
27
    private $reader;
28
29
    // 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)
30
    const LAX_MODE = 'LAX_MODE';
31
    // In this mode, exceptions will be thrown for any incorrect annotations.
32
    const STRICT_MODE = 'STRICT_MODE';
33
34
    /**
35
     * Classes in those namespaces MUST have valid annotations (otherwise, an error is thrown).
36
     *
37
     * @var string[]
38
     */
39
    private $strictNamespaces;
40
41
    /**
42
     * If true, no exceptions will be thrown for incorrect annotations in code coming from the "vendor/" directory.
43
     *
44
     * @var string
45
     */
46
    private $mode;
47
48
    /**
49
     * AnnotationReader constructor.
50
     * @param Reader $reader
51
     * @param string $mode One of self::LAX_MODE or self::STRICT_MODE
52
     * @param array $strictNamespaces
53
     */
54
    public function __construct(Reader $reader, string $mode = self::STRICT_MODE, array $strictNamespaces = [])
55
    {
56
        $this->reader = $reader;
57
        if (!in_array($mode, [self::LAX_MODE, self::STRICT_MODE], true)) {
58
            throw new \InvalidArgumentException('The mode passed must be one of AnnotationReader::LAX_MODE, AnnotationReader::STRICT_MODE');
59
        }
60
        $this->mode = $mode;
61
        $this->strictNamespaces = $strictNamespaces;
62
    }
63
64
    public function getTypeAnnotation(ReflectionClass $refClass): ?Type
65
    {
66
        // TODO: customize the way errors are handled here!
67
        try {
68
            /** @var Type|null $typeField */
69
            $typeField = $this->getClassAnnotation($refClass, Type::class);
70
        } catch (ClassNotFoundException $e) {
71
            throw ClassNotFoundException::wrapException($e, $refClass->getName());
72
        }
73
        return $typeField;
74
    }
75
76
    public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest
77
    {
78
        /** @var AbstractRequest|null $queryAnnotation */
79
        $queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationName);
80
        return $queryAnnotation;
81
    }
82
83
    public function getLoggedAnnotation(ReflectionMethod $refMethod): ?Logged
84
    {
85
        /** @var Logged|null $loggedAnnotation */
86
        $loggedAnnotation = $this->getMethodAnnotation($refMethod, Logged::class);
87
        return $loggedAnnotation;
88
    }
89
90
    public function getRightAnnotation(ReflectionMethod $refMethod): ?Right
91
    {
92
        /** @var Right|null $rightAnnotation */
93
        $rightAnnotation = $this->getMethodAnnotation($refMethod, Right::class);
94
        return $rightAnnotation;
95
    }
96
97
    /**
98
     * @return SourceField[]
99
     */
100
    public function getSourceFields(ReflectionClass $refClass): array
101
    {
102
        /** @var SourceField[] $sourceFields */
103
        $sourceFields = $this->getClassAnnotations($refClass, SourceField::class);
104
        return $sourceFields;
105
    }
106
107
    public function getFactoryAnnotation(ReflectionMethod $refMethod): ?Factory
108
    {
109
        /** @var Factory|null $factoryAnnotation */
110
        $factoryAnnotation = $this->getMethodAnnotation($refMethod, Factory::class);
111
        return $factoryAnnotation;
112
    }
113
114
    /**
115
     * Returns a class annotation. Finds in the parents if not found in the main class.
116
     *
117
     * @return object|null
118
     */
119
    private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass)
120
    {
121
        do {
122
            $type = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $type is dead and can be removed.
Loading history...
123
            try {
124
                $type = $this->reader->getClassAnnotation($refClass, $annotationClass);
125
            } catch (AnnotationException $e) {
126
                switch ($this->mode) {
127
                    case self::STRICT_MODE:
128
                        throw $e;
129
                    case self::LAX_MODE:
130
                        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

130
                        if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
131
                            throw $e;
132
                        } else {
133
                            return null;
134
                        }
135
                    default:
136
                        throw new \RuntimeException("Unexpected mode '$this->mode'."); // @codeCoverageIgnore
137
                }
138
            }
139
            if ($type !== null) {
140
                return $type;
141
            }
142
            $refClass = $refClass->getParentClass();
143
        } while ($refClass);
144
        return null;
145
    }
146
147
    /**
148
     * Returns a method annotation and handles correctly errors.
149
     *
150
     * @return object|null
151
     */
152
    private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass)
153
    {
154
        try {
155
            return $this->reader->getMethodAnnotation($refMethod, $annotationClass);
156
        } catch (AnnotationException $e) {
157
            switch ($this->mode) {
158
                case self::STRICT_MODE:
159
                    throw $e;
160
                case self::LAX_MODE:
161
                    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

161
                    if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refMethod->getDocComment(), $refMethod->getDeclaringClass()->getName())) {
Loading history...
162
                        throw $e;
163
                    } else {
164
                        return null;
165
                    }
166
                default:
167
                    throw new \RuntimeException("Unexpected mode '$this->mode'."); // @codeCoverageIgnore
168
            }
169
        }
170
    }
171
172
    /**
173
     * Returns true if the annotation class name is part of the docblock comment.
174
     *
175
     */
176
    private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
177
    {
178
        foreach ($this->strictNamespaces as $strictNamespace) {
179
            if (strpos($className, $strictNamespace) === 0) {
180
                return true;
181
            }
182
        }
183
        $shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);
184
        return strpos($docComment, '@'.$shortAnnotationClass) !== false;
185
    }
186
187
    /**
188
     * Returns the class annotations. Finds in the parents too.
189
     *
190
     * @return object[]
191
     */
192
    public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass): array
193
    {
194
        $toAddAnnotations = [];
195
        do {
196
            try {
197
                $allAnnotations = $this->reader->getClassAnnotations($refClass);
198
                $toAddAnnotations[] = \array_filter($allAnnotations, function($annotation) use ($annotationClass): bool {
199
                    return $annotation instanceof $annotationClass;
200
                });
201
            } catch (AnnotationException $e) {
202
                if ($this->mode === self::STRICT_MODE) {
203
                    throw $e;
204
                } elseif ($this->mode === self::LAX_MODE) {
205
                    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

205
                    if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
206
                        throw $e;
207
                    }
208
                }
209
            }
210
            $refClass = $refClass->getParentClass();
211
        } while ($refClass);
212
213
        if (!empty($toAddAnnotations)) {
214
            return array_merge(...$toAddAnnotations);
215
        } else {
216
            return [];
217
        }
218
    }
219
}
220