Skip to content

Commit

Permalink
#[Input] Type Improvements (#458)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
oojacoboo authored Apr 19, 2022
1 parent 4824b96 commit 063b9b5
Show file tree
Hide file tree
Showing 20 changed files with 586 additions and 441 deletions.
347 changes: 175 additions & 172 deletions src/AnnotationReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
use TheCodingMachine\GraphQLite\Annotations\Type;
use TheCodingMachine\GraphQLite\Annotations\TypeInterface;
use Webmozart\Assert\Assert;

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

/**
* Returns a class annotation. Does not look in the parent class.
*
* @param ReflectionClass<object> $refClass
* @param class-string<T> $annotationClass
*
* @return T|null
*
* @throws AnnotationException
* @throws ClassNotFoundException
*
* @template T of object
*/
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass): ?object
{
$type = null;
try {
// If attribute & annotation, let's prefer the PHP 8 attribute
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refClass, 'getAttributes');
$attribute = $refClass->getAttributes($annotationClass)[0] ?? null;
if ($attribute) {
$instance = $attribute->newInstance();
assert($instance instanceof $annotationClass);
return $instance;
}
}

$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
assert($type === null || $type instanceof $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;

case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
throw $e;
} else {
return null;
}
default:
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
}
}

return $type;
}

/**
* Returns a method annotation and handles correctly errors.
*
* @param class-string<object> $annotationClass
*/
private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?object
{
$cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_' . $annotationClass;
if (array_key_exists($cacheKey, $this->methodAnnotationCache)) {
return $this->methodAnnotationCache[$cacheKey];
}

try {
// If attribute & annotation, let's prefer the PHP 8 attribute
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refMethod, 'getAttributes');
$attribute = $refMethod->getAttributes($annotationClass)[0] ?? null;
if ($attribute) {
return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance();
}
}

return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;

case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) {
throw $e;
} else {
return null;
}
default:
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
}
}
}

/**
* Returns true if the annotation class name is part of the docblock comment.
*/
private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
{
foreach ($this->strictNamespaces as $strictNamespace) {
if (strpos($className, $strictNamespace) === 0) {
return true;
}
}
$shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);

return strpos($docComment, '@' . $shortAnnotationClass) !== false;
}

/**
* Returns the class annotations. Finds in the parents too.
*
* @param ReflectionClass<T> $refClass
* @param class-string<A> $annotationClass
*
* @return A[]
*
* @throws AnnotationException
*
* @template T of object
* @template A of object
*/
public function getTypeAnnotation(ReflectionClass $refClass): ?Type
public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array
{
/**
* @var array<array<A>>
*/
$toAddAnnotations = [];
do {
try {
$allAnnotations = $this->reader->getClassAnnotations($refClass);
$toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
return $annotation instanceof $annotationClass;
});
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refClass, 'getAttributes');

/** @var A[] $attributes */
$attributes = array_map(
static function ($attribute) {
return $attribute->newInstance();
},
array_filter($refClass->getAttributes(), static function ($annotation) use ($annotationClass): bool {
return is_a($annotation->getName(), $annotationClass, true);
})
);

$toAddAnnotations[] = $attributes;
}
} catch (AnnotationException $e) {
if ($this->mode === self::STRICT_MODE) {
throw $e;
}

if ($this->mode === self::LAX_MODE) {
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
throw $e;
}
}
}
$refClass = $refClass->getParentClass();
} while ($inherited && $refClass);

if (! empty($toAddAnnotations)) {
return array_merge(...$toAddAnnotations);
}

return [];
}

