Skip to content

Commit 063b9b5

Browse files
authored
#[Input] Type Improvements (#458)
* Fixed incorrect mapping causing input types to be undiscovered during recursive type mapping * Treat "Input" as a "type" for AnnotationReader * Use TypeInterface for return typing * Further interface implementation * Default to true, regardless of name attribute - following @type implementation * Ensure the mapping has the type name as well * CS fixes * Fixed tests * Handle Input type processing in RecursiveTypeMapper * Default value is null * Resolve PHPStan error * Resolved PHPStan errors * Revert default logic to avoid default attribute requirements * CS fixes * Added additional clarity to docs on @input annotation * Removed superfluous annotation name * Remove temporary phpunit group annotation
1 parent 4824b96 commit 063b9b5

20 files changed

+586
-441
lines changed

src/AnnotationReader.php

Lines changed: 175 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
2727
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
2828
use TheCodingMachine\GraphQLite\Annotations\Type;
29+
use TheCodingMachine\GraphQLite\Annotations\TypeInterface;
2930
use Webmozart\Assert\Assert;
3031

3132
use function array_diff_key;
@@ -93,14 +94,181 @@ public function __construct(Reader $reader, string $mode = self::STRICT_MODE, ar
9394
}
9495

9596
/**
97+
* Returns a class annotation. Does not look in the parent class.
98+
*
99+
* @param ReflectionClass<object> $refClass
100+
* @param class-string<T> $annotationClass
101+
*
102+
* @return T|null
103+
*
104+
* @throws AnnotationException
105+
* @throws ClassNotFoundException
106+
*
107+
* @template T of object
108+
*/
109+
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass): ?object
110+
{
111+
$type = null;
112+
try {
113+
// If attribute & annotation, let's prefer the PHP 8 attribute
114+
if (PHP_MAJOR_VERSION >= 8) {
115+
Assert::methodExists($refClass, 'getAttributes');
116+
$attribute = $refClass->getAttributes($annotationClass)[0] ?? null;
117+
if ($attribute) {
118+
$instance = $attribute->newInstance();
119+
assert($instance instanceof $annotationClass);
120+
return $instance;
121+
}
122+
}
123+
124+
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
125+
assert($type === null || $type instanceof $annotationClass);
126+
} catch (AnnotationException $e) {
127+
switch ($this->mode) {
128+
case self::STRICT_MODE:
129+
throw $e;
130+
131+
case self::LAX_MODE:
132+
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
133+
throw $e;
134+
} else {
135+
return null;
136+
}
137+
default:
138+
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
139+
}
140+
}
141+
142+
return $type;
143+
}
144+
145+
/**
146+
* Returns a method annotation and handles correctly errors.
147+
*
148+
* @param class-string<object> $annotationClass
149+
*/
150+
private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?object
151+
{
152+
$cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_' . $annotationClass;
153+
if (array_key_exists($cacheKey, $this->methodAnnotationCache)) {
154+
return $this->methodAnnotationCache[$cacheKey];
155+
}
156+
157+
try {
158+
// If attribute & annotation, let's prefer the PHP 8 attribute
159+
if (PHP_MAJOR_VERSION >= 8) {
160+
Assert::methodExists($refMethod, 'getAttributes');
161+
$attribute = $refMethod->getAttributes($annotationClass)[0] ?? null;
162+
if ($attribute) {
163+
return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance();
164+
}
165+
}
166+
167+
return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
168+
} catch (AnnotationException $e) {
169+
switch ($this->mode) {
170+
case self::STRICT_MODE:
171+
throw $e;
172+
173+
case self::LAX_MODE:
174+
if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) {
175+
throw $e;
176+
} else {
177+
return null;
178+
}
179+
default:
180+
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
181+
}
182+
}
183+
}
184+
185+
/**
186+
* Returns true if the annotation class name is part of the docblock comment.
187+
*/
188+
private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
189+
{
190+
foreach ($this->strictNamespaces as $strictNamespace) {
191+
if (strpos($className, $strictNamespace) === 0) {
192+
return true;
193+
}
194+
}
195+
$shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);
196+
197+
return strpos($docComment, '@' . $shortAnnotationClass) !== false;
198+
}
199+
200+
/**
201+
* Returns the class annotations. Finds in the parents too.
202+
*
96203
* @param ReflectionClass<T> $refClass
204+
* @param class-string<A> $annotationClass
205+
*
206+
* @return A[]
207+
*
208+
* @throws AnnotationException
97209
*
98210
* @template T of object
211+
* @template A of object
99212
*/
100-
public function getTypeAnnotation(ReflectionClass $refClass): ?Type
213+
public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array
214+
{
215+
/**
216+
* @var array<array<A>>
217+
*/
218+
$toAddAnnotations = [];
219+
do {
220+
try {
221+
$allAnnotations = $this->reader->getClassAnnotations($refClass);
222+
$toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
223+
return $annotation instanceof $annotationClass;
224+
});
225+
if (PHP_MAJOR_VERSION >= 8) {
226+
Assert::methodExists($refClass, 'getAttributes');
227+
228+
/** @var A[] $attributes */
229+
$attributes = array_map(
230+
static function ($attribute) {
231+
return $attribute->newInstance();
232+
},
233+
array_filter($refClass->getAttributes(), static function ($annotation) use ($annotationClass): bool {
234+
return is_a($annotation->getName(), $annotationClass, true);
235+
})
236+
);
237+
238+
$toAddAnnotations[] = $attributes;
239+
}
240+
} catch (AnnotationException $e) {
241+
if ($this->mode === self::STRICT_MODE) {
242+
throw $e;
243+
}
244+
245+
if ($this->mode === self::LAX_MODE) {
246+
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
247+
throw $e;
248+
}
249+
}
250+
}
251+
$refClass = $refClass->getParentClass();
252+
} while ($inherited && $refClass);
253+
254+
if (! empty($toAddAnnotations)) {
255+
return array_merge(...$toAddAnnotations);
256+
}
257+
258+
return [];
259+
}
260+
261+
/**
262+
* @param ReflectionClass<T> $refClass
263+
*
264+
* @template T of object
265+
*/
266+
public function getTypeAnnotation(ReflectionClass $refClass): ?TypeInterface
101267
{
102268
try {
103-
$type = $this->getClassAnnotation($refClass, Type::class);
269+
$type = $this->getClassAnnotation($refClass, Type::class)
270+
?? $this->getClassAnnotation($refClass, Input::class);
271+
104272
if ($type !== null && $type->isSelfType()) {
105273
$type->setClass($refClass->getName());
106274
}
@@ -151,6 +319,11 @@ public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
151319
return $extendType;
152320
}
153321

322+
public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType
323+
{
324+
return $this->getClassAnnotation($refClass, EnumType::class);
325+
}
326+
154327
/**
155328
* @param class-string<AbstractRequest> $annotationClass
156329
*/
@@ -293,171 +466,6 @@ public function getMiddlewareAnnotations($reflection): MiddlewareAnnotations
293466
return new MiddlewareAnnotations($middlewareAnnotations);
294467
}
295468

