Completed
Push — master ( 0eb5d9...ce204c )
by David
14s
created

AnnotationReader::getFailWithAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

213
                    if ($this->isErrorImportant($annotationClass, /** @scrutinizer ignore-type */ $refClass->getDocComment(), $refClass->getName())) {
Loading history...
214
                        throw $e;
215
                    }
216
                }
217
            }
218
            $refClass = $refClass->getParentClass();
219
        } while ($refClass);
220
221
        if (!empty($toAddAnnotations)) {
222
            return array_merge(...$toAddAnnotations);
223
        } else {
224
            return [];
225
        }
226
    }
227
}
228