/**
* @param ReflectionClass<T> $refClass
*
* @template T of object
*/
public function getTypeAnnotation(ReflectionClass $refClass): ?TypeInterface
{
try {
$type = $this->getClassAnnotation($refClass, Type::class);
$type = $this->getClassAnnotation($refClass, Type::class)
?? $this->getClassAnnotation($refClass, Input::class);

if ($type !== null && $type->isSelfType()) {
$type->setClass($refClass->getName());
}
Expand Down Expand Up @@ -151,6 +319,11 @@ public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
return $extendType;
}

public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType
{
return $this->getClassAnnotation($refClass, EnumType::class);
}

/**
* @param class-string<AbstractRequest> $annotationClass
*/
Expand Down Expand Up @@ -293,171 +466,6 @@ public function getMiddlewareAnnotations($reflection): MiddlewareAnnotations
return new MiddlewareAnnotations($middlewareAnnotations);
}

/**
* Returns a class annotation. Does not look in the parent class.
*
* @param ReflectionClass<object> $refClass
* @param class-string<T> $annotationClass
*
* @return T|null
*
* @throws AnnotationException
* @throws ClassNotFoundException
*
* @template T of object
*/
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass): ?object
{
$type = null;
try {
// If attribute & annotation, let's prefer the PHP 8 attribute
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refClass, 'getAttributes');
$attribute = $refClass->getAttributes($annotationClass)[0] ?? null;
if ($attribute) {
$instance = $attribute->newInstance();
assert($instance instanceof $annotationClass);
return $instance;
}
}

$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
assert($type === null || $type instanceof $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;

case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
throw $e;
} else {
return null;
}
default:
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
}
}

return $type;
}

/**
* Returns a method annotation and handles correctly errors.
*
* @param class-string<object> $annotationClass
*/
private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?object
{
$cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_' . $annotationClass;
if (array_key_exists($cacheKey, $this->methodAnnotationCache)) {
return $this->methodAnnotationCache[$cacheKey];
}

try {
// If attribute & annotation, let's prefer the PHP 8 attribute
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refMethod, 'getAttributes');
$attribute = $refMethod->getAttributes($annotationClass)[0] ?? null;
if ($attribute) {
return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance();
}
}

return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;

case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) {
throw $e;
} else {
return null;
}
default:
throw new RuntimeException("Unexpected mode '" . $this->mode . "'."); // @codeCoverageIgnore
}
}
}

/**
* Returns true if the annotation class name is part of the docblock comment.
*/
private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
{
foreach ($this->strictNamespaces as $strictNamespace) {
if (strpos($className, $strictNamespace) === 0) {
return true;
}
}
$shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);

return strpos($docComment, '@' . $shortAnnotationClass) !== false;
}

/**
* Returns the class annotations. Finds in the parents too.
*
* @param ReflectionClass<T> $refClass
* @param class-string<A> $annotationClass
*
* @return A[]
*
* @throws AnnotationException
*
* @template T of object
* @template A of object
*/
public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass, bool $inherited = true): array
{
/**
* @var array<array<A>>
*/
$toAddAnnotations = [];
do {
try {
$allAnnotations = $this->reader->getClassAnnotations($refClass);
$toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool {
return $annotation instanceof $annotationClass;
});
if (PHP_MAJOR_VERSION >= 8) {
Assert::methodExists($refClass, 'getAttributes');

/** @var A[] $attributes */
$attributes = array_map(
static function ($attribute) {
return $attribute->newInstance();
},
array_filter($refClass->getAttributes(), static function ($annotation) use ($annotationClass): bool {
return is_a($annotation->getName(), $annotationClass, true);
})
);

$toAddAnnotations[] = $attributes;
}
} catch (AnnotationException $e) {
if ($this->mode === self::STRICT_MODE) {
throw $e;
}

if ($this->mode === self::LAX_MODE) {
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) {
throw $e;
}
}
}
$refClass = $refClass->getParentClass();
} while ($inherited && $refClass);

if (! empty($toAddAnnotations)) {
return array_merge(...$toAddAnnotations);
}

return [];
}

/**
* Returns the method's annotations.
*
Expand Down Expand Up @@ -566,9 +574,4 @@ static function ($attribute) {

return $toAddAnnotations;
}

public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType
{
return $this->getClassAnnotation($refClass, EnumType::class);
}
}
Loading

0 comments on commit 063b9b5

Please sign in to comment.