296-
/**
297-
* Returns a class annotation. Does not look in the parent class.
298-
*
299-
* @param ReflectionClass<object> $refClass
300-
* @param class-string<T> $annotationClass
301-
*
302-
* @return T|null
303-
*
304-
* @throws AnnotationException
305-
* @throws ClassNotFoundException
306-
*
307-
* @template T of object
308-
*/
309-
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass): ?object
310-
{
311-
$type = null;
312-
try {
313-
// If attribute & annotation, let's prefer the PHP 8 attribute
314-
if (PHP_MAJOR_VERSION >= 8) {
315-
Assert::methodExists($refClass, 'getAttributes');
316-
$attribute = $refClass->getAttributes($annotationClass)[0] ?? null;
317-
if ($attribute) {
318-
$instance = $attribute->newInstance();
319-
assert($instance instanceof $annotationClass);
320-
return $instance;
321-
}
322-
}
323-
324-
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
325-
assert($type === null || $type instanceof $annotationClass);
326-
} catch (AnnotationException $e) {
327-
switch ($this->mode) {
328-
case self::STRICT_MODE:
329-
throw $e;
330-
331-
case self::LAX_MODE:
332-
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
333-
throw $e;
334-
} else {
335-
return null;
336-
}
337-
default:
338-
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
339-
}
340-
}
341-
342-
return $type;
343-
}
344-
345-
/**
346-
* Returns a method annotation and handles correctly errors.
347-
*
348-
* @param class-string<object> $annotationClass
349-
*/
350-
private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?object
351-
{
352-
$cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_' . $annotationClass;
353-
if (array_key_exists($cacheKey, $this->methodAnnotationCache)) {
354-
return $this->methodAnnotationCache[$cacheKey];
355-
}
356-
357-
try {
358-
// If attribute & annotation, let's prefer the PHP 8 attribute
359-
if (PHP_MAJOR_VERSION >= 8) {
360-
Assert::methodExists($refMethod, 'getAttributes');
361-
$attribute = $refMethod->getAttributes($annotationClass)[0] ?? null;
362-
if ($attribute) {
363-
return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance();
364-
}
365-
}
366-
367-
return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
368-
} catch (AnnotationException $e) {
369-
switch ($this->mode) {
370-
case self::STRICT_MODE:
371-
throw $e;
372-
373-
case self::LAX_MODE:
374-
if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) {
375-
throw $e;
376-
} else {
377-
return null;
378-
}
379-
default:
380-
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
381-
}
382-
}
383-
}
384-
385-
/**
386-
* Returns true if the annotation class name is part of the docblock comment.
387-
*/
388-
private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
389-
{
390-
foreach ($this->strictNamespaces as $strictNamespace) {
391-
if (strpos($className, $strictNamespace) === 0) {
392-
return true;
393-
}
394-
}
395-
$shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);
396-
397-
return strpos($docComment, '@' . $shortAnnotationClass) !== false;
398-
}
399-
400-
/**
401-
* Returns the class annotations. Finds in the parents too.
402-
*
403-
* @param ReflectionClass<T> $refClass
404-
* @param class-string<A> $annotationClass
405-
*
406-
* @return A[]
407-
*
408-
* @throws AnnotationException
409-
*
410-
* @template T of object
411-
* @template A of object
412-
*/
413-
public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array
414-
{
415-
/**
416-
* @var array<array<A>>
417-
*/
418-
$toAddAnnotations = [];
419-
do {
420-
try {
421-
$allAnnotations = $this->reader->getClassAnnotations($refClass);
422-
$toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
423-
return $annotation instanceof $annotationClass;
424-
});
425-
if (PHP_MAJOR_VERSION >= 8) {
426-
Assert::methodExists($refClass, 'getAttributes');
427-
428-
/** @var A[] $attributes */
429-
$attributes = array_map(
430-
static function ($attribute) {
431-
return $attribute->newInstance();
432-
},
433-
array_filter($refClass->getAttributes(), static function ($annotation) use ($annotationClass): bool {
434-
return is_a($annotation->getName(), $annotationClass, true);
435-
})
436-
);
437-
438-
$toAddAnnotations[] = $attributes;
439-
}
440-
} catch (AnnotationException $e) {
441-
if ($this->mode === self::STRICT_MODE) {
442-
throw $e;
443-
}
444-
445-
if ($this->mode === self::LAX_MODE) {
446-
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
447-
throw $e;
448-
}
449-
}
450-
}
451-
$refClass = $refClass->getParentClass();
452-
} while ($inherited && $refClass);
453-
454-
if (! empty($toAddAnnotations)) {
455-
return array_merge(...$toAddAnnotations);
456-
}
457-
458-
return [];
459-
}
460-
461469
/**
462470
* Returns the method's annotations.
463471
*
@@ -566,9 +574,4 @@ static function ($attribute) {
566574

567575
return $toAddAnnotations;
568576
}
569-
570-
public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType
571-
{
572-
return $this->getClassAnnotation($refClass, EnumType::class);
573-
}
574577
}

0 commit comments

Comments
 (0)