diff --git a/composer.json b/composer.json index e482e16693..5f89d4d2be 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "require": { "php": ">=8.1", "ext-json": "*", - "doctrine/annotations": "^1.14 || ^2.0", "composer/package-versions-deprecated": "^1.8", "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "phpdocumentor/type-resolver": "^1.4", diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index a65d15ac10..95efef9421 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -4,14 +4,10 @@ namespace TheCodingMachine\GraphQLite; -use Doctrine\Common\Annotations\AnnotationException; -use Doctrine\Common\Annotations\Reader; -use InvalidArgumentException; use ReflectionClass; use ReflectionMethod; use ReflectionParameter; use ReflectionProperty; -use RuntimeException; use TheCodingMachine\GraphQLite\Annotations\AbstractRequest; use TheCodingMachine\GraphQLite\Annotations\Decorate; use TheCodingMachine\GraphQLite\Annotations\EnumType; @@ -28,32 +24,17 @@ use TheCodingMachine\GraphQLite\Annotations\Type; use TheCodingMachine\GraphQLite\Annotations\TypeInterface; -use function array_diff_key; use function array_filter; use function array_key_exists; use function array_map; use function array_merge; -use function array_values; use function assert; use function count; -use function get_class; -use function in_array; use function is_a; use function reset; -use function str_contains; -use function str_starts_with; -use function strrpos; -use function substr; class AnnotationReader { - // 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) - public const LAX_MODE = 'LAX_MODE'; - - // In this mode, exceptions will be thrown for any incorrect annotations. - public const STRICT_MODE = 'STRICT_MODE'; - - /** @var array */ private array $methodAnnotationCache = []; @@ -63,17 +44,6 @@ class AnnotationReader /** @var array> */ private array $propertyAnnotationsCache = []; - /** - * @param string $mode One of self::LAX_MODE or self::STRICT_MODE. If true, no exceptions will be thrown for incorrect annotations in code coming from the "vendor/" directory. - * @param array $strictNamespaces Classes in those namespaces MUST have valid annotations (otherwise, an error is thrown). - */ - public function __construct(private readonly Reader $reader, private readonly string $mode = self::STRICT_MODE, private readonly array $strictNamespaces = []) - { - if (! in_array($mode, [self::LAX_MODE, self::STRICT_MODE], true)) { - throw new InvalidArgumentException('The mode passed must be one of AnnotationReader::LAX_MODE, AnnotationReader::STRICT_MODE'); - } - } - /** * Returns a class annotation. Does not look in the parent class. * @@ -82,38 +52,25 @@ public function __construct(private readonly Reader $reader, private readonly st * * @return T|null * - * @throws AnnotationException * @throws ClassNotFoundException * * @template T of object */ private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass): object|null { - try { - $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); - return $type; - } catch (AnnotationException $e) { - return match ($this->mode) { - self::STRICT_MODE=> throw $e, - self::LAX_MODE=>$this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName()) ? throw $e : null, - default=>throw new RuntimeException("Unexpected mode '" . $this->mode . "'.") // @codeCoverageIgnore - }; + $attribute = $refClass->getAttributes($annotationClass)[0] ?? null; + if (! $attribute) { + return null; } + $instance = $attribute->newInstance(); + assert($instance instanceof $annotationClass); + return $instance; } /** * Returns a method annotation and handles correctly errors. * * @param class-string $annotationClass - * - * @throws AnnotationException */ private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): object|null { @@ -122,35 +79,15 @@ private function getMethodAnnotation(ReflectionMethod $refMethod, string $annota return $this->methodAnnotationCache[$cacheKey]; } - try { - $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) { - return match ($this->mode) { - self::STRICT_MODE=> throw $e, - self::LAX_MODE=>$this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getName()) ? throw $e : null, - default=>throw new RuntimeException("Unexpected mode '" . $this->mode . "'.") // @codeCoverageIgnore - }; - } - } + $attribute = $refMethod->getAttributes($annotationClass)[0] ?? null; + if (! $attribute) { + $this->methodAnnotationCache[$cacheKey] = null; - /** - * 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 (str_starts_with($className, $strictNamespace)) { - return true; - } + return null; } - $shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1); + $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance(); - return str_contains($docComment, '@' . $shortAnnotationClass); + return $this->methodAnnotationCache[$cacheKey]; } /** @@ -161,8 +98,6 @@ private function isErrorImportant(string $annotationClass, string $docComment, s * * @return A[] * - * @throws AnnotationException - * * @template T of object * @template A of object */ @@ -170,39 +105,22 @@ public function getClassAnnotations(ReflectionClass $refClass, string $annotatio { $toAddAnnotations = []; do { - try { - $allAnnotations = $this->reader->getClassAnnotations($refClass); - $toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { - return $annotation instanceof $annotationClass; - }); - - /** @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); - }), - ); - + /** @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); + }), + ); + if (count($attributes) > 0) { $toAddAnnotations[] = $attributes; - } catch (AnnotationException $e) { - if ($this->mode === self::STRICT_MODE) { - throw $e; - } - - if ( - ($this->mode === self::LAX_MODE) - && $this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName()) - ) { - throw $e; - } } $refClass = $refClass->getParentClass(); } while ($inherited && $refClass); - if (! empty($toAddAnnotations)) { + if (count($toAddAnnotations) > 0) { return array_merge(...$toAddAnnotations); } @@ -212,6 +130,8 @@ static function ($attribute) { /** * @param ReflectionClass $refClass * + * @throws ClassNotFoundException + * * @template T of object */ public function getTypeAnnotation(ReflectionClass $refClass): TypeInterface|null @@ -234,7 +154,7 @@ public function getTypeAnnotation(ReflectionClass $refClass): TypeInterface|null * * @return array * - * @throws AnnotationException + * @throws ClassNotFoundException * * @template T of object */ @@ -256,6 +176,8 @@ public function getInputAnnotations(ReflectionClass $refClass): array /** * @param ReflectionClass $refClass * + * @throws ClassNotFoundException + * * @template T of object */ public function getExtendTypeAnnotation(ReflectionClass $refClass): ExtendType|null @@ -314,34 +236,12 @@ public function getDecorateAnnotation(ReflectionMethod $refMethod): Decorate|nul return $decorateAnnotation; } - /** - * Only used in unit tests/ - * - * @deprecated Use getParameterAnnotationsPerParameter instead - * - * @throws AnnotationException - */ - public function getParameterAnnotations(ReflectionParameter $refParameter): ParameterAnnotations - { - $method = $refParameter->getDeclaringFunction(); - assert($method instanceof ReflectionMethod); - /** @var ParameterAnnotationInterface[] $parameterAnnotations */ - $parameterAnnotations = $this->getMethodAnnotations($method, ParameterAnnotationInterface::class); - $name = $refParameter->getName(); - - $filteredAnnotations = array_values(array_filter($parameterAnnotations, static function (ParameterAnnotationInterface $parameterAnnotation) use ($name) { - return $parameterAnnotation->getTarget() === $name; - })); - - return new ParameterAnnotations($filteredAnnotations); - } - /** * @param ReflectionParameter[] $refParameters * * @return array * - * @throws AnnotationException + * @throws InvalidParameterException */ public function getParameterAnnotationsPerParameter(array $refParameters): array { @@ -353,27 +253,6 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array $method = $firstParam->getDeclaringFunction(); assert($method instanceof ReflectionMethod); - /** @var ParameterAnnotationInterface[] $parameterAnnotations */ - $parameterAnnotations = $this->getMethodAnnotations($method, ParameterAnnotationInterface::class); - - /** @var array> $parameterAnnotationsPerParameter */ - $parameterAnnotationsPerParameter = []; - foreach ($parameterAnnotations as $parameterAnnotation) { - $parameterAnnotationsPerParameter[$parameterAnnotation->getTarget()][] = $parameterAnnotation; - } - - // Let's check that the referenced parameters actually do exist: - $parametersByKey = []; - foreach ($refParameters as $refParameter) { - $parametersByKey[$refParameter->getName()] = true; - } - $diff = array_diff_key($parameterAnnotationsPerParameter, $parametersByKey); - if (count($diff) > 0) { - foreach ($diff as $parameterName => $parameterAnnotations) { - throw InvalidParameterException::parameterNotFound($parameterName, get_class($parameterAnnotations[0]), $method); - } - } - foreach ($refParameters as $refParameter) { $attributes = $refParameter->getAttributes(); $parameterAnnotationsPerParameter[$refParameter->getName()] = [...$parameterAnnotationsPerParameter[$refParameter->getName()] ?? @@ -398,7 +277,6 @@ static function (array $parameterAnnotations): ParameterAnnotations { ); } - /** @throws AnnotationException */ public function getMiddlewareAnnotations(ReflectionMethod|ReflectionProperty $reflection): MiddlewareAnnotations { if ($reflection instanceof ReflectionMethod) { @@ -417,47 +295,31 @@ public function getMiddlewareAnnotations(ReflectionMethod|ReflectionProperty $re * * @return array * - * @throws AnnotationException - * * @template T of object */ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annotationClass): array { $cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_s_' . $annotationClass; if (isset($this->methodAnnotationsCache[$cacheKey])) { - return $this->methodAnnotationsCache[$cacheKey]; - } - - $toAddAnnotations = []; - try { - $allAnnotations = $this->reader->getMethodAnnotations($refMethod); - $toAddAnnotations = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { - return $annotation instanceof $annotationClass; - }); - $attributes = $refMethod->getAttributes(); - $toAddAnnotations = [ - ...$toAddAnnotations, - ...array_map( - static function ($attribute) { - return $attribute->newInstance(); - }, - array_filter($attributes, static function ($annotation) use ($annotationClass): bool { - return is_a($annotation->getName(), $annotationClass, true); - }), - ), - ]; - } catch (AnnotationException $e) { - if ($this->mode === self::STRICT_MODE) { - throw $e; - } + /** @var array $annotations */ + $annotations = $this->methodAnnotationsCache[$cacheKey]; - if ($this->mode === self::LAX_MODE) { - if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) { - throw $e; - } - } + return $annotations; } + $attributes = $refMethod->getAttributes(); + /** @var array $toAddAnnotations */ + $toAddAnnotations = [ + ...array_map( + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation) use ($annotationClass): bool { + return is_a($annotation->getName(), $annotationClass, true); + }), + ), + ]; + $this->methodAnnotationsCache[$cacheKey] = $toAddAnnotations; return $toAddAnnotations; @@ -470,8 +332,6 @@ static function ($attribute) { * * @return array * - * @throws AnnotationException - * * @template T of object */ public function getPropertyAnnotations(ReflectionProperty $refProperty, string $annotationClass): array @@ -484,35 +344,18 @@ public function getPropertyAnnotations(ReflectionProperty $refProperty, string $ return $annotations; } - $toAddAnnotations = []; - try { - $allAnnotations = $this->reader->getPropertyAnnotations($refProperty); - $toAddAnnotations = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { - return $annotation instanceof $annotationClass; - }); - $attributes = $refProperty->getAttributes(); - $toAddAnnotations = [ - ...$toAddAnnotations, - ...array_map( - static function ($attribute) { - return $attribute->newInstance(); - }, - array_filter($attributes, static function ($annotation) use ($annotationClass): bool { - return is_a($annotation->getName(), $annotationClass, true); - }), - ), - ]; - } catch (AnnotationException $e) { - if ($this->mode === self::STRICT_MODE) { - throw $e; - } - - if ($this->mode === self::LAX_MODE) { - if ($this->isErrorImportant($annotationClass, $refProperty->getDocComment() ?: '', $refProperty->getDeclaringClass()->getName())) { - throw $e; - } - } - } + $attributes = $refProperty->getAttributes(); + /** @var array $toAddAnnotations */ + $toAddAnnotations = [ + ...array_map( + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation) use ($annotationClass): bool { + return is_a($annotation->getName(), $annotationClass, true); + }), + ), + ]; $this->propertyAnnotationsCache[$cacheKey] = $toAddAnnotations; diff --git a/src/Annotations/Autowire.php b/src/Annotations/Autowire.php index 5974b96a6d..554b44f4ab 100644 --- a/src/Annotations/Autowire.php +++ b/src/Annotations/Autowire.php @@ -11,33 +11,28 @@ use function ltrim; /** - * Use this annotation to autowire a service from the container into a given parameter of a field/query/mutation. - * - * @Annotation - * @Target({"METHOD", "ANNOTATION"}) - * @Attributes({ - * @Attribute("for", type = "string"), - * @Attribute("identifier", type = "string") - * }) + * Use this attribute to autowire a service from the container into a given parameter of a field/query/mutation. */ #[Attribute(Attribute::TARGET_PARAMETER)] class Autowire implements ParameterAnnotationInterface { - /** @var string|null */ - private $for; - /** @var string|null */ - private $identifier; - - /** @param array|string $identifier */ - public function __construct(array|string $identifier = []) + private string|null $for = null; + private string|null $identifier = null; + + /** @param array|string $params */ + public function __construct( + array|string $params = [], + string|null $for = null, + string|null $identifier = null, + ) { - $values = $identifier; + $values = $params; if (is_string($values)) { $this->identifier = $values; } else { - $this->identifier = $values['identifier'] ?? $values['value'] ?? null; - if (isset($values['for'])) { - $this->for = ltrim($values['for'], '$'); + $this->identifier = $identifier ?? $values['identifier'] ?? $values['value'] ?? null; + if (isset($values['for']) || $for !== null) { + $this->for = ltrim($for ?? $values['for'], '$'); } } } @@ -45,7 +40,7 @@ public function __construct(array|string $identifier = []) public function getTarget(): string { if ($this->for === null) { - throw new BadMethodCallException('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"'); + throw new BadMethodCallException('The #[Autowire] attribute must be passed a target. For instance: "#[Autowire(for: "$myService")]"'); } return $this->for; } diff --git a/src/Annotations/Decorate.php b/src/Annotations/Decorate.php index ce9bc3b30f..a7ed1e36dc 100644 --- a/src/Annotations/Decorate.php +++ b/src/Annotations/Decorate.php @@ -10,20 +10,13 @@ use function is_string; /** - * Methods with this annotation are decorating an input type when the input type is resolved. + * Methods with this attribute are decorating an input type when the input type is resolved. * This is meant to be used only when the input type is provided by a third-party library and you want to modify it. - * - * @Annotation - * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("inputTypeName", type = "string"), - * }) */ #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Decorate { - /** @var string */ - private $inputTypeName; + private string $inputTypeName; /** * @param array|string $inputTypeName @@ -36,7 +29,7 @@ public function __construct(array|string $inputTypeName = []) if (is_string($values)) { $this->inputTypeName = $values; } elseif (! isset($values['value']) && ! isset($values['inputTypeName'])) { - throw new BadMethodCallException('The @Decorate annotation must be passed an input type. For instance: "@Decorate("MyInputType")"'); + throw new BadMethodCallException('The #[Decorate] attribute must be passed an input type. For instance: "#[Decorate("MyInputType")]"'); } else { $this->inputTypeName = $values['value'] ?? $values['inputTypeName']; } diff --git a/src/Annotations/Exceptions/ClassNotFoundException.php b/src/Annotations/Exceptions/ClassNotFoundException.php index 6cd1f9d6e4..aaa472e07a 100644 --- a/src/Annotations/Exceptions/ClassNotFoundException.php +++ b/src/Annotations/Exceptions/ClassNotFoundException.php @@ -17,11 +17,11 @@ public static function couldNotFindClass(string $className): self public static function wrapException(self $e, string $className): self { - return new self($e->getMessage() . " defined in @Type annotation of class '" . $className . "'"); + return new self($e->getMessage() . " defined in #[Type] attribute of class '" . $className . "'"); } public static function wrapExceptionForExtendTag(self $e, string $className): self { - return new self($e->getMessage() . " defined in @ExtendType annotation of class '" . $className . "'"); + return new self($e->getMessage() . " defined in #[ExtendType] attribute of class '" . $className . "'"); } } diff --git a/src/Annotations/Exceptions/InvalidParameterException.php b/src/Annotations/Exceptions/InvalidParameterException.php index 91d31a7ad0..86738a7ec3 100644 --- a/src/Annotations/Exceptions/InvalidParameterException.php +++ b/src/Annotations/Exceptions/InvalidParameterException.php @@ -11,13 +11,8 @@ class InvalidParameterException extends BadMethodCallException { - public static function parameterNotFound(string $parameter, string $annotationClass, ReflectionMethod $reflectionMethod): self - { - return new self(sprintf('Parameter "%s" declared in annotation "%s" of method "%s::%s()" does not exist.', $parameter, $annotationClass, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName())); - } - public static function parameterNotFoundFromSourceField(string $parameter, string $annotationClass, ReflectionMethod $reflectionMethod): self { - return new self(sprintf('Could not find parameter "%s" declared in annotation "%s". This annotation is itself declared in a SourceField annotation targeting resolver "%s::%s()".', $parameter, $annotationClass, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName())); + return new self(sprintf('Could not find parameter "%s" declared in annotation "%s". This annotation is itself declared in a SourceField attribute targeting resolver "%s::%s()".', $parameter, $annotationClass, $reflectionMethod->getDeclaringClass()->getName(), $reflectionMethod->getName())); } } diff --git a/src/Annotations/ExtendType.php b/src/Annotations/ExtendType.php index 40592090fd..609c141138 100644 --- a/src/Annotations/ExtendType.php +++ b/src/Annotations/ExtendType.php @@ -13,14 +13,7 @@ use function ltrim; /** - * The ExtendType annotation must be put in a GraphQL type class docblock and is used to add additional fields to the underlying PHP class. - * - * @Annotation - * @Target({"CLASS"}) - * @Attributes({ - * @Attribute("class", type = "string"), - * @Attribute("name", type = "string"), - * }) + * The ExtendType attribute must be put in a GraphQL type class docblock and is used to add additional fields to the underlying PHP class. */ #[Attribute(Attribute::TARGET_CLASS)] class ExtendType @@ -41,7 +34,7 @@ public function __construct(array $attributes = [], string|null $class = null, s $this->name = $name ?? $attributes['name'] ?? null; $this->class = $className; if (! $this->class && ! $this->name) { - throw new BadMethodCallException('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".'); + throw new BadMethodCallException('In attribute #[ExtendType], missing one of the compulsory parameter "class" or "name".'); } } diff --git a/src/Annotations/Factory.php b/src/Annotations/Factory.php index 457eb087ed..9d617e4744 100644 --- a/src/Annotations/Factory.php +++ b/src/Annotations/Factory.php @@ -9,13 +9,6 @@ /** * Factories are methods used to declare GraphQL input types. - * - * @Annotation - * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * @Attribute("default", type = "bool") - * }) */ #[Attribute(Attribute::TARGET_METHOD)] class Factory @@ -33,7 +26,7 @@ public function __construct(array $attributes = [], string|null $name = null, bo $this->default = $default ?? $attributes['default'] ?? ! isset($attributes['name']); if ($this->name === null && $this->default === false) { - throw new GraphQLRuntimeException('A @Factory that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).'); + throw new GraphQLRuntimeException('A #[Factory] that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).'); } } diff --git a/src/Annotations/FailWith.php b/src/Annotations/FailWith.php index 7e153a5e50..9e87eb0d80 100644 --- a/src/Annotations/FailWith.php +++ b/src/Annotations/FailWith.php @@ -10,14 +10,6 @@ use function array_key_exists; use function is_array; -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - * @Attributes({ - * @Attribute("value", type = "mixed"), - * @Attribute("mode", type = "string") - * }) - */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class FailWith implements MiddlewareAnnotationInterface { @@ -35,8 +27,10 @@ public function __construct(mixed $values = [], mixed $value = '__fail__with__ma $this->value = $value; } elseif (is_array($values) && array_key_exists('value', $values)) { $this->value = $values['value']; + } elseif (! is_array($values)) { + $this->value = $values; } else { - throw new BadMethodCallException('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); + throw new BadMethodCallException('The #[FailWith] attribute must be passed a defaultValue. For instance: "#[FailWith(null)]"'); } } diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 8e4112c535..12712bd6e8 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -10,18 +10,6 @@ use const E_USER_DEPRECATED; -/** - * @Annotation - * @Target({"PROPERTY", "METHOD"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * @Attribute("outputType", type = "string"), - * @Attribute("prefetchMethod", type = "string"), - * @Attribute("for", type = "string[]"), - * @Attribute("description", type = "string"), - * @Attribute("inputType", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Field extends AbstractRequest { diff --git a/src/Annotations/HideIfUnauthorized.php b/src/Annotations/HideIfUnauthorized.php index 98727c7655..505ab18262 100644 --- a/src/Annotations/HideIfUnauthorized.php +++ b/src/Annotations/HideIfUnauthorized.php @@ -7,11 +7,8 @@ use Attribute; /** - * Fields/Queries/Mutations annotated with this annotation will be hidden from the schema if the user is not logged + * Fields/Queries/Mutations annotated with this attribute will be hidden from the schema if the user is not logged * or has no right associated. - * - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class HideIfUnauthorized implements MiddlewareAnnotationInterface diff --git a/src/Annotations/HideParameter.php b/src/Annotations/HideParameter.php index 2c14fe4f7a..33c1328b8e 100644 --- a/src/Annotations/HideParameter.php +++ b/src/Annotations/HideParameter.php @@ -10,35 +10,28 @@ use function ltrim; /** - * Use this annotation to tell GraphQLite to ignore a parameter and not expose it as an input parameter. + * Use this attribute to tell GraphQLite to ignore a parameter and not expose it as an input parameter. * This is only possible if the parameter has a default value. - * - * @Annotation - * @Target({"METHOD", "ANNOTATION"}) - * @Attributes({ - * @Attribute("for", type = "string") - * }) */ #[Attribute(Attribute::TARGET_PARAMETER)] class HideParameter implements ParameterAnnotationInterface { - /** @var string */ - private $for; + private string|null $for = null; /** @param array $values */ - public function __construct(array $values = []) + public function __construct(array $values = [], string|null $for = null) { - if (! isset($values['for'])) { + if (! isset($values['for']) && $for === null) { return; } - $this->for = ltrim($values['for'], '$'); + $this->for = ltrim($for ?? $values['for'], '$'); } public function getTarget(): string { if ($this->for === null) { - throw new BadMethodCallException('The @HideParameter annotation must be passed a target. For instance: "@HideParameter(for="$myParameterToHide")"'); + throw new BadMethodCallException('The #[HideParameter] attribute must be passed a target. For instance: "#[HideParameter(for: "$myParameterToHide")]"'); } return $this->for; } diff --git a/src/Annotations/InjectUser.php b/src/Annotations/InjectUser.php index 7d050b3c52..bcdde944f7 100644 --- a/src/Annotations/InjectUser.php +++ b/src/Annotations/InjectUser.php @@ -10,20 +10,13 @@ use function ltrim; /** - * Use this annotation to tell GraphQLite to inject the current logged user as an input parameter. + * Use this attribute to tell GraphQLite to inject the current logged user as an input parameter. * If the parameter is not nullable, the user MUST be logged to access the resource. - * - * @Annotation - * @Target({"METHOD", "ANNOTATION"}) - * @Attributes({ - * @Attribute("for", type = "string") - * }) */ #[Attribute(Attribute::TARGET_PARAMETER)] class InjectUser implements ParameterAnnotationInterface { - /** @var string */ - private $for; + private string|null $for = null; /** @param array $values */ public function __construct(array $values = []) @@ -38,7 +31,7 @@ public function __construct(array $values = []) public function getTarget(): string { if ($this->for === null) { - throw new BadMethodCallException('The @InjectUser annotation must be passed a target. For instance: "@InjectUser(for="$user")"'); + throw new BadMethodCallException('The #[InjectUser] attribute must be passed a target. For instance: "#[InjectUser(for: "$user")]"'); } return $this->for; } diff --git a/src/Annotations/Input.php b/src/Annotations/Input.php index abe22a97d7..31066500a6 100644 --- a/src/Annotations/Input.php +++ b/src/Annotations/Input.php @@ -8,17 +8,8 @@ use RuntimeException; /** - * The Input annotation must be put in a GraphQL input type class docblock and is used to map to the underlying PHP class + * The Input attribute must be put in a GraphQL input type class docblock and is used to map to the underlying PHP class * this is exposed via this input type. - * - * @Annotation - * @Target({"CLASS"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * @Attribute("default", type = "bool"), - * @Attribute("description", type = "string"), - * @Attribute("update", type = "bool"), - * }) */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class Input implements TypeInterface @@ -56,16 +47,16 @@ public function __construct( public function getClass(): string { if ($this->class === null) { - throw new RuntimeException('Empty class for @Input annotation. You MUST create the Input annotation object using the GraphQLite AnnotationReader'); + throw new RuntimeException('Empty class for #[Input] attribute. You MUST create the Input attribute object using the GraphQLite AnnotationReader'); } return $this->class; } - /** @param class-string $class */ - public function setClass(string $class): void + /** @param class-string $className */ + public function setClass(string $className): void { - $this->class = $class; + $this->class = $className; } /** @@ -103,7 +94,7 @@ public function isUpdate(): bool /** * By default there isn't support for defining the type outside - * This is used by the @Type annotation with the "external" attribute. + * This is used by the #[Type] attribute with the "external" attribute. */ public function isSelfType(): bool { diff --git a/src/Annotations/Logged.php b/src/Annotations/Logged.php index e802dcd29d..b71400a8b7 100644 --- a/src/Annotations/Logged.php +++ b/src/Annotations/Logged.php @@ -6,10 +6,6 @@ use Attribute; -/** - * @Annotation - * @Target({"PROPERTY", "METHOD", "ANNOTATION"}) - */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)] class Logged implements MiddlewareAnnotationInterface { diff --git a/src/Annotations/MagicField.php b/src/Annotations/MagicField.php index da0aff9979..650143ff2e 100644 --- a/src/Annotations/MagicField.php +++ b/src/Annotations/MagicField.php @@ -12,15 +12,6 @@ /** * SourceFields are fields that are directly source from the base object into GraphQL. - * - * @Annotation - * @Target({"CLASS"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * @Attribute("outputType", type = "string"), - * @Attribute("phpType", type = "string"), - * @Attribute("annotations", type = "mixed"), - * }) */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class MagicField implements SourceFieldInterface @@ -59,10 +50,10 @@ public function __construct(array $attributes = [], string|null $name = null, st $this->sourceName = $attributes['sourceName'] ?? $sourceName ?? null; if (! $this->name || (! $this->outputType && ! $this->phpType)) { - throw new BadMethodCallException('The @MagicField annotation must be passed a name and an output type or a php type. For instance: "@MagicField(name=\'phone\', outputType=\'String!\')" or "@MagicField(name=\'phone\', phpType=\'string\')"'); + throw new BadMethodCallException('The #[MagicField] attribute must be passed a name and an output type or a php type. For instance: "#[MagicField(name: \'phone\', outputType: \'String!\')]" or "#[MagicField(name: \'phone\', phpType: \'string\')]"'); } if (isset($this->outputType) && $this->phpType) { - throw new BadMethodCallException('In a @MagicField annotation, you cannot use the outputType and the phpType at the same time. For instance: "@MagicField(name=\'phone\', outputType=\'String!\')" or "@MagicField(name=\'phone\', phpType=\'string\')"'); + throw new BadMethodCallException('In a #[MagicField] attribute, you cannot use the outputType and the phpType at the same time. For instance: "#[MagicField(name: \'phone\', outputType: \'String!\')]" or "#[MagicField(name: \'phone\', phpType: \'string\')]"'); } $middlewareAnnotations = []; $parameterAnnotations = []; @@ -76,7 +67,7 @@ public function __construct(array $attributes = [], string|null $name = null, st } elseif ($annotation instanceof ParameterAnnotationInterface) { $parameterAnnotations[$annotation->getTarget()][] = $annotation; } else { - throw new BadMethodCallException('The @MagicField annotation\'s "annotations" attribute must be passed an array of annotations implementing either MiddlewareAnnotationInterface or ParameterAnnotationInterface."'); + throw new BadMethodCallException('The #[MagicField] attribute\'s "annotations" attribute must be passed an array of annotations implementing either MiddlewareAnnotationInterface or ParameterAnnotationInterface."'); } } $this->middlewareAnnotations = new MiddlewareAnnotations($middlewareAnnotations); diff --git a/src/Annotations/Mutation.php b/src/Annotations/Mutation.php index 060ee146b2..39db2aa594 100644 --- a/src/Annotations/Mutation.php +++ b/src/Annotations/Mutation.php @@ -6,13 +6,6 @@ use Attribute; -/** - * @Annotation - * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("outputType", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_METHOD)] class Mutation extends AbstractRequest { diff --git a/src/Annotations/Query.php b/src/Annotations/Query.php index 99ac7a3a5d..8213ae5e10 100644 --- a/src/Annotations/Query.php +++ b/src/Annotations/Query.php @@ -6,13 +6,6 @@ use Attribute; -/** - * @Annotation - * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("outputType", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_METHOD)] class Query extends AbstractRequest { diff --git a/src/Annotations/Right.php b/src/Annotations/Right.php index b58f8ebac0..d14597a326 100644 --- a/src/Annotations/Right.php +++ b/src/Annotations/Right.php @@ -9,13 +9,6 @@ use function is_string; -/** - * @Annotation - * @Target({"PROPERTY", "ANNOTATION", "METHOD"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Right implements MiddlewareAnnotationInterface { @@ -34,7 +27,7 @@ public function __construct(array|string $name = []) $data = ['name' => $data]; } if (! isset($data['value']) && ! isset($data['name'])) { - throw new BadMethodCallException('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); + throw new BadMethodCallException('The #[Right] attribute must be passed a right name. For instance: "#[Right(\'my_right\')]"'); } $this->name = $data['value'] ?? $data['name']; } diff --git a/src/Annotations/Security.php b/src/Annotations/Security.php index 22381f3312..c41c396f60 100644 --- a/src/Annotations/Security.php +++ b/src/Annotations/Security.php @@ -14,16 +14,6 @@ use function is_string; use function sprintf; -/** - * @Annotation - * @Target({"PROPERTY", "ANNOTATION", "METHOD"}) - * @Attributes({ - * @Attribute("expression", type = "string"), - * @Attribute("failWith", type = "mixed"), - * @Attribute("statusCode", type = "int"), - * @Attribute("message", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Security implements MiddlewareAnnotationInterface { @@ -53,7 +43,7 @@ public function __construct(array|string $data = [], string|null $expression = n $this->expression = $data['value'] ?? $data['expression'] ?? $expression; if (! $this->expression) { - throw new BadMethodCallException('The @Security annotation must be passed an expression. For instance: "@Security("is_granted(\'CAN_EDIT_STUFF\')")"'); + throw new BadMethodCallException('The #[Security] attribute must be passed an expression. For instance: "#[Security("is_granted(\'CAN_EDIT_STUFF\')")]"'); } if (array_key_exists('failWith', $data)) { @@ -66,7 +56,7 @@ public function __construct(array|string $data = [], string|null $expression = n $this->message = $message ?? $data['message'] ?? 'Access denied.'; $this->statusCode = $statusCode ?? $data['statusCode'] ?? 403; if ($this->failWithIsSet === true && (($message || isset($data['message'])) || ($statusCode || isset($data['statusCode'])))) { - throw new BadMethodCallException('A @Security annotation that has "failWith" attribute set cannot have a message or a statusCode attribute.'); + throw new BadMethodCallException('A #[Security] attribute that has "failWith" attribute set cannot have a message or a statusCode attribute.'); } } diff --git a/src/Annotations/SourceField.php b/src/Annotations/SourceField.php index 492101d85d..cc5ceb6ad1 100644 --- a/src/Annotations/SourceField.php +++ b/src/Annotations/SourceField.php @@ -12,15 +12,6 @@ /** * SourceFields are fields that are directly source from the base object into GraphQL. - * - * @Annotation - * @Target({"CLASS"}) - * @Attributes({ - * @Attribute("name", type = "string"), - * @Attribute("outputType", type = "string"), - * @Attribute("phpType", type = "string"), - * @Attribute("annotations", type = "mixed"), - * }) */ #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class SourceField implements SourceFieldInterface @@ -48,7 +39,7 @@ public function __construct(array $attributes = [], string|null $name = null, st { $name = $name ?? $attributes['name'] ?? null; if ($name === null) { - throw new BadMethodCallException('The @SourceField annotation must be passed a name. For instance: "@SourceField(name=\'phone\')"'); + throw new BadMethodCallException('The #[SourceField] attribute must be passed a name. For instance: "#[SourceField(name: \'phone\')]"'); } $this->name = $name; @@ -58,7 +49,7 @@ public function __construct(array $attributes = [], string|null $name = null, st $this->sourceName = $sourceName ?? $attributes['sourceName'] ?? null; if ($this->outputType && $this->phpType) { - throw new BadMethodCallException('In a @SourceField annotation, you cannot use the outputType and the phpType at the same time. For instance: "@SourceField(name=\'phone\', outputType=\'String!\')" or "@SourceField(name=\'phone\', phpType=\'string\')"'); + throw new BadMethodCallException('In a #[SourceField] attribute, you cannot use the outputType and the phpType at the same time. For instance: "#[SourceField(name: \'phone\', outputType: \'String!\')]" or "#[SourceField(name: \'phone\', phpType: \'string\')]"'); } $middlewareAnnotations = []; $parameterAnnotations = []; @@ -72,7 +63,7 @@ public function __construct(array $attributes = [], string|null $name = null, st } elseif ($annotation instanceof ParameterAnnotationInterface) { $parameterAnnotations[$annotation->getTarget()][] = $annotation; } else { - throw new BadMethodCallException('The @SourceField annotation\'s "annotations" attribute must be passed an array of annotations implementing either MiddlewareAnnotationInterface or ParameterAnnotationInterface."'); + throw new BadMethodCallException('The #[SourceField] attribute\'s "annotations" attribute must be passed an array of annotations implementing either MiddlewareAnnotationInterface or ParameterAnnotationInterface."'); } } $this->middlewareAnnotations = new MiddlewareAnnotations($middlewareAnnotations); diff --git a/src/Annotations/Subscription.php b/src/Annotations/Subscription.php index 1d5fce3b15..7da35e9661 100644 --- a/src/Annotations/Subscription.php +++ b/src/Annotations/Subscription.php @@ -6,13 +6,6 @@ use Attribute; -/** - * @Annotation - * @Target({"METHOD"}) - * @Attributes({ - * @Attribute("outputType", type = "string"), - * }) - */ #[Attribute(Attribute::TARGET_METHOD)] class Subscription extends AbstractRequest { diff --git a/src/Annotations/Type.php b/src/Annotations/Type.php index 0505eec76f..f8465782a5 100644 --- a/src/Annotations/Type.php +++ b/src/Annotations/Type.php @@ -14,39 +14,25 @@ use function ltrim; /** - * The Type annotation must be put in a GraphQL type class docblock and is used to map to the underlying PHP class + * The Type attribute must be put in a GraphQL type class attribute and is used to map to the underlying PHP class * this is exposed via this type. - * - * @Annotation - * @Target({"CLASS"}) - * @Attributes({ - * @Attribute("class", type = "string"), - * @Attribute("name", type = "string"), - * @Attribute("default", type = "bool"), - * @Attribute("external", type = "bool"), - * }) */ #[Attribute(Attribute::TARGET_CLASS)] class Type implements TypeInterface { /** @var class-string|null */ - private $class; + private string|null $class = null; - /** @var string|null */ - private $name; + private string|null $name = null; - /** @var bool */ - private $default; + private bool $default = true; /** - * Is the class having the annotation a GraphQL type itself? - * - * @var bool + * Is the class having the attribute a GraphQL type itself? */ - private $selfType = false; + private bool $selfType = false; - /** @var bool */ - private $useEnumValues = false; + private bool $useEnumValues = false; /** * @param mixed[] $attributes @@ -89,27 +75,27 @@ public function __construct( public function getClass(): string { if ($this->class === null) { - throw new RuntimeException('Empty class for @Type annotation. You MUST create the Type annotation object using the GraphQLite AnnotationReader'); + throw new RuntimeException('Empty class for #[Type] attribute. You MUST create the Type attribute object using the GraphQLite AnnotationReader'); } return $this->class; } - public function setClass(string $class): void + public function setClass(string $className): void { - $class = ltrim($class, '\\'); - $isInterface = interface_exists($class); - if (! class_exists($class) && ! $isInterface) { - throw ClassNotFoundException::couldNotFindClass($class); + $className = ltrim($className, '\\'); + $isInterface = interface_exists($className); + if (! class_exists($className) && ! $isInterface) { + throw ClassNotFoundException::couldNotFindClass($className); } - $this->class = $class; + $this->class = $className; if (! $isInterface) { return; } if ($this->default === false) { - throw new GraphQLRuntimeException('Problem in annotation @Type for interface "' . $class . '": you cannot use the default="false" attribute on interfaces'); + throw new GraphQLRuntimeException('Problem in attribute #[Type] for interface "' . $className . '": you cannot use the default="false" attribute on interfaces'); } } diff --git a/src/Annotations/UseInputType.php b/src/Annotations/UseInputType.php index 248270762b..6463d4ea1a 100644 --- a/src/Annotations/UseInputType.php +++ b/src/Annotations/UseInputType.php @@ -11,36 +11,30 @@ use function ltrim; /** - * Use this annotation to force using a specific input type for an input argument. - * - * @Annotation - * @Target({"METHOD", "ANNOTATION"}) - * @Attributes({ - * @Attribute("for", type = "string"), - * @Attribute("inputType", type = "string"), - * }) + * Use this attribute to force using a specific input type for an input argument. */ #[Attribute(Attribute::TARGET_PARAMETER)] class UseInputType implements ParameterAnnotationInterface { - /** @var string|null */ - private $for; - /** @var string */ - private $inputType; + private string|null $for = null; + private string $inputType; /** * @param array|string $inputType * * @throws BadMethodCallException */ - public function __construct(array|string $inputType = []) + public function __construct(array|string $inputType = [], string|null $for = null) { $values = $inputType; if (is_string($values)) { $values = ['inputType' => $values]; } + if (is_string($for) && $for !== '') { + $values['for'] = $for; + } if (! isset($values['inputType'])) { - throw new BadMethodCallException('The @UseInputType annotation must be passed an input type. For instance: #[UseInputType("MyInputType")]'); + throw new BadMethodCallException('The #[UseInputType] attribute must be passed an input type. For instance: #[UseInputType("MyInputType")]'); } $this->inputType = $values['inputType']; if (! isset($values['for'])) { @@ -53,7 +47,7 @@ public function __construct(array|string $inputType = []) public function getTarget(): string { if ($this->for === null) { - throw new BadMethodCallException('The @UseInputType annotation must be passed a target and an input type. For instance: #[UseInputType("MyInputType")]'); + throw new BadMethodCallException('The #[UseInputType] attribute must be passed a target and an input type. For instance: #[UseInputType("MyInputType")]'); } return $this->for; } diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 2681f740e5..de99011166 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -4,7 +4,6 @@ namespace TheCodingMachine\GraphQLite; -use Doctrine\Common\Annotations\AnnotationException; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\NonNull; @@ -105,6 +104,10 @@ public function __construct( * @return array * * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ public function getQueries(object $controller): array { @@ -115,6 +118,10 @@ public function getQueries(object $controller): array * @return array * * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ public function getMutations(object $controller): array { @@ -125,13 +132,25 @@ public function getMutations(object $controller): array * @return array * * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ public function getSubscriptions(object $controller): array { return $this->getFieldsByAnnotations($controller, Subscription::class, false); } - /** @return array QueryField indexed by name. */ + /** + * @return array QueryField indexed by name. + * + * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue + */ public function getFields(object $controller, string|null $typeName = null): array { $fieldAnnotations = $this->getFieldsByAnnotations($controller, Annotations\Field::class, true, $typeName); @@ -159,8 +178,11 @@ public function getFields(object $controller, string|null $typeName = null): arr * * @return array * - * @throws AnnotationException * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ public function getInputFields(string $className, string $inputName, bool $isUpdate = false): array { @@ -247,6 +269,12 @@ public function getInputFields(string $className, string $inputName, bool $isUpd * @param class-string $className * * @return array QueryField indexed by name. + * + * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ public function getSelfFields(string $className, string|null $typeName = null): array { @@ -259,7 +287,6 @@ public function getSelfFields(string $className, string|null $typeName = null): $refClass = new ReflectionClass($className); - /** @var SourceFieldInterface[] $sourceFields */ $sourceFields = $this->annotationReader->getSourceFields($refClass); $fieldsFromSourceFields = $this->getQueryFieldsFromSourceFields($sourceFields, $refClass); @@ -277,6 +304,8 @@ public function getSelfFields(string $className, string|null $typeName = null): * @param int $skip Skip first N parameters if those are passed in externally * * @return array Returns an array of parameters. + * + * @throws InvalidArgumentException */ public function getParameters(ReflectionMethod $refMethod, int $skip = 0): array { @@ -292,6 +321,8 @@ public function getParameters(ReflectionMethod $refMethod, int $skip = 0): array * @param ReflectionMethod $refMethod A method annotated with a Decorate annotation. * * @return array Returns an array of parameters. + * + * @throws InvalidArgumentException */ public function getParametersForDecorator(ReflectionMethod $refMethod): array { @@ -308,6 +339,10 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array * @return array * * @throws ReflectionException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, string|null $typeName = null): array { @@ -387,7 +422,10 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo * * @return array * - * @throws AnnotationException + * @throws InvalidArgumentException + * @throws CannotMapTypeExceptionInterface + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getFieldsByMethodAnnotations( string|object $controller, @@ -500,7 +538,10 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): FieldDefinition|n * * @return array * - * @throws AnnotationException + * @throws InvalidArgumentException + * @throws CannotMapTypeException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getFieldsByPropertyAnnotations( string|object $controller, @@ -589,9 +630,12 @@ public function handle(QueryFieldDescriptor $fieldDescriptor): FieldDefinition|n * * @return FieldDefinition[] * - * @throws CannotMapTypeException * @throws CannotMapTypeExceptionInterface * @throws ReflectionException + * @throws FieldNotFoundException + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getQueryFieldsFromSourceFields( array $sourceFields, @@ -744,7 +788,11 @@ private function getMagicGetMethodFromSourceClassOrProxy( return $sourceRefClass->getMethod($magicGet); } - /** @param ReflectionClass $refClass */ + /** + * @param ReflectionClass $refClass + * + * @throws CannotMapTypeExceptionInterface + */ private function resolveOutputType( string $outputType, ReflectionClass $refClass, @@ -759,7 +807,11 @@ private function resolveOutputType( } } - /** @param ReflectionClass $refClass */ + /** + * @param ReflectionClass $refClass + * + * @throws CannotMapTypeExceptionInterface + */ private function resolvePhpType( string $phpTypeStr, ReflectionClass $refClass, @@ -781,6 +833,9 @@ private function resolvePhpType( /** * @param ReflectionClass $reflectionClass * + * @throws ReflectionException + * @throws FieldNotFoundException + * * @template T of object */ private function getMethodFromPropertyName( @@ -804,6 +859,8 @@ private function getMethodFromPropertyName( * @param ReflectionParameter[] $refParameters * * @return array + * + * @throws InvalidParameterException */ private function mapParameters( array $refParameters, @@ -814,6 +871,7 @@ private function mapParameters( if (empty($refParameters)) { return []; } + $args = []; $additionalParameterAnnotations = $sourceField?->getParameterAnnotations() ?? []; @@ -933,11 +991,14 @@ private function getPrefetchParameter( * Gets input fields by class method annotations. * * @param class-string $annotationName - * @param array $defaultProperties + * @param array $defaultProperties * * @return array * - * @throws AnnotationException + * @throws CannotMapTypeExceptionInterface + * @throws InvalidArgumentException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getInputFieldsByMethodAnnotations( string|object $controller, @@ -1039,11 +1100,14 @@ public function handle(InputFieldDescriptor $inputFieldDescriptor): InputField|n * Gets input fields by class property annotations. * * @param class-string $annotationName - * @param array $defaultProperties + * @param array $defaultProperties * * @return array * - * @throws AnnotationException + * @throws InvalidArgumentException + * @throws CannotMapTypeException + * + * @phpstan-ignore-next-line - simple-cache < 2.0 issue */ private function getInputFieldsByPropertyAnnotations( string|object $controller, diff --git a/src/Reflection/CachedDocBlockFactory.php b/src/Reflection/CachedDocBlockFactory.php index 70b5c6699c..978e1e344d 100644 --- a/src/Reflection/CachedDocBlockFactory.php +++ b/src/Reflection/CachedDocBlockFactory.php @@ -6,6 +6,7 @@ use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlockFactory; +use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; use Psr\SimpleCache\CacheInterface; @@ -24,7 +25,7 @@ */ class CachedDocBlockFactory { - private DocBlockFactory $docBlockFactory; + private DocBlockFactoryInterface $docBlockFactory; /** @var array */ private array $docBlockArrayCache = []; /** @var array */ @@ -32,7 +33,7 @@ class CachedDocBlockFactory private ContextFactory $contextFactory; /** @param CacheInterface $cache The cache we fetch data from. Note this is a SAFE cache. It does not need to be purged. */ - public function __construct(private readonly CacheInterface $cache, DocBlockFactory|null $docBlockFactory = null) + public function __construct(private readonly CacheInterface $cache, DocBlockFactoryInterface|null $docBlockFactory = null) { $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); $this->contextFactory = new ContextFactory(); diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 04e4bfa6ba..a4c2bd2c6d 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -4,15 +4,11 @@ namespace TheCodingMachine\GraphQLite; -use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; -use Doctrine\Common\Annotations\PsrCachedReader; -use Doctrine\Common\Annotations\Reader; use GraphQL\Type\SchemaConfig; use Kcs\ClassFinder\Finder\ComposerFinder; use Kcs\ClassFinder\Finder\FinderInterface; use MyCLabs\Enum\Enum; use PackageVersions\Versions; -use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\Adapter\Psr16Adapter; @@ -100,8 +96,6 @@ class SchemaFactory /** @var ParameterMiddlewareInterface[] */ private array $parameterMiddlewares = []; - private Reader|null $doctrineAnnotationReader = null; - private AuthenticationServiceInterface|null $authenticationService = null; private AuthorizationServiceInterface|null $authorizationService = null; @@ -126,7 +120,7 @@ class SchemaFactory private string $cacheNamespace; - public function __construct(private CacheInterface $cache, private ContainerInterface $container) + public function __construct(private readonly CacheInterface $cache, private readonly ContainerInterface $container) { $this->cacheNamespace = substr(md5(Versions::getVersion('thecodingmachine/graphqlite')), 0, 8); } @@ -211,23 +205,6 @@ public function addParameterMiddleware(ParameterMiddlewareInterface $parameterMi return $this; } - /** @deprecated Use PHP8 Attributes instead */ - public function setDoctrineAnnotationReader(Reader $annotationReader): self - { - $this->doctrineAnnotationReader = $annotationReader; - - return $this; - } - - /** - * Returns a cached Doctrine annotation reader. - * Note: we cannot get the annotation reader service in the container as we are in a compiler pass. - */ - private function getDoctrineAnnotationReader(CacheItemPoolInterface $cache): Reader - { - return $this->doctrineAnnotationReader ?? new PsrCachedReader(new DoctrineAnnotationReader(), $cache, true); - } - public function setAuthenticationService(AuthenticationServiceInterface $authenticationService): self { $this->authenticationService = $authenticationService; @@ -336,7 +313,7 @@ public function setExpressionLanguage(ExpressionLanguage $expressionLanguage): s public function createSchema(): Schema { $symfonyCache = new Psr16Adapter($this->cache, $this->cacheNamespace); - $annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader($symfonyCache), AnnotationReader::LAX_MODE); + $annotationReader = new AnnotationReader(); $authenticationService = $this->authenticationService ?: new FailAuthenticationService(); $authorizationService = $this->authorizationService ?: new FailAuthorizationService(); $typeResolver = new TypeResolver(); diff --git a/src/Types/TypeResolver.php b/src/Types/TypeResolver.php index 5edb635a01..9e6e095837 100644 --- a/src/Types/TypeResolver.php +++ b/src/Types/TypeResolver.php @@ -12,6 +12,7 @@ use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Schema; use GraphQL\Utils\AST; +use JsonException; use RuntimeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; @@ -30,7 +31,7 @@ public function registerSchema(Schema $schema): void $this->schema = $schema; } - /** @throws CannotMapTypeExceptionInterface */ + /** @throws CannotMapTypeExceptionInterface|JsonException */ public function mapNameToType(string $typeName): Type { if ($this->schema === null) { diff --git a/tests/AbstractQueryProvider.php b/tests/AbstractQueryProvider.php index 37ca186cce..129068079c 100644 --- a/tests/AbstractQueryProvider.php +++ b/tests/AbstractQueryProvider.php @@ -1,14 +1,15 @@ [ 'test' => Type::string(), ], - ], function ($source, $args) { + ], static function ($source, $args) { return new TestObject($args['test']); }); } @@ -123,29 +122,18 @@ protected function getTypeMapper() $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); - $this->typeMapper = new RecursiveTypeMapper(new class($this->getTestObjectType(), $this->getTestObjectType2(), $this->getInputTestObjectType()/*, $this->getInputTestObjectType2()*/ - ) implements TypeMapperInterface { - /** - * @var ObjectType - */ + $this->typeMapper = new RecursiveTypeMapper(new class ($this->getTestObjectType(), $this->getTestObjectType2(), $this->getInputTestObjectType()/*, $this->getInputTestObjectType2()*/) implements TypeMapperInterface { + /** @var ObjectType */ private $testObjectType; - /** - * @var ObjectType - */ + /** @var ObjectType */ private $testObjectType2; - /** - * @var InputObjectType - */ + /** @var InputObjectType */ private $inputTestObjectType; - /** - * @var InputObjectType - */ - public function __construct( - ObjectType $testObjectType, - ObjectType $testObjectType2, - InputObjectType $inputTestObjectType + ObjectType $testObjectType, + ObjectType $testObjectType2, + InputObjectType $inputTestObjectType, ) { $this->testObjectType = $testObjectType; @@ -153,7 +141,7 @@ public function __construct( $this->inputTestObjectType = $inputTestObjectType; } - public function mapClassToType(string $className, ?OutputType $subType): MutableInterface + public function mapClassToType(string $className, OutputType|null $subType): MutableInterface { if ($className === TestObject::class) { return $this->testObjectType; @@ -234,7 +222,6 @@ public function decorateInputTypeForName(string $typeName, ResolvableMutableInpu { throw CannotMapTypeException::createForDecorateName($typeName, $type); } - }, new NamingStrategy(), new Psr16Cache($arrayAdapter), $this->getTypeRegistry(), $this->getAnnotationReader()); } return $this->typeMapper; @@ -264,7 +251,7 @@ protected function buildAutoWiringContainer(ContainerInterface $container): Basi protected function getAnnotationReader(): AnnotationReader { if ($this->annotationReader === null) { - $this->annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $this->annotationReader = new AnnotationReader(); } return $this->annotationReader; } @@ -297,18 +284,20 @@ protected function buildFieldsBuilder(): FieldsBuilder $fieldMiddlewarePipe = new FieldMiddlewarePipe(); $fieldMiddlewarePipe->pipe(new AuthorizationFieldMiddleware( new VoidAuthenticationService(), - new VoidAuthorizationService() + new VoidAuthorizationService(), )); $expressionLanguage = new ExpressionLanguage( new Psr16Adapter($psr16Cache), - [new SecurityExpressionLanguageProvider()] + [new SecurityExpressionLanguageProvider()], ); $fieldMiddlewarePipe->pipe( - new SecurityFieldMiddleware($expressionLanguage, + new SecurityFieldMiddleware( + $expressionLanguage, new VoidAuthenticationService(), - new VoidAuthorizationService()) + new VoidAuthorizationService(), + ), ); $inputFieldMiddlewarePipe = new InputFieldMiddlewarePipe(); @@ -323,7 +312,7 @@ protected function buildFieldsBuilder(): FieldsBuilder $this->buildRootTypeMapper(), $parameterMiddlewarePipe, $fieldMiddlewarePipe, - $inputFieldMiddlewarePipe + $inputFieldMiddlewarePipe, ); $parameterizedCallableResolver = new ParameterizedCallableResolver($fieldsBuilder, $container); @@ -354,7 +343,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface $rootTypeMapper = new BaseTypeMapper( $errorRootTypeMapper, $this->getTypeMapper(), - $topRootTypeMapper + $topRootTypeMapper, ); // Annotation support - deprecated @@ -362,14 +351,14 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface $rootTypeMapper, $this->getAnnotationReader(), $arrayAdapter, - [] + [], ); $rootTypeMapper = new EnumTypeMapper( $rootTypeMapper, $this->getAnnotationReader(), $arrayAdapter, - [] + [], ); $rootTypeMapper = new CompoundTypeMapper( @@ -377,7 +366,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface $topRootTypeMapper, new NamingStrategy(), $this->getTypeRegistry(), - $this->getTypeMapper() + $this->getTypeMapper(), ); $rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper); @@ -407,7 +396,7 @@ protected function getTypeGenerator(): TypeGenerator $this->getTypeRegistry(), $this->getRegistry(), $this->getTypeMapper(), - $this->getFieldsBuilder() + $this->getFieldsBuilder(), ); return $this->typeGenerator; @@ -421,7 +410,7 @@ protected function getInputTypeGenerator(): InputTypeGenerator $this->inputTypeGenerator = new InputTypeGenerator( $this->getInputTypeUtils(), - $this->getFieldsBuilder() + $this->getFieldsBuilder(), ); return $this->inputTypeGenerator; @@ -432,7 +421,7 @@ protected function getInputTypeUtils(): InputTypeUtils if ($this->inputTypeUtils === null) { $this->inputTypeUtils = new InputTypeUtils( $this->getAnnotationReader(), - new NamingStrategy() + new NamingStrategy(), ); } return $this->inputTypeUtils; @@ -442,7 +431,7 @@ protected function getTypeResolver(): TypeResolver { if ($this->typeResolver === null) { $this->typeResolver = new TypeResolver(); - $this->typeResolver->registerSchema(new \GraphQL\Type\Schema([])); + $this->typeResolver->registerSchema(new Schema([])); } return $this->typeResolver; } diff --git a/tests/AnnotationReaderTest.php b/tests/AnnotationReaderTest.php index a476222c27..0ec8f2158e 100644 --- a/tests/AnnotationReaderTest.php +++ b/tests/AnnotationReaderTest.php @@ -1,14 +1,13 @@ expectException(InvalidArgumentException::class); - new AnnotationReader(new DoctrineAnnotationReader(), 'foo'); - } - - public function testStrictMode(): void + public function testBadAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::STRICT_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getTypeAnnotation(new ReflectionClass(ClassWithInvalidClassAnnotation::class)); - } - - public function testLaxModeWithBadAnnotation(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); + $annotationReader = new AnnotationReader(); $type = $annotationReader->getTypeAnnotation(new ReflectionClass(ClassWithInvalidClassAnnotation::class)); $this->assertNull($type); } - public function testLaxModeWithSmellyAnnotation(): void + public function testSmellyAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); + $annotationReader = new AnnotationReader(); - $this->expectException(AnnotationException::class); - $annotationReader->getTypeAnnotation(new ReflectionClass(ClassWithInvalidTypeAnnotation::class)); + $this->assertNull($annotationReader->getTypeAnnotation(new ReflectionClass(ClassWithInvalidTypeAnnotation::class))); } - public function testLaxModeWithBadAnnotationAndStrictNamespace(): void + public function testGetAnnotationsWithBadAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, ['TheCodingMachine\\GraphQLite\\Fixtures']); - - $this->expectException(AnnotationException::class); - $annotationReader->getTypeAnnotation(new ReflectionClass(ClassWithInvalidClassAnnotation::class)); - } - - public function testGetAnnotationsStrictMode(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::STRICT_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getClassAnnotations(new ReflectionClass(ClassWithInvalidClassAnnotation::class), Type::class); - } - - public function testGetAnnotationsLaxModeWithBadAnnotation(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); + $annotationReader = new AnnotationReader(); $types = $annotationReader->getClassAnnotations(new ReflectionClass(ClassWithInvalidClassAnnotation::class), Type::class); $this->assertSame([], $types); } - public function testGetAnnotationsLaxModeWithSmellyAnnotation(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getClassAnnotations(new ReflectionClass(ClassWithInvalidTypeAnnotation::class), Type::class); - } - - public function testGetAnnotationsLaxModeWithBadAnnotationAndStrictNamespace(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, ['TheCodingMachine\\GraphQLite\\Fixtures']); - - $this->expectException(AnnotationException::class); - $annotationReader->getClassAnnotations(new ReflectionClass(ClassWithInvalidClassAnnotation::class), Type::class); - } - - public function testMethodStrictMode(): void + public function testMethodWithBadAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::STRICT_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getRequestAnnotation(new ReflectionMethod(ClassWithInvalidClassAnnotation::class, 'testMethod'), Field::class); - } - - public function testMethodLaxModeWithBadAnnotation(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); + $annotationReader = new AnnotationReader(); $type = $annotationReader->getRequestAnnotation(new ReflectionMethod(ClassWithInvalidClassAnnotation::class, 'testMethod'), Field::class); $this->assertNull($type); } - public function testMethodLaxModeWithSmellyAnnotation(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getRequestAnnotation(new ReflectionMethod(ClassWithInvalidTypeAnnotation::class, 'testMethod'), Field::class); - } - public function testExtendAnnotationException(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::STRICT_MODE, []); + $annotationReader = new AnnotationReader(); $this->expectException(ClassNotFoundException::class); - $this->expectExceptionMessage("Could not autoload class 'foo' defined in @ExtendType annotation of class 'TheCodingMachine\GraphQLite\Fixtures\Annotations\ClassWithInvalidExtendTypeAnnotation'"); + $this->expectExceptionMessage("Could not autoload class 'foo' defined in #[ExtendType] attribute of class 'TheCodingMachine\GraphQLite\Fixtures\Annotations\ClassWithInvalidExtendTypeAnnotation'"); $annotationReader->getExtendTypeAnnotation(new ReflectionClass(ClassWithInvalidExtendTypeAnnotation::class)); } - public function testMethodsStrictMode(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::STRICT_MODE, []); - - $this->expectException(AnnotationException::class); - $annotationReader->getMethodAnnotations(new ReflectionMethod(ClassWithInvalidClassAnnotation::class, 'testMethod'), Field::class); - } - - public function testMethodsLaxModeWithBadAnnotation(): void + public function testMethodsWithBadAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, []); + $annotationReader = new AnnotationReader(); $type = $annotationReader->getMethodAnnotations(new ReflectionMethod(ClassWithInvalidClassAnnotation::class, 'testMethod'), Field::class); $this->assertSame([], $type); } - public function testGetMethodsAnnotationsLaxModeWithBadAnnotationAndStrictNamespace(): void - { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader(), AnnotationReader::LAX_MODE, ['TheCodingMachine\\GraphQLite\\Fixtures']); - - $this->expectException(AnnotationException::class); - $annotationReader->getMethodAnnotations(new ReflectionMethod(ClassWithInvalidClassAnnotation::class, 'testMethod'), Type::class); - } - public function testEmptyGetParameterAnnotations(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); $this->assertEmpty($annotationReader->getParameterAnnotationsPerParameter([])); } public function testPhp8AttributeClassAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); $type = $annotationReader->getTypeAnnotation(new ReflectionClass(TestType::class)); $this->assertSame(TestType::class, $type->getClass()); @@ -168,7 +88,7 @@ public function testPhp8AttributeClassAnnotation(): void public function testPhp8AttributeClassAnnotations(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); $types = $annotationReader->getSourceFields(new ReflectionClass(TestType::class)); @@ -177,7 +97,7 @@ public function testPhp8AttributeClassAnnotations(): void public function testPhp8AttributeMethodAnnotation(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); $type = $annotationReader->getRequestAnnotation(new ReflectionMethod(TestType::class, 'getField'), Field::class); $this->assertInstanceOf(Field::class, $type); @@ -185,7 +105,7 @@ public function testPhp8AttributeMethodAnnotation(): void public function testPhp8AttributeMethodAnnotations(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); $middlewareAnnotations = $annotationReader->getMiddlewareAnnotations(new ReflectionMethod(TestType::class, 'getField')); @@ -199,16 +119,16 @@ public function testPhp8AttributeMethodAnnotations(): void public function testPhp8AttributeParameterAnnotations(): void { - $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + $annotationReader = new AnnotationReader(); - $parameterAnnotations = $annotationReader->getParameterAnnotationsPerParameter((new ReflectionMethod(__CLASS__, 'method1'))->getParameters()); + $parameterAnnotations = $annotationReader->getParameterAnnotationsPerParameter((new ReflectionMethod(self::class, 'method1'))->getParameters()); $this->assertInstanceOf(Autowire::class, $parameterAnnotations['dao']->getAnnotationByType(Autowire::class)); } private function method1( #[Autowire('myService')] - $dao + $dao, ): void { } } diff --git a/tests/Annotations/AutowireTest.php b/tests/Annotations/AutowireTest.php index acc8883e4f..9c68ffa332 100644 --- a/tests/Annotations/AutowireTest.php +++ b/tests/Annotations/AutowireTest.php @@ -11,7 +11,7 @@ class AutowireTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"'); + $this->expectExceptionMessage('The #[Autowire] attribute must be passed a target. For instance: "#[Autowire(for: "$myService")]"'); (new Autowire([]))->getTarget(); } } diff --git a/tests/Annotations/DecorateTest.php b/tests/Annotations/DecorateTest.php index 083fff18d1..3015edbd4d 100644 --- a/tests/Annotations/DecorateTest.php +++ b/tests/Annotations/DecorateTest.php @@ -12,7 +12,7 @@ class DecorateTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @Decorate annotation must be passed an input type. For instance: "@Decorate("MyInputType")"'); + $this->expectExceptionMessage('The #[Decorate] attribute must be passed an input type. For instance: "#[Decorate("MyInputType")]"'); new Decorate([]); } diff --git a/tests/Annotations/ExtendTypeTest.php b/tests/Annotations/ExtendTypeTest.php index 5530d7fa99..ab0d7627a3 100644 --- a/tests/Annotations/ExtendTypeTest.php +++ b/tests/Annotations/ExtendTypeTest.php @@ -11,7 +11,7 @@ class ExtendTypeTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".'); + $this->expectExceptionMessage('In attribute #[ExtendType], missing one of the compulsory parameter "class" or "name".'); new ExtendType([]); } } diff --git a/tests/Annotations/FailWithTest.php b/tests/Annotations/FailWithTest.php index 79842c7029..468c9d73c7 100644 --- a/tests/Annotations/FailWithTest.php +++ b/tests/Annotations/FailWithTest.php @@ -12,7 +12,7 @@ class FailWithTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); + $this->expectExceptionMessage('The #[FailWith] attribute must be passed a defaultValue. For instance: "#[FailWith(null)]"'); new FailWith([]); } diff --git a/tests/Annotations/InputTest.php b/tests/Annotations/InputTest.php new file mode 100644 index 0000000000..0fcdaa5b14 --- /dev/null +++ b/tests/Annotations/InputTest.php @@ -0,0 +1,18 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('Empty class for #[Input] attribute. You MUST create the Input attribute object using the GraphQLite AnnotationReader'); + (new Input())->getClass(); + } +} diff --git a/tests/Annotations/RightTest.php b/tests/Annotations/RightTest.php index 4ca7fa86ed..0040e1061d 100644 --- a/tests/Annotations/RightTest.php +++ b/tests/Annotations/RightTest.php @@ -12,7 +12,7 @@ class RightTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); + $this->expectExceptionMessage('The #[Right] attribute must be passed a right name. For instance: "#[Right(\'my_right\')]"'); new Right([]); } diff --git a/tests/Annotations/SecurityTest.php b/tests/Annotations/SecurityTest.php index 8ddafb5468..a35275092f 100644 --- a/tests/Annotations/SecurityTest.php +++ b/tests/Annotations/SecurityTest.php @@ -13,14 +13,14 @@ class SecurityTest extends TestCase public function testBadParams(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @Security annotation must be passed an expression. For instance: "@Security("is_granted(\'CAN_EDIT_STUFF\')")"'); + $this->expectExceptionMessage('The #[Security] attribute must be passed an expression. For instance: "#[Security("is_granted(\'CAN_EDIT_STUFF\')")]"'); new Security([]); } public function testIncompatibleParams(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('A @Security annotation that has "failWith" attribute set cannot have a message or a statusCode attribute.'); + $this->expectExceptionMessage('A #[Security] attribute that has "failWith" attribute set cannot have a message or a statusCode attribute.'); new Security(['expression'=>'foo', 'failWith'=>null, 'statusCode'=>500]); } } diff --git a/tests/Annotations/TypeTest.php b/tests/Annotations/TypeTest.php index a3909844b2..486befd85a 100644 --- a/tests/Annotations/TypeTest.php +++ b/tests/Annotations/TypeTest.php @@ -13,7 +13,7 @@ public function testException(): void { $type = new Type([]); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Empty class for @Type annotation. You MUST create the Type annotation object using the GraphQLite AnnotationReader'); + $this->expectExceptionMessage('Empty class for #[Type] attribute. You MUST create the Type attribute object using the GraphQLite AnnotationReader'); $type->getClass(); } @@ -27,7 +27,7 @@ public function testException2() { $type = new Type(['default'=>false]); $this->expectException(GraphQLRuntimeException::class); - $this->expectExceptionMessage('Problem in annotation @Type for interface "TheCodingMachine\GraphQLite\Fixtures\AnnotatedInterfaces\Types\FooInterface": you cannot use the default="false" attribute on interfaces'); + $this->expectExceptionMessage('Problem in attribute #[Type] for interface "TheCodingMachine\GraphQLite\Fixtures\AnnotatedInterfaces\Types\FooInterface": you cannot use the default="false" attribute on interfaces'); $type->setClass(FooInterface::class); } } diff --git a/tests/Annotations/UseInputTypeTest.php b/tests/Annotations/UseInputTypeTest.php index 47cb85b329..b2815ecdd2 100644 --- a/tests/Annotations/UseInputTypeTest.php +++ b/tests/Annotations/UseInputTypeTest.php @@ -12,14 +12,14 @@ class UseInputTypeTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @UseInputType annotation must be passed an input type. For instance: #[UseInputType("MyInputType")]'); + $this->expectExceptionMessage('The #[UseInputType] attribute must be passed an input type. For instance: #[UseInputType("MyInputType")]'); new UseInputType([]); } public function testException2(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @UseInputType annotation must be passed a target and an input type. For instance: #[UseInputType("MyInputType")]'); + $this->expectExceptionMessage('The #[UseInputType] attribute must be passed a target and an input type. For instance: #[UseInputType("MyInputType")]'); (new UseInputType(['inputType' => 'foo']))->getTarget(); } diff --git a/tests/FieldsBuilderTest.php b/tests/FieldsBuilderTest.php index 630b63c7bc..ffcc0b522f 100644 --- a/tests/FieldsBuilderTest.php +++ b/tests/FieldsBuilderTest.php @@ -1,5 +1,7 @@ assertInstanceOf(EnumType::class, $usersQuery->args[9]->getType()); $this->assertSame('TestObjectInput', $usersQuery->args[1]->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name); - $context = ['int' => 42, 'string' => 'foo', 'list' => [ - ['test' => 42], - ['test' => 12], - ], + $context = [ + 'int' => 42, + 'string' => 'foo', + 'list' => [ + ['test' => '42'], + ['test' => '12'], + ], 'boolean' => true, 'float' => 4.2, 'dateTimeImmutable' => '2017-01-01 01:01:01', 'dateTime' => '2017-01-01 01:01:01', 'id' => 42, - 'enum' => TestEnum::ON() + 'enum' => TestEnum::ON(), ]; $resolve = $usersQuery->resolveFn; @@ -147,7 +153,7 @@ public function testMutations(): void $resolve = $testReturnMutation->resolveFn; $result = $resolve( new stdClass(), - ['testObject' => ['test' => 42]], + ['testObject' => ['test' => '42']], null, $this->createMock(ResolveInfo::class), ); @@ -179,10 +185,7 @@ public function testSubscriptions(): void public function testErrors(): void { $controller = new class { - /** - * @Query - * @return string - */ + #[Query] public function test($noTypeHint): string { return 'foo'; @@ -198,12 +201,8 @@ public function test($noTypeHint): string public function testTypeInDocBlock(): void { $controller = new class { - /** - * @Query - * @param int $typeHintInDocBlock - * @return string - */ - public function test($typeHintInDocBlock) + #[Query] + public function test(int $typeHintInDocBlock): string { return 'foo'; } @@ -344,7 +343,7 @@ public function isLogged(): bool return true; } - public function getUser(): ?object + public function getUser(): object|null { return new stdClass(); } @@ -360,16 +359,15 @@ public function getUser(): ?object $this->getParameterMiddlewarePipe(), new AuthorizationFieldMiddleware( $authenticationService, - new VoidAuthorizationService() + new VoidAuthorizationService(), ), - new InputFieldMiddlewarePipe() + new InputFieldMiddlewarePipe(), ); - $fields = $queryProvider->getFields(new TestType(), true); + $fields = $queryProvider->getFields(new TestType()); $this->assertCount(4, $fields); $this->assertSame('testBool', $fields['testBool']->name); - } public function testRightInSourceField(): void @@ -392,16 +390,15 @@ public function isAllowed(string $right, $subject = null): bool $this->getParameterMiddlewarePipe(), new AuthorizationFieldMiddleware( new VoidAuthenticationService(), - $authorizationService + $authorizationService, ), - new InputFieldMiddlewarePipe() + new InputFieldMiddlewarePipe(), ); - $fields = $queryProvider->getFields(new TestType(), true); + $fields = $queryProvider->getFields(new TestType()); $this->assertCount(4, $fields); $this->assertSame('testRight', $fields['testRight']->name); - } public function testMissingTypeAnnotation(): void @@ -409,7 +406,7 @@ public function testMissingTypeAnnotation(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(MissingAnnotationException::class); - $queryProvider->getFields(new TestTypeMissingAnnotation(), true); + $queryProvider->getFields(new TestTypeMissingAnnotation()); } public function testSourceFieldDoesNotExists(): void @@ -417,8 +414,8 @@ public function testSourceFieldDoesNotExists(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(FieldNotFoundException::class); - $this->expectExceptionMessage("There is an issue with a @SourceField annotation in class \"TheCodingMachine\GraphQLite\Fixtures\TestTypeMissingField\": Could not find a getter or a isser for field \"notExists\". Looked for: \"TheCodingMachine\GraphQLite\Fixtures\TestObject::notExists()\", \"TheCodingMachine\GraphQLite\Fixtures\TestObject::getNotExists()\", \"TheCodingMachine\GraphQLite\Fixtures\TestObject::isNotExists()"); - $queryProvider->getFields(new TestTypeMissingField(), true); + $this->expectExceptionMessage('There is an issue with a @SourceField annotation in class "TheCodingMachine\GraphQLite\Fixtures\TestTypeMissingField": Could not find a getter or a isser for field "notExists". Looked for: "TheCodingMachine\GraphQLite\Fixtures\TestObject::notExists()", "TheCodingMachine\GraphQLite\Fixtures\TestObject::getNotExists()", "TheCodingMachine\GraphQLite\Fixtures\TestObject::isNotExists()'); + $queryProvider->getFields(new TestTypeMissingField()); } public function testSourceFieldHasMissingReturnType(): void @@ -427,13 +424,13 @@ public function testSourceFieldHasMissingReturnType(): void $this->expectException(CannotMapTypeException::class); $this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Fixtures\TestObjectMissingReturnType::getTest, a type-hint is missing (or PHPDoc specifies a "mixed" type-hint). Please provide a better type-hint.'); - $queryProvider->getFields(new TestTypeMissingReturnType(), true); + $queryProvider->getFields(new TestTypeMissingReturnType()); } public function testSourceFieldIsId(): void { $queryProvider = $this->buildFieldsBuilder(); - $fields = $queryProvider->getFields(new TestTypeId(), true); + $fields = $queryProvider->getFields(new TestTypeId()); $this->assertCount(1, $fields); $this->assertSame('test', $fields['test']->name); @@ -454,15 +451,14 @@ public function testFromSourceFieldsInterface(): void $this->getParameterMiddlewarePipe(), new AuthorizationFieldMiddleware( new VoidAuthenticationService(), - new VoidAuthorizationService() + new VoidAuthorizationService(), ), - new InputFieldMiddlewarePipe() + new InputFieldMiddlewarePipe(), ); - $fields = $queryProvider->getFields(new TestTypeWithSourceFieldInterface(), true); + $fields = $queryProvider->getFields(new TestTypeWithSourceFieldInterface()); $this->assertCount(1, $fields); $this->assertSame('test', $fields['test']->name); - } public function testQueryProviderWithIterableClass(): void @@ -571,7 +567,7 @@ public function testQueryProviderWithInvalidInputType(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(CannotMapTypeException::class); - $this->expectExceptionMessage('For parameter $foo, in TheCodingMachine\GraphQLite\Fixtures\TestControllerWithInvalidInputType::test, cannot map class "Exception" to a known GraphQL input type. Are you missing a @Factory annotation? If you have a @Factory annotation, is it in a namespace analyzed by GraphQLite?'); + $this->expectExceptionMessage('For parameter $foo, in TheCodingMachine\GraphQLite\Fixtures\TestControllerWithInvalidInputType::test, cannot map class "Throwable" to a known GraphQL input type. Are you missing a @Factory annotation? If you have a @Factory annotation, is it in a namespace analyzed by GraphQLite?'); $queryProvider->getQueries($controller); } @@ -657,14 +653,13 @@ public function testSourceFieldWithFailWith(): void $queryProvider = $this->buildFieldsBuilder(); - $fields = $queryProvider->getFields($controller, true); + $fields = $queryProvider->getFields($controller); $this->assertCount(1, $fields); $this->assertSame('test', $fields['test']->name); $this->assertInstanceOf(StringType::class, $fields['test']->getType()); - $resolve = $fields['test']->resolveFn; $result = $resolve(new stdClass(), [], null, $this->createMock(ResolveInfo::class)); @@ -678,7 +673,7 @@ public function testSourceFieldBadOutputTypeException(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(CannotMapTypeExceptionInterface::class); $this->expectExceptionMessage('For @SourceField "test" declared in "TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType", cannot find GraphQL type "[NotExists]". Check your TypeMapper configuration.'); - $queryProvider->getFields(new TestSourceFieldBadOutputType(), true); + $queryProvider->getFields(new TestSourceFieldBadOutputType()); } public function testSourceFieldBadOutputType2Exception(): void @@ -686,7 +681,7 @@ public function testSourceFieldBadOutputType2Exception(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(CannotMapTypeExceptionInterface::class); $this->expectExceptionMessage('For @SourceField "test" declared in "TheCodingMachine\GraphQLite\Fixtures\TestSourceFieldBadOutputType2", Syntax Error: Expected ], found '); - $queryProvider->getFields(new TestSourceFieldBadOutputType2(), true); + $queryProvider->getFields(new TestSourceFieldBadOutputType2()); } public function testBadOutputTypeException(): void @@ -694,7 +689,7 @@ public function testBadOutputTypeException(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(CannotMapTypeExceptionInterface::class); $this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Fixtures\TestFieldBadOutputType::test, cannot find GraphQL type "[NotExists]". Check your TypeMapper configuration.'); - $queryProvider->getFields(new TestFieldBadOutputType(), true); + $queryProvider->getFields(new TestFieldBadOutputType()); } public function testBadInputTypeException(): void @@ -702,7 +697,7 @@ public function testBadInputTypeException(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(CannotMapTypeExceptionInterface::class); $this->expectExceptionMessage('For parameter $input, in TheCodingMachine\GraphQLite\Fixtures\TestFieldBadInputType::testInput, cannot find GraphQL type "[NotExists]". Check your TypeMapper configuration.'); - $queryProvider->getFields(new TestFieldBadInputType(), true); + $queryProvider->getFields(new TestFieldBadInputType()); } public function testDoubleReturnException(): void @@ -710,7 +705,7 @@ public function testDoubleReturnException(): void $queryProvider = $this->buildFieldsBuilder(); $this->expectException(InvalidDocBlockRuntimeException::class); $this->expectExceptionMessage('Method TheCodingMachine\\GraphQLite\\Fixtures\\TestDoubleReturnTag::test has several @return annotations.'); - $queryProvider->getFields(new TestDoubleReturnTag(), true); + $queryProvider->getFields(new TestDoubleReturnTag()); } public function testMissingArgument(): void @@ -855,17 +850,6 @@ public function testQueryProviderWithUnionInputParam(): void $queries = $queryProvider->getQueries($controller); } - public function testParameterAnnotationOnNonExistingParameter(): void - { - $controller = new TestControllerWithInvalidParameterAnnotation(); - - $queryProvider = $this->buildFieldsBuilder(); - - $this->expectException(InvalidParameterException::class); - $this->expectExceptionMessage('Parameter "id" declared in annotation "TheCodingMachine\\GraphQLite\\Annotations\\HideParameter" of method "TheCodingMachine\\GraphQLite\\Fixtures\\TestControllerWithInvalidParameterAnnotation::test()" does not exist.'); - $queries = $queryProvider->getQueries($controller); - } - public function testParameterAnnotationOnNonExistingParameterInSourceField(): void { $controller = new TestTypeWithSourceFieldInvalidParameterAnnotation(); @@ -873,7 +857,7 @@ public function testParameterAnnotationOnNonExistingParameterInSourceField(): vo $queryProvider = $this->buildFieldsBuilder(); $this->expectException(InvalidParameterException::class); - $this->expectExceptionMessage('Could not find parameter "foo" declared in annotation "TheCodingMachine\\GraphQLite\\Annotations\\HideParameter". This annotation is itself declared in a SourceField annotation targeting resolver "TheCodingMachine\\GraphQLite\\Fixtures\\TestObject::getSibling()".'); + $this->expectExceptionMessage('Could not find parameter "foo" declared in annotation "TheCodingMachine\\GraphQLite\\Annotations\\HideParameter". This annotation is itself declared in a SourceField attribute targeting resolver "TheCodingMachine\\GraphQLite\\Fixtures\\TestObject::getSibling()".'); $fields = $queryProvider->getFields($controller); } diff --git a/tests/Fixtures/AbstractTestController.php b/tests/Fixtures/AbstractTestController.php index deb15c2d81..f4042a198b 100644 --- a/tests/Fixtures/AbstractTestController.php +++ b/tests/Fixtures/AbstractTestController.php @@ -8,9 +8,7 @@ // An abstract class to test that the GlobControllerQueryProvider does not try anything with it. abstract class AbstractTestController { - /** - * @Query() - */ + #[Query] public function test(): string { return 'foo'; diff --git a/tests/Fixtures/AnnotatedInterfaces/Controllers/AnnotatedInterfaceController.php b/tests/Fixtures/AnnotatedInterfaces/Controllers/AnnotatedInterfaceController.php index f93611fe16..4b0970ba1e 100644 --- a/tests/Fixtures/AnnotatedInterfaces/Controllers/AnnotatedInterfaceController.php +++ b/tests/Fixtures/AnnotatedInterfaces/Controllers/AnnotatedInterfaceController.php @@ -15,17 +15,13 @@ class AnnotatedInterfaceController { - /** - * @Query() - */ + #[Query] public function getClassA(): ClassA { return new ClassA(); } - /** - * @Query() - */ + #[Query] public function getFoo(): FooInterface { return new ClassD(); @@ -39,17 +35,13 @@ public function getFoo(): FooInterface return new ClassD(); }*/ - /** - * @Query() - */ + #[Query] public function getClassDAsWizInterface(): WizzInterface { return new ClassD(); } - /** - * @Query() - */ + #[Query] public function getQux(): QuxInterface { return new NotAnnotatedQux(); diff --git a/tests/Fixtures/AnnotatedInterfaces/Types/BarInterface.php b/tests/Fixtures/AnnotatedInterfaces/Types/BarInterface.php index b4f9ea3ccf..ade2792985 100644 --- a/tests/Fixtures/AnnotatedInterfaces/Types/BarInterface.php +++ b/tests/Fixtures/AnnotatedInterfaces/Types/BarInterface.php @@ -1,19 +1,15 @@ circularInputB = $circularInputB; } - /** - * @Field - */ - public function setBar(int $bar): void { + #[Field] + public function setBar(int $bar): void + { $this->bar = $bar; } - /** - * @return CircularInputB - */ - public function getCircularInputB() + public function getCircularInputB(): CircularInputB { return $this->circularInputB; } - /** - * @return int - */ public function getBar(): int { return $this->bar; diff --git a/tests/Fixtures/CircularInputReference/Types/CircularInputB.php b/tests/Fixtures/CircularInputReference/Types/CircularInputB.php index aaedad7775..eb5f4f793d 100644 --- a/tests/Fixtures/CircularInputReference/Types/CircularInputB.php +++ b/tests/Fixtures/CircularInputReference/Types/CircularInputB.php @@ -1,48 +1,36 @@ circularInputA = $circularInputA; } - /** - * @Field - */ - public function setBar(int $bar): void { + #[Field] + public function setBar(int $bar): void + { $this->bar = $bar; } - /** - * @return CircularInputA - */ public function getCircularInputA() { return $this->circularInputA; } - /** - * @return int - */ public function getBar(): int { return $this->bar; diff --git a/tests/Fixtures/DuplicateInputTypes/TestFactory.php b/tests/Fixtures/DuplicateInputTypes/TestFactory.php index 6fe81dd275..5072bd8288 100644 --- a/tests/Fixtures/DuplicateInputTypes/TestFactory.php +++ b/tests/Fixtures/DuplicateInputTypes/TestFactory.php @@ -1,20 +1,15 @@ value = $value; } - /** - * @Field() - */ + #[Field] public function getValue(): string { return $this->value; } - /** - * @Factory(name="InAndOut") - */ + #[Factory(name: 'InAndOut')] public static function create(string $value): self { return new self($value); diff --git a/tests/Fixtures/Inputs/FooBar.php b/tests/Fixtures/Inputs/FooBar.php index ff76408377..56fa07db42 100644 --- a/tests/Fixtures/Inputs/FooBar.php +++ b/tests/Fixtures/Inputs/FooBar.php @@ -1,46 +1,32 @@ foo = $foo; $this->bar = $bar; diff --git a/tests/Fixtures/Inputs/InputInterface.php b/tests/Fixtures/Inputs/InputInterface.php index 744299c5f5..f307cf916c 100644 --- a/tests/Fixtures/Inputs/InputInterface.php +++ b/tests/Fixtures/Inputs/InputInterface.php @@ -1,12 +1,12 @@ foo = $foo; } - /** - * @Field(for="InputWithSetterInput") - * @Field(for="ForcedTypeInput", inputType="Int!") - */ - public function setBar(int $bar): void { + #[Field(for: 'InputWithSetterInput')] + #[Field(for: 'ForcedTypeInput', inputType: 'Int!')] + public function setBar(int $bar): void + { $this->bar = $bar; } - /** - * @return string - */ public function getFoo(): string { return $this->foo; } - /** - * @return int - */ public function getBar(): int { return $this->bar; diff --git a/tests/Fixtures/Inputs/TestConstructorAndProperties.php b/tests/Fixtures/Inputs/TestConstructorAndProperties.php index 82647c91ad..57243d4926 100644 --- a/tests/Fixtures/Inputs/TestConstructorAndProperties.php +++ b/tests/Fixtures/Inputs/TestConstructorAndProperties.php @@ -1,49 +1,40 @@ date = $date; $this->foo = $foo; } - public function getDate(): \DateTimeImmutable + public function getDate(): DateTimeImmutable { return $this->date; } public function setFoo(string $foo): void { - throw new \RuntimeException("This should not be called"); + throw new RuntimeException('This should not be called'); } public function getFoo(): string diff --git a/tests/Fixtures/Inputs/TestOnlyConstruct.php b/tests/Fixtures/Inputs/TestOnlyConstruct.php index d4a0ecbf83..e01be70029 100644 --- a/tests/Fixtures/Inputs/TestOnlyConstruct.php +++ b/tests/Fixtures/Inputs/TestOnlyConstruct.php @@ -1,36 +1,26 @@ foo = $foo; $this->bar = $bar; @@ -46,7 +36,7 @@ public function setBar(int $bar): void { throw new Exception('This should not be called!'); } - + public function setBaz(bool $baz): void { throw new Exception('This should not be called!'); diff --git a/tests/Fixtures/Inputs/TypedFooBar.php b/tests/Fixtures/Inputs/TypedFooBar.php index 770eb24fd9..d04e92c481 100644 --- a/tests/Fixtures/Inputs/TypedFooBar.php +++ b/tests/Fixtures/Inputs/TypedFooBar.php @@ -1,23 +1,18 @@ new Contact('Joe'), 'Bill' => new Contact('Bill'), default => null, @@ -43,16 +42,15 @@ public function saveContact(Contact $contact): Contact } #[Mutation] - public function saveBirthDate(\DateTimeInterface $birthDate): Contact { + public function saveBirthDate(DateTimeInterface $birthDate): Contact + { $contact = new Contact('Bill'); $contact->setBirthDate($birthDate); return $contact; } - /** - * @return Contact[] - */ + /** @return Contact[] */ #[Query] public function getContactsIterator(): ArrayResult { @@ -62,9 +60,7 @@ public function getContactsIterator(): ArrayResult ]); } - /** - * @return string[]|ArrayResult - */ + /** @return string[]|ArrayResult */ #[Query] public function getContactsNamesIterator(): ArrayResult { @@ -86,7 +82,7 @@ public function getOtherContact(): Contact * @return Result|Contact[]|null */ #[Query] - public function getNullableResult(): ?Result + public function getNullableResult(): Result|null { return null; } diff --git a/tests/Fixtures/Integration/Controllers/FilterController.php b/tests/Fixtures/Integration/Controllers/FilterController.php index 9c027cf6ad..abb7ee2e57 100644 --- a/tests/Fixtures/Integration/Controllers/FilterController.php +++ b/tests/Fixtures/Integration/Controllers/FilterController.php @@ -1,30 +1,29 @@ getValues()); + return array_map(static function ($item) { + return (string) $item; + }, $filter->getValues()); } - /** - * @Query() - * @return string[]|null - */ - public function echoNullableFilters(?Filter $filter): ?array + /** @return string[]|null */ + #[Query] + public function echoNullableFilters(Filter|null $filter): array|null { if ($filter === null) { return null; @@ -32,10 +31,7 @@ public function echoNullableFilters(?Filter $filter): ?array return $this->echoFilters($filter); } - /** - * @Query() - * @return string - */ + #[Query] public function echoResolveInfo(ResolveInfo $info): string { return $info->fieldName; diff --git a/tests/Fixtures/Integration/Controllers/PostController.php b/tests/Fixtures/Integration/Controllers/PostController.php index 1390069187..8df4ae2179 100644 --- a/tests/Fixtures/Integration/Controllers/PostController.php +++ b/tests/Fixtures/Integration/Controllers/PostController.php @@ -10,26 +10,28 @@ class PostController { /** - * @Mutation() * @param Post $post * * @return Post */ + #[Mutation] public function createPost(Post $post): Post { return $post; } /** - * @Mutation() - * @UseInputType(for="$post", inputType="UpdatePostInput") * * @param int $id * @param Post $post * * @return Post */ - public function updatePost(int $id, Post $post): Post + #[Mutation] + public function updatePost( + int $id, + #[UseInputType('UpdatePostInput')] + Post $post): Post { $post->id = $id; diff --git a/tests/Fixtures/Integration/Controllers/PreferencesController.php b/tests/Fixtures/Integration/Controllers/PreferencesController.php index a31f21900b..599ec12b4c 100644 --- a/tests/Fixtures/Integration/Controllers/PreferencesController.php +++ b/tests/Fixtures/Integration/Controllers/PreferencesController.php @@ -8,11 +8,11 @@ class PreferencesController { /** - * @Mutation() * @param Preferences $preferences * * @return Preferences */ + #[Mutation] public function updatePreferences(Preferences $preferences): Preferences { return $preferences; diff --git a/tests/Fixtures/Integration/Controllers/ProductController.php b/tests/Fixtures/Integration/Controllers/ProductController.php index 75779b10ca..65b1c42408 100644 --- a/tests/Fixtures/Integration/Controllers/ProductController.php +++ b/tests/Fixtures/Integration/Controllers/ProductController.php @@ -1,16 +1,15 @@ setName("Special","box"); + $product->setName('Special', 'box'); $product->price = 11.99; $product->multi = 11.11; return $product; } - /** - * @Mutation() - * @UseInputType(for="$product", inputType="UpdateTrickyProductInput!") - * - * @param TrickyProduct $product - * @return TrickyProduct - */ - public function updateTrickyProduct(TrickyProduct $product): TrickyProduct + #[Mutation] + public function updateTrickyProduct( + #[UseInputType('UpdateTrickyProductInput!')] + TrickyProduct $product, + ): TrickyProduct { return $product; } diff --git a/tests/Fixtures/Integration/Controllers/SecurityController.php b/tests/Fixtures/Integration/Controllers/SecurityController.php index 27c09b7b03..47c9dc294e 100644 --- a/tests/Fixtures/Integration/Controllers/SecurityController.php +++ b/tests/Fixtures/Integration/Controllers/SecurityController.php @@ -1,82 +1,65 @@ bar; } diff --git a/tests/Fixtures/Integration/Models/Article.php b/tests/Fixtures/Integration/Models/Article.php index 0cf0f3a80d..49d914bc0e 100644 --- a/tests/Fixtures/Integration/Models/Article.php +++ b/tests/Fixtures/Integration/Models/Article.php @@ -6,22 +6,15 @@ use TheCodingMachine\GraphQLite\Annotations\Input; use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type() - * @Input() - */ + +#[Type, Input] class Article extends Post { - /** - * @Field(for="Article") - * @var int - */ - public $id = 2; - /** - * @Field() - * @var string|null - */ - public $magazine = null; + #[Field(for: "Article")] + public int $id = 2; + + #[Field] + public ?string $magazine = null; } diff --git a/tests/Fixtures/Integration/Models/Button.php b/tests/Fixtures/Integration/Models/Button.php index 4685ac00a3..0e1a024caa 100644 --- a/tests/Fixtures/Integration/Models/Button.php +++ b/tests/Fixtures/Integration/Models/Button.php @@ -7,52 +7,26 @@ use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type - */ +#[Type] class Button { - /** - * @var Color - */ - private $color; - - /** - * @var Size - */ - private $size; - - /** - * @var Position - */ - private $state; - - public function __construct(Color $color, Size $size, Position $state) + public function __construct(private Color $color, private Size $size, private Position $state) { - $this->color = $color; - $this->size = $size; - $this->state = $state; } - /** - * @Field - */ + #[Field] public function getColor(): Color { return $this->color; } - /** - * @Field - */ + #[Field] public function getSize(): Size { return $this->size; } - /** - * @Field - */ + #[Field] public function getState(): Position { return $this->state; diff --git a/tests/Fixtures/Integration/Models/Contact.php b/tests/Fixtures/Integration/Models/Contact.php index 58d12549dd..6fdc1f344d 100644 --- a/tests/Fixtures/Integration/Models/Contact.php +++ b/tests/Fixtures/Integration/Models/Contact.php @@ -1,122 +1,72 @@ name = $name; } public function getName() @@ -124,70 +74,49 @@ public function getName() return $this->name; } - /** - * @deprecated use field `name` - */ + /** @deprecated use field `name` */ public function getDeprecatedName() { return $this->name; } - public function getManager(): ?Contact + public function getManager(): Contact|null { return $this->manager; } - /** - * @param Contact|null $manager - */ - public function setManager(?Contact $manager): void + public function setManager(Contact|null $manager): void { $this->manager = $manager; } - /** - * @return Contact[] - */ + /** @return Contact[] */ public function getRelations(): array { return $this->relations; } - /** - * @param Contact[] $relations - */ + /** @param Contact[] $relations */ public function setRelations(array $relations): void { $this->relations = $relations; } - /** - * @return UploadedFileInterface - */ public function getPhoto(): UploadedFileInterface { return $this->photo; } - /** - * @param UploadedFileInterface $photo - */ public function setPhoto(UploadedFileInterface $photo): void { $this->photo = $photo; } - /** - * @return DateTimeInterface - */ - public function getBirthDate() + public function getBirthDate(): DateTimeInterface { return $this->birthDate; } - /** - * @param DateTimeInterface $birthDate - */ public function setBirthDate(DateTimeInterface $birthDate): void { $this->birthDate = $birthDate; @@ -195,31 +124,25 @@ public function setBirthDate(DateTimeInterface $birthDate): void /** * This getter will be overridden in the extend class. - * - * @Field() - * @return string */ + #[Field] public function getCompany(): string { return $this->company; } - /** - * @param string $company - */ public function setCompany(string $company): void { $this->company = $company; } - /** - * @Field() - */ - public function repeatInnerName(#[Prefetch('prefetchTheContacts')] $data): string + #[Field] + public function repeatInnerName(#[Prefetch('prefetchTheContacts')] + $data,): string { $index = array_search($this, $data, true); if ($index === false) { - throw new \RuntimeException('Index not found'); + throw new RuntimeException('Index not found'); } return $data[$index]->getName(); } @@ -229,76 +152,60 @@ public static function prefetchTheContacts(iterable $contacts) return $contacts; } - /** - * @Field() - * @Logged() - * @return string - */ + #[Field] + + #[Logged] public function onlyLogged(): string { return 'you can see this only if you are logged'; } - /** - * @Field() - * @Right(name="CAN_SEE_SECRET") - * @return string - */ + #[Field] + + #[Right('CAN_SEE_SECRET')] public function secret(): string { return 'you can see this only if you have the good right'; } - /** - * @return int - */ public function getAge(): int { return $this->age; } - /** - * @return string - */ public function getStatus(): string { return 'bar'; } - /** - * @return string - */ public function getZipcode(string $foo): string { return $this->zipcode; } - /** - * @return string - */ private function getAddress(): string { return $this->address; } - /** - * @Field() - * @Autowire(for="testService", identifier="testService") - * @Autowire(for="$otherTestService") - * @return string - */ - public function injectService(string $testService, stdClass $otherTestService = null): string + #[Field] + public function injectService( + #[Autowire(identifier: 'testService')] + string $testService, + #[Autowire] + stdClass|null $otherTestService = null, + ): string { if ($testService !== 'foo') { return 'KO'; } - if (!$otherTestService instanceof stdClass) { + if (! $otherTestService instanceof stdClass) { return 'KO'; } return 'OK'; } - public function injectServiceFromExternal(string $testService, string $testSkip = "foo", string $id = '42'): string + public function injectServiceFromExternal(string $testService, string $testSkip = 'foo', string $id = '42'): string { if ($testService !== 'foo') { return 'KO'; diff --git a/tests/Fixtures/Integration/Models/Filter.php b/tests/Fixtures/Integration/Models/Filter.php index e482c7e3fe..0acf5dab20 100644 --- a/tests/Fixtures/Integration/Models/Filter.php +++ b/tests/Fixtures/Integration/Models/Filter.php @@ -34,8 +34,8 @@ public function mergeValues(array $values): void /** * @param string[] $values * - * @Factory() */ + #[Factory] public static function create(array $values = []): self { return new self($values); diff --git a/tests/Fixtures/Integration/Models/Post.php b/tests/Fixtures/Integration/Models/Post.php index ae91b13ae0..758931f447 100644 --- a/tests/Fixtures/Integration/Models/Post.php +++ b/tests/Fixtures/Integration/Models/Post.php @@ -1,5 +1,7 @@ title = $title; $this->description = 'bar'; } - /** - * @return string|null - */ - public function getDescription(): ?string + public function getDescription(): string|null { return $this->description; } - /** - * @param string|null $description - */ - public function setDescription(?string $description): void + public function setDescription(string|null $description): void { $this->description = $description; } - /** - * @param string|null $summary - */ - public function setSummary(?string $summary): void + public function setSummary(string|null $summary): void { $this->summary = $summary; } - /** - * @param string $inaccessible - */ private function setInaccessible(string $inaccessible): void { $this->inaccessible = $inaccessible; diff --git a/tests/Fixtures/Integration/Models/Preferences.php b/tests/Fixtures/Integration/Models/Preferences.php index 9f37a247cd..499190a235 100644 --- a/tests/Fixtures/Integration/Models/Preferences.php +++ b/tests/Fixtures/Integration/Models/Preferences.php @@ -7,35 +7,23 @@ use TheCodingMachine\GraphQLite\Annotations\Input; use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type() - * @Input() - */ +#[Type, Input] class Preferences { - /** - * @Field(inputType="Int!") - * @var int - */ - private $id; + #[Field(inputType: "Int!")] + private int $id; /** - * @Field(inputType="[String!]!") * @var string[] */ - private $options; + #[Field(inputType: "[String!]!")] + private array $options; - /** - * @Field(inputType="Boolean!") - * @var bool - */ - private $enabled; + #[Field(inputType: "Boolean!")] + private bool $enabled; - /** - * @Field(inputType="String!") - * @var string - */ - private $name; + #[Field(inputType: "String!")] + private string $name; public function __construct(int $id, array $options, bool $enabled, string $name) { diff --git a/tests/Fixtures/Integration/Models/Product.php b/tests/Fixtures/Integration/Models/Product.php index e9479c3d90..ee98fc610f 100644 --- a/tests/Fixtures/Integration/Models/Product.php +++ b/tests/Fixtures/Integration/Models/Product.php @@ -1,11 +1,9 @@ name = $name; - $this->price = $price; - $this->type = $type; } - /** - * @Field(name="name") - * @return string - */ + #[Field(name: 'name')] public function getName(): string { return $this->name; } - /** - * @Field() - * @return float - */ + #[Field] public function getPrice(): float { return $this->price; } - /** - * @Field() - * @Right("YOU_DONT_HAVE_THIS_RIGHT") - * @FailWith(null) - * @return string - */ + #[Field] + #[Right('YOU_DONT_HAVE_THIS_RIGHT')] + #[FailWith(null)] public function getUnauthorized(): string { return 'You are not allowed to see this'; } - /** - * @return ProductTypeEnum - */ public function getType(): ProductTypeEnum { return $this->type; } - /** - * @Factory() - * @return Product - */ - public static function create(string $name, float $price, ProductTypeEnum $type = null): self + #[Factory] + public static function create(string $name, float $price, ProductTypeEnum|null $type = null): self { return new self($name, $price, $type); } - /** - * @Field() - * @Security("this.isAllowed(secret)") - */ + #[Field] + #[Security(expression: 'this.isAllowed(secret)')] public function getMargin(string $secret): float { return 12.0; diff --git a/tests/Fixtures/Integration/Models/ProductTypeEnum.php b/tests/Fixtures/Integration/Models/ProductTypeEnum.php index 6bc1983700..57cd6f2d2e 100644 --- a/tests/Fixtures/Integration/Models/ProductTypeEnum.php +++ b/tests/Fixtures/Integration/Models/ProductTypeEnum.php @@ -4,10 +4,7 @@ use MyCLabs\Enum\Enum; use TheCodingMachine\GraphQLite\Annotations\EnumType; - -/** - * @EnumType(name="ProductTypes") - */ +#[EnumType(name: "ProductTypes")] class ProductTypeEnum extends Enum { const FOOD = 'food'; diff --git a/tests/Fixtures/Integration/Models/SpecialProduct.php b/tests/Fixtures/Integration/Models/SpecialProduct.php index 7c78ce908f..bf162ccc51 100644 --- a/tests/Fixtures/Integration/Models/SpecialProduct.php +++ b/tests/Fixtures/Integration/Models/SpecialProduct.php @@ -1,57 +1,34 @@ name = $name; - $this->price = $price; } - /** - * @Field() - * @return string - */ + #[Field] public function getSpecial(): string { - return "unicorn"; + return 'unicorn'; } - /** - * @Field() - * @return string - */ + #[Field] public function getName(): string { return $this->name; } - /** - * @Field() - * @return float - */ + #[Field] public function getPrice(): float { return $this->price; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Integration/Models/TrickyProduct.php b/tests/Fixtures/Integration/Models/TrickyProduct.php index 203cefda58..4003568168 100644 --- a/tests/Fixtures/Integration/Models/TrickyProduct.php +++ b/tests/Fixtures/Integration/Models/TrickyProduct.php @@ -1,123 +1,86 @@ name; } - /** - * @return float - */ public function getPrice(): float { return $this->price; } - /** - * @Field() - * @Autowire(for="testService", identifier="testService") - * @param string $name - * @param string $testService - * @return void - */ - public function setName(string $name, string $testService): void + #[Field] + public function setName( + string $name, + #[Autowire(identifier: 'testService')] + string $testService, + ): void { - $this->name = $name . " " . $testService; + $this->name = $name . ' ' . $testService; } - /** - * @Field() - * @Right("CAN_SEE_SECRET") - * @return string - */ + #[Field] + + #[Right('CAN_SEE_SECRET')] public function getSecret(): string { return $this->secret; } - /** - * @Field() - * @Right("CAN_SET_SECRET") - * @param string $secret - */ + #[Field] + + #[Right('CAN_SEE_SECRET')] public function setSecret(string $secret): void { $this->secret = $secret; } - /** - * @Field() - * @Security("conditionalSecret == 'actually{secret}'") - * @Security("user && user.bar == 42") - * @param string $conditionalSecret - */ + #[Field] + #[Security("conditionalSecret == 'actually{secret}'")] + #[Security('user && user.bar == 42')] public function setConditionalSecret(string $conditionalSecret): void { $this->conditionalSecret = $conditionalSecret; } - /** - * @Field() - * @Security("this.isAllowed(key)") - */ + #[Field] + #[Security('this.isAllowed(key)')] public function getConditionalSecret(int $key): string { return $this->conditionalSecret; @@ -127,4 +90,4 @@ public function isAllowed(string $conditionalSecret): bool { return $conditionalSecret === '1234'; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Integration/Models/User.php b/tests/Fixtures/Integration/Models/User.php index f4ca921770..82bd2303be 100644 --- a/tests/Fixtures/Integration/Models/User.php +++ b/tests/Fixtures/Integration/Models/User.php @@ -1,31 +1,21 @@ email = $email; } - /** - * @Field(name="email") - * @return string - */ + #[Field] public function getEmail(): string { return $this->email; diff --git a/tests/Fixtures/Integration/Types/ContactFactory.php b/tests/Fixtures/Integration/Types/ContactFactory.php index 782394bdd4..e3802f503a 100644 --- a/tests/Fixtures/Integration/Types/ContactFactory.php +++ b/tests/Fixtures/Integration/Types/ContactFactory.php @@ -1,9 +1,9 @@ getName()); diff --git a/tests/Fixtures/Integration/Types/ContactType.php b/tests/Fixtures/Integration/Types/ContactType.php index 33ff5449f9..2881e50ccf 100644 --- a/tests/Fixtures/Integration/Types/ContactType.php +++ b/tests/Fixtures/Integration/Types/ContactType.php @@ -1,70 +1,65 @@ getName()); + return $prefix . ' ' . strtoupper($contact->getName()); } - /** - * @Field() - */ - public function repeatName(Contact $contact, #[Prefetch('prefetchContacts')] $data, string $suffix): string + #[Field] + public function repeatName(Contact $contact, #[Prefetch('prefetchContacts')] + $data, string $suffix,): string { $index = array_search($contact, $data['contacts'], true); if ($index === false) { - throw new \RuntimeException('Index not found'); + throw new RuntimeException('Index not found'); } - return $data['prefix'].$data['contacts'][$index]->getName().$suffix; + return $data['prefix'] . $data['contacts'][$index]->getName() . $suffix; } public static function prefetchContacts(iterable $contacts, string $prefix) { return [ 'contacts' => $contacts, - 'prefix' => $prefix + 'prefix' => $prefix, ]; } - /** - * - * @return Post[]|null - */ + /** @return Post[]|null */ #[Field] public function getPosts( Contact $contact, #[Prefetch('prefetchPosts')] - $posts - ): ?array { + $posts, + ): array|null { return $posts[$contact->getName()] ?? null; } @@ -74,10 +69,10 @@ public static function prefetchPosts(iterable $contacts): array foreach ($contacts as $contact) { $contactPost = array_filter( self::getContactPosts(), - fn(Post $post) => $post->author?->getName() === $contact->getName() + static fn (Post $post) => $post->author?->getName() === $contact->getName(), ); - if (!$contactPost) { + if (! $contactPost) { continue; } @@ -90,15 +85,15 @@ public static function prefetchPosts(iterable $contacts): array private static function getContactPosts(): array { return [ - self::generatePost('First Joe post', '1', new Contact('Joe')), - self::generatePost('First Bill post', '2', new Contact('Bill')), - self::generatePost('First Kate post', '3', new Contact('Kate')), + self::generatePost('First Joe post', 1, new Contact('Joe')), + self::generatePost('First Bill post', 2, new Contact('Bill')), + self::generatePost('First Kate post', 3, new Contact('Kate')), ]; } private static function generatePost( string $title, - string $id, + int $id, Contact $author, ): Post { $post = new Post($title); diff --git a/tests/Fixtures/Integration/Types/ExtendedContactOtherType.php b/tests/Fixtures/Integration/Types/ExtendedContactOtherType.php index 483689ffde..264f1babc5 100644 --- a/tests/Fixtures/Integration/Types/ExtendedContactOtherType.php +++ b/tests/Fixtures/Integration/Types/ExtendedContactOtherType.php @@ -1,27 +1,21 @@ getName()); } - /** - * @Field() - * @deprecated use field `uppercaseName` - */ + /** @deprecated use field `uppercaseName` */ + #[Field] public function deprecatedUppercaseName(Contact $contact): string { return strtoupper($contact->getName()); @@ -34,12 +28,10 @@ public function deprecatedUppercaseName(Contact $contact): string /** * Here, we are testing overriding the field in the extend class. - * - * @Field() - * @return string */ + #[Field] public function company(Contact $contact): string { - return $contact->getName().' Ltd'; + return $contact->getName() . ' Ltd'; } } diff --git a/tests/Fixtures/Integration/Types/FilterDecorator.php b/tests/Fixtures/Integration/Types/FilterDecorator.php index 305e6d2140..76cca38f7f 100644 --- a/tests/Fixtures/Integration/Types/FilterDecorator.php +++ b/tests/Fixtures/Integration/Types/FilterDecorator.php @@ -1,49 +1,37 @@ mergeValues($moreValues); return $filter; } - /** - * @Decorate(inputTypeName="FilterInput") - * @param Filter $filter - * @param int[] $evenMoreValues - * @return Filter - */ + /** @param int[] $evenMoreValues */ + #[Decorate(inputTypeName: 'FilterInput')] public static function staticDecorate(Filter $filter, array $evenMoreValues = []): Filter { $filter->mergeValues($evenMoreValues); return $filter; } - /** - * @Decorate(inputTypeName="FilterInput") - * @UseInputType(for="innerFilter", inputType="FilterInput") - * @param Filter $filter - * @param Filter|null $innerFilter - * @return Filter - */ - public static function recursiveDecorate(Filter $filter, ?Filter $innerFilter = null): Filter + #[Decorate(inputTypeName: 'FilterInput')] + public static function recursiveDecorate( + Filter $filter, + #[UseInputType(inputType: 'FilterInput')] + Filter|null $innerFilter = null, + ): Filter { return $filter; } diff --git a/tests/Fixtures/Interfaces/ClassA.php b/tests/Fixtures/Interfaces/ClassA.php index 951efa83ec..772174b646 100644 --- a/tests/Fixtures/Interfaces/ClassA.php +++ b/tests/Fixtures/Interfaces/ClassA.php @@ -1,22 +1,17 @@ foo = $foo; } public function getFoo(): string { return $this->foo; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Interfaces/ClassB.php b/tests/Fixtures/Interfaces/ClassB.php index ffe72d3fde..8371cb11d5 100644 --- a/tests/Fixtures/Interfaces/ClassB.php +++ b/tests/Fixtures/Interfaces/ClassB.php @@ -1,20 +1,14 @@ bar = $bar; } public function getBar(): string diff --git a/tests/Fixtures/Interfaces/Types/ClassAType.php b/tests/Fixtures/Interfaces/Types/ClassAType.php index 615ac51204..ea12b01533 100644 --- a/tests/Fixtures/Interfaces/Types/ClassAType.php +++ b/tests/Fixtures/Interfaces/Types/ClassAType.php @@ -1,5 +1,6 @@ getTest(); } - return new TestObject($string.$int.$str.($boolean?'true':'false').$float.$dateTimeImmutable->format('YmdHis').$dateTime->format('YmdHis').$withDefault.($id !== null ? $id->val() : '').$enum->getValue()); + return new TestObject($string . $int . $str . ($boolean ? 'true' : 'false') . $float . $dateTimeImmutable->format('YmdHis') . $dateTime->format('YmdHis') . $withDefault . ($id?->val() ?? '') . $enum->getValue()); } #[Query] @@ -50,7 +52,7 @@ public function testLogged(): TestObject } #[Query] - #[Right(name: "CAN_FOO")] + #[Right(name: 'CAN_FOO')] #[HideIfUnauthorized] public function testRight(): TestObject { @@ -69,47 +71,36 @@ public function testNameFromAnnotation(): TestObject return new TestObject('foo'); } - /** - * @return ArrayObject|TestObject[] - */ + /** @return ArrayObject|TestObject[] */ #[Query(name: 'arrayObject')] public function testArrayObject(): ArrayObject { return new ArrayObject([]); } - /** - * @return ArrayObject - */ + /** @return ArrayObject */ #[Query(name: 'arrayObjectGeneric')] public function testArrayObjectGeneric(): ArrayObject { return new ArrayObject([]); } - /** - * @return iterable|TestObject[] - */ + /** @return iterable|TestObject[] */ #[Query(name: 'iterable')] public function testIterable(): iterable { - return array(); + return []; } - /** - * @return iterable - */ + /** @return iterable */ #[Query(name: 'iterableGeneric')] public function testIterableGeneric(): iterable { - return array(); + return []; } - /** - * @return TestObject|TestObject2 - */ #[Query(name: 'union')] - public function testUnion() + public function testUnion(): TestObject|TestObject2 { return new TestObject2('foo'); } @@ -133,9 +124,11 @@ public function testReturn(TestObject $testObject): TestObject #[Subscription(outputType: 'ID')] public function testSubscribe(): void - {} + { + } #[Subscription(outputType: 'ID')] public function testSubscribeWithInput(TestObject $testObject): void - {} + { + } } diff --git a/tests/Fixtures/TestControllerNoReturnType.php b/tests/Fixtures/TestControllerNoReturnType.php index 1823c24453..9e9a3a4736 100644 --- a/tests/Fixtures/TestControllerNoReturnType.php +++ b/tests/Fixtures/TestControllerNoReturnType.php @@ -1,19 +1,14 @@ $params * @return array */ + #[Query] public function test(array $params): array { return $params; diff --git a/tests/Fixtures/TestControllerWithParamDateTime.php b/tests/Fixtures/TestControllerWithParamDateTime.php index c3cfb33fc7..6233a8f30a 100644 --- a/tests/Fixtures/TestControllerWithParamDateTime.php +++ b/tests/Fixtures/TestControllerWithParamDateTime.php @@ -1,5 +1,6 @@ test = $test; - $this->testBool = $testBool; } /** * This is a test summary - * @return string */ public function getTest(): string { return $this->test; } - /** - * @return bool - */ public function isTestBool(): bool { return $this->testBool; } - /** - * @return ?string - */ - public function testRight() + public function testRight(): string|null { - return "foo"; + return 'foo'; } public function getSibling(self $foo): self diff --git a/tests/Fixtures/TestObject2.php b/tests/Fixtures/TestObject2.php index 09ece4f89b..f0dd249db4 100644 --- a/tests/Fixtures/TestObject2.php +++ b/tests/Fixtures/TestObject2.php @@ -1,23 +1,15 @@ test2 = $test2; } - /** - * @return string - */ public function getTest2(): string { return $this->test2; diff --git a/tests/Fixtures/TestSelfType.php b/tests/Fixtures/TestSelfType.php index 5e967b89c0..2c725517b0 100644 --- a/tests/Fixtures/TestSelfType.php +++ b/tests/Fixtures/TestSelfType.php @@ -6,10 +6,8 @@ use TheCodingMachine\GraphQLite\Annotations\SourceField; use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type() - * @SourceField(name="test") - */ +#[Type] +#[SourceField(name: 'test')] class TestSelfType { private $foo = 'foo'; diff --git a/tests/Fixtures/TestSourceFieldBadOutputType.php b/tests/Fixtures/TestSourceFieldBadOutputType.php index 42b5cabc83..edf613f39d 100644 --- a/tests/Fixtures/TestSourceFieldBadOutputType.php +++ b/tests/Fixtures/TestSourceFieldBadOutputType.php @@ -1,15 +1,14 @@ foo = $foo; - $this->bar = $bar; } public function __get($name) diff --git a/tests/Fixtures/TestSourceNameType.php b/tests/Fixtures/TestSourceNameType.php index ddfbaa9a2f..2cf0b39009 100644 --- a/tests/Fixtures/TestSourceNameType.php +++ b/tests/Fixtures/TestSourceNameType.php @@ -1,17 +1,16 @@ getTest().$param; + return $test->getTest() . $param; } } diff --git a/tests/Fixtures/TestTypeId.php b/tests/Fixtures/TestTypeId.php index 626928afd3..74a7ae7257 100644 --- a/tests/Fixtures/TestTypeId.php +++ b/tests/Fixtures/TestTypeId.php @@ -1,15 +1,14 @@ getTest().$arg1; + return $test->getTest() . $arg1; } } diff --git a/tests/Fixtures/TestTypeWithFailWith.php b/tests/Fixtures/TestTypeWithFailWith.php index ad362e1f55..0d008c4d59 100644 --- a/tests/Fixtures/TestTypeWithFailWith.php +++ b/tests/Fixtures/TestTypeWithFailWith.php @@ -1,18 +1,16 @@ 'test']), + new SourceField(['name' => 'test']), ]; } } diff --git a/tests/Fixtures/TestTypeWithSourceFieldInvalidParameterAnnotation.php b/tests/Fixtures/TestTypeWithSourceFieldInvalidParameterAnnotation.php index 71dd3b33a2..2cc4b67c9a 100644 --- a/tests/Fixtures/TestTypeWithSourceFieldInvalidParameterAnnotation.php +++ b/tests/Fixtures/TestTypeWithSourceFieldInvalidParameterAnnotation.php @@ -1,16 +1,15 @@ getTest().$param; + return $test->getTest() . $param; } } diff --git a/tests/Fixtures/Types/FooExtendType.php b/tests/Fixtures/Types/FooExtendType.php index e462b90a1b..0ca423cf5b 100644 --- a/tests/Fixtures/Types/FooExtendType.php +++ b/tests/Fixtures/Types/FooExtendType.php @@ -1,23 +1,19 @@ getTest()); diff --git a/tests/Fixtures/Types/FooType.php b/tests/Fixtures/Types/FooType.php index 22b8c4d55b..c8ab91a854 100644 --- a/tests/Fixtures/Types/FooType.php +++ b/tests/Fixtures/Types/FooType.php @@ -3,13 +3,10 @@ namespace TheCodingMachine\GraphQLite\Fixtures\Types; -use TheCodingMachine\GraphQLite\Annotations\Right; -use TheCodingMachine\GraphQLite\Annotations\SourceField; use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Fixtures\TestObject; -/** - * @Type(class=TheCodingMachine\GraphQLite\Fixtures\TestObject::class) - */ +#[Type(class: TestObject::class)] class FooType extends AbstractFooType { } diff --git a/tests/Fixtures/Types/TestFactory.php b/tests/Fixtures/Types/TestFactory.php index 77e22f97a9..5d619b9a9e 100644 --- a/tests/Fixtures/Types/TestFactory.php +++ b/tests/Fixtures/Types/TestFactory.php @@ -1,42 +1,37 @@ format('Y-m-d').'-'.implode('-', $stringList).'-'.count($dateList)); + return new TestObject2($date->format('Y-m-d') . '-' . implode('-', $stringList) . '-' . count($dateList)); } - /** - * @Decorate("InputObject") - */ + #[Decorate('InputObject')] public function myDecorator(TestObject $testObject, int $int): TestObject { return $testObject; diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index fccd26d937..ca9eb58033 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -4,7 +4,6 @@ namespace TheCodingMachine\GraphQLite\Integration; -use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index d363393d5f..06db73cce9 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -2,7 +2,6 @@ namespace TheCodingMachine\GraphQLite\Integration; -use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use Kcs\ClassFinder\Finder\ComposerFinder; @@ -282,7 +281,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf ); }, AnnotationReader::class => static function (ContainerInterface $container) { - return new AnnotationReader(new DoctrineAnnotationReader()); + return new AnnotationReader(); }, NamingStrategyInterface::class => static function () { return new NamingStrategy(); diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/GlobTypeMapperTest.php index ce8ab0aad4..e7e12053ae 100644 --- a/tests/Mappers/GlobTypeMapperTest.php +++ b/tests/Mappers/GlobTypeMapperTest.php @@ -1,13 +1,17 @@ function () { + FooType::class => static function () { return new FooType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); @@ -43,7 +46,7 @@ public function testGlobTypeMapper(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertSame([TestObject::class], $mapper->getSupportedClasses()); $this->assertTrue($mapper->canMapClassToType(TestObject::class)); @@ -53,25 +56,25 @@ public function testGlobTypeMapper(): void $this->assertFalse($mapper->canMapNameToType('NotExists')); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertTrue($anotherMapperSameCache->canMapClassToType(TestObject::class)); $this->assertTrue($anotherMapperSameCache->canMapNameToType('Foo')); $this->expectException(CannotMapTypeException::class); - $mapper->mapClassToType(\stdClass::class, null); + $mapper->mapClassToType(stdClass::class, null); } public function testGlobTypeMapperDuplicateTypesException(): void { $container = new LazyContainer([ - TestType::class => function () { + TestType::class => static function () { return new TestType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); $this->expectException(DuplicateMappingException::class); $mapper->canMapClassToType(TestType::class); @@ -80,14 +83,14 @@ public function testGlobTypeMapperDuplicateTypesException(): void public function testGlobTypeMapperDuplicateInputsException(): void { $container = new LazyContainer([ - TestInput::class => function () { + TestInput::class => static function () { return new TestInput(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputs'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputs'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); $this->expectException(DuplicateMappingException::class); $mapper->canMapClassToInputType(TestInput::class); @@ -103,18 +106,20 @@ public function testGlobTypeMapperDuplicateInputTypesException(): void $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); $caught = false; try { $mapper->canMapClassToInputType(TestObject::class); } catch (DuplicateMappingException $e) { // Depending on the environment, one of the messages can be returned. - $this->assertContains($e->getMessage(), + $this->assertContains( + $e->getMessage(), [ 'The class \'TheCodingMachine\GraphQLite\Fixtures\TestObject\' should be mapped to only one GraphQL Input type. Two methods are pointing via the @Factory annotation to this class: \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory::myFactory\' and \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory2::myFactory\'', - 'The class \'TheCodingMachine\GraphQLite\Fixtures\TestObject\' should be mapped to only one GraphQL Input type. Two methods are pointing via the @Factory annotation to this class: \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory2::myFactory\' and \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory::myFactory\'' - ]); + 'The class \'TheCodingMachine\GraphQLite\Fixtures\TestObject\' should be mapped to only one GraphQL Input type. Two methods are pointing via the @Factory annotation to this class: \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory2::myFactory\' and \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory::myFactory\'', + ], + ); $caught = true; } $this->assertTrue($caught, 'DuplicateMappingException is thrown'); @@ -123,14 +128,14 @@ public function testGlobTypeMapperDuplicateInputTypesException(): void public function testGlobTypeMapperInheritedInputTypesException(): void { $container = new LazyContainer([ - ChildTestFactory::class => function() { + ChildTestFactory::class => static function () { return new ChildTestFactory(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\InheritedInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\InheritedInputTypes'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); //$this->expectException(DuplicateMappingException::class); //$this->expectExceptionMessage('The class \'TheCodingMachine\GraphQLite\Fixtures\TestObject\' should be mapped to only one GraphQL Input type. Two methods are pointing via the @Factory annotation to this class: \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory::myFactory\' and \'TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes\TestFactory2::myFactory\''); @@ -141,31 +146,31 @@ public function testGlobTypeMapperInheritedInputTypesException(): void public function testGlobTypeMapperClassNotFoundException(): void { $container = new LazyContainer([ - TestType::class => function () { + TestType::class => static function () { return new TestType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadClassType'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadClassType'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); $this->expectException(ClassNotFoundException::class); - $this->expectExceptionMessage("Could not autoload class 'Foobar' defined in @Type annotation of class 'TheCodingMachine\\GraphQLite\\Fixtures\\BadClassType\\TestType'"); + $this->expectExceptionMessage("Could not autoload class 'Foobar' defined in #[Type] attribute of class 'TheCodingMachine\\GraphQLite\\Fixtures\\BadClassType\\TestType'"); $mapper->canMapClassToType(TestType::class); } public function testGlobTypeMapperNameNotFoundException(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new NullAdapter())); $this->expectException(CannotMapTypeException::class); $mapper->mapNameToType('NotExists', $this->getTypeMapper()); @@ -174,19 +179,19 @@ public function testGlobTypeMapperNameNotFoundException(): void public function testGlobTypeMapperInputType(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); }, - TestFactory::class => function () { + TestFactory::class => static function () { return new TestFactory(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertTrue($mapper->canMapClassToInputType(TestObject::class)); @@ -195,12 +200,11 @@ public function testGlobTypeMapperInputType(): void $this->assertSame('TestObjectInput', $inputType->name); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertTrue($anotherMapperSameCache->canMapClassToInputType(TestObject::class)); $this->assertSame('TestObjectInput', $anotherMapperSameCache->mapClassToInputType(TestObject::class, $this->getTypeMapper())->name); - $this->expectException(CannotMapTypeException::class); $mapper->mapClassToInputType(TestType::class, $this->getTypeMapper()); } @@ -208,12 +212,12 @@ public function testGlobTypeMapperInputType(): void public function testGlobTypeMapperExtend(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); }, - FooExtendType::class => function () { + FooExtendType::class => static function () { return new FooExtendType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); @@ -221,7 +225,7 @@ public function testGlobTypeMapperExtend(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $type = $mapper->mapClassToType(TestObject::class, null); @@ -232,12 +236,12 @@ public function testGlobTypeMapperExtend(): void $this->assertFalse($mapper->canExtendTypeForName('NotExists', $type)); // Again to test cache - $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $anotherMapperSameCache = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertTrue($anotherMapperSameCache->canExtendTypeForClass(TestObject::class, $type)); $this->assertTrue($anotherMapperSameCache->canExtendTypeForName('TestObject', $type)); $this->expectException(CannotMapTypeException::class); - $mapper->extendTypeForClass(\stdClass::class, $type); + $mapper->extendTypeForClass(stdClass::class, $type); } public function testEmptyGlobTypeMapper(): void @@ -249,7 +253,7 @@ public function testEmptyGlobTypeMapper(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->assertSame([], $mapper->getSupportedClasses()); } @@ -257,9 +261,9 @@ public function testEmptyGlobTypeMapper(): void public function testGlobTypeMapperDecorate(): void { $container = new LazyContainer([ - FilterDecorator::class => function () { + FilterDecorator::class => static function () { return new FilterDecorator(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); @@ -267,9 +271,9 @@ public function testGlobTypeMapperDecorate(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration\Types'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); - $inputType = new MockResolvableInputObjectType(['name'=>'FilterInput']); + $inputType = new MockResolvableInputObjectType(['name' => 'FilterInput']); $mapper->decorateInputTypeForName('FilterInput', $inputType); @@ -283,14 +287,14 @@ public function testGlobTypeMapperDecorate(): void public function testInvalidName(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new ArrayAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), new Psr16Cache(new ArrayAdapter())); $this->assertFalse($mapper->canExtendTypeForName('{}()/\\@:', new MutableObjectType(['name' => 'foo']))); $this->assertFalse($mapper->canDecorateInputTypeForName('{}()/\\@:', new MockResolvableInputObjectType(['name' => 'foo']))); @@ -300,13 +304,13 @@ public function testInvalidName(): void public function testGlobTypeMapperExtendBadName(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); }, - FooExtendType::class => function () { + FooExtendType::class => static function () { return new FooExtendType(); }, - BadExtendType::class => function () { + BadExtendType::class => static function () { return new BadExtendType(); }, ]); @@ -316,7 +320,7 @@ public function testGlobTypeMapperExtendBadName(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $testObjectType = new MutableObjectType([ 'name' => 'TestObject', @@ -333,13 +337,13 @@ public function testGlobTypeMapperExtendBadName(): void public function testGlobTypeMapperExtendBadClass(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); }, - FooExtendType::class => function () { + FooExtendType::class => static function () { return new FooExtendType(); }, - BadExtendType2::class => function () { + BadExtendType2::class => static function () { return new BadExtendType2(); }, ]); @@ -349,7 +353,7 @@ public function testGlobTypeMapperExtendBadClass(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType2'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\BadExtendType2'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $testObjectType = new MutableObjectType([ 'name' => 'TestObject', @@ -366,9 +370,9 @@ public function testGlobTypeMapperExtendBadClass(): void public function testNonInstantiableType(): void { $container = new LazyContainer([ - FooType::class => function () { + FooType::class => static function () { return new FooType(); - } + }, ]); $typeGenerator = $this->getTypeGenerator(); @@ -376,7 +380,7 @@ public function testNonInstantiableType(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->expectException(GraphQLRuntimeException::class); $this->expectExceptionMessage('Class "TheCodingMachine\GraphQLite\Fixtures\NonInstantiableType\AbstractFooType" annotated with @Type(class="TheCodingMachine\GraphQLite\Fixtures\TestObject") must be instantiable.'); @@ -391,7 +395,7 @@ public function testNonInstantiableInput(): void $inputTypeGenerator = $this->getInputTypeGenerator(); $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->expectException(FailedResolvingInputType::class); $this->expectExceptionMessage("Class 'TheCodingMachine\GraphQLite\Fixtures\NonInstantiableInput\AbstractFoo' annotated with @Input must be instantiable."); diff --git a/tests/Mappers/Parameters/ContainerParameterMapperTest.php b/tests/Mappers/Parameters/ContainerParameterMapperTest.php index cbb261b42e..a33e8295ae 100644 --- a/tests/Mappers/Parameters/ContainerParameterMapperTest.php +++ b/tests/Mappers/Parameters/ContainerParameterMapperTest.php @@ -1,5 +1,7 @@ getRegistry()); - $refMethod = new ReflectionMethod(__CLASS__, 'dummy'); + $refMethod = new ReflectionMethod(self::class, 'dummy'); $parameter = $refMethod->getParameters()[0]; $this->expectException(MissingAutowireTypeException::class); $this->expectExceptionMessage('For parameter $foo in TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterMapperTest::dummy, annotated with annotation @Autowire, you must either provide a type-hint or specify the container identifier with @Autowire(identifier="my_service")'); - $mapper->mapParameter($parameter, - new DocBlock(), null, $this->getAnnotationReader()->getParameterAnnotations($parameter), new class implements ParameterHandlerInterface { - public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations): ParameterInterface + $mapper->mapParameter( + $parameter, + new DocBlock(), + null, + $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$parameter])['foo'], + new class implements ParameterHandlerInterface { + public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations): ParameterInterface { } - }); + }, + ); } - /** - * @Autowire(for="foo") - */ - private function dummy($foo) { - + private function dummy( + #[Autowire] + $foo, + ): void + { } } diff --git a/tests/Mappers/Parameters/TypeMapperTest.php b/tests/Mappers/Parameters/TypeMapperTest.php index dd1327dde5..825cdaacb0 100644 --- a/tests/Mappers/Parameters/TypeMapperTest.php +++ b/tests/Mappers/Parameters/TypeMapperTest.php @@ -1,8 +1,9 @@ getDocBlock($refMethod); $this->expectException(CannotMapTypeException::class); - $this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Mappers\Parameters\TypeMapperTest::dummy, in GraphQL, you can only use union types between objects. These types cannot be used in union types: Int!, String!'); + $this->expectExceptionMessage('For return type of TheCodingMachine\GraphQLite\Mappers\Parameters\TypeMapperTest::dummy, in GraphQL, you can only use union types between objects. These types cannot be used in union types: String!, Int!'); $typeMapper->mapReturnType($refMethod, $docBlockObj); } @@ -80,14 +83,13 @@ public function testMapObjectNullableUnionWorks(): void $gqType = $typeMapper->mapReturnType($refMethod, $docBlockObj); $this->assertNotInstanceOf(NonNull::class, $gqType); - assert(!($gqType instanceof NonNull)); + assert(! ($gqType instanceof NonNull)); $this->assertInstanceOf(UnionType::class, $gqType); assert($gqType instanceof UnionType); $unionTypes = $gqType->getTypes(); $this->assertEquals(2, count($unionTypes)); $this->assertEquals('TestObject', $unionTypes[0]->name); $this->assertEquals('TestObject2', $unionTypes[1]->name); - } public function testHideParameter(): void @@ -104,7 +106,7 @@ public function testHideParameter(): void $refMethod = new ReflectionMethod($this, 'withDefaultValue'); $refParameter = $refMethod->getParameters()[0]; $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); - $annotations = $this->getAnnotationReader()->getParameterAnnotations($refParameter); + $annotations = $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']; $param = $typeMapper->mapParameter($refParameter, $docBlockObj, null, $annotations); @@ -129,7 +131,7 @@ public function testParameterWithDescription(): void $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); $refParameter = $refMethod->getParameters()[0]; - $parameter = $typeMapper->mapParameter($refParameter, $docBlockObj, null, $this->getAnnotationReader()->getParameterAnnotations($refParameter)); + $parameter = $typeMapper->mapParameter($refParameter, $docBlockObj, null, $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']); $this->assertInstanceOf(InputTypeParameter::class, $parameter); assert($parameter instanceof InputTypeParameter); $this->assertEquals('Foo parameter', $parameter->getDescription()); @@ -149,7 +151,7 @@ public function testHideParameterException(): void $refMethod = new ReflectionMethod($this, 'withoutDefaultValue'); $refParameter = $refMethod->getParameters()[0]; $docBlockObj = $cachedDocBlockFactory->getDocBlock($refMethod); - $annotations = $this->getAnnotationReader()->getParameterAnnotations($refParameter); + $annotations = $this->getAnnotationReader()->getParameterAnnotationsPerParameter([$refParameter])['foo']; $this->expectException(CannotHideParameterRuntimeException::class); $this->expectExceptionMessage('For parameter $foo of method TheCodingMachine\GraphQLite\Mappers\Parameters\TypeMapperTest::withoutDefaultValue(), cannot use the @HideParameter annotation. The parameter needs to provide a default value.'); @@ -157,35 +159,22 @@ public function testHideParameterException(): void $typeMapper->mapParameter($refParameter, $docBlockObj, null, $annotations); } - /** - * @return int|string - */ - private function dummy() + private function dummy(): int|string { - } - /** - * @param int $foo Foo parameter - */ - private function withParamDescription(int $foo) + /** @param int $foo Foo parameter */ + private function withParamDescription(int $foo): void { - } - /** - * @HideParameter(for="$foo") - */ - private function withDefaultValue($foo = 24) + private function withDefaultValue(#[HideParameter] + $foo = 24,): void { - } - /** - * @HideParameter(for="$foo") - */ - private function withoutDefaultValue($foo) + private function withoutDefaultValue(#[HideParameter] + $foo,): void { - } } diff --git a/tests/Mappers/RecursiveTypeMapperTest.php b/tests/Mappers/RecursiveTypeMapperTest.php index 6d67e3bdd1..8fc14ee27c 100644 --- a/tests/Mappers/RecursiveTypeMapperTest.php +++ b/tests/Mappers/RecursiveTypeMapperTest.php @@ -1,26 +1,22 @@ 'Foobar' - ]); + $objectType = new MutableObjectType(['name' => 'Foobar']); $typeMapper = new StaticTypeMapper( - types: [ - ClassB::class => $objectType - ] + types: [ClassB::class => $objectType], ); $recursiveTypeMapper = new RecursiveTypeMapper( @@ -46,7 +37,7 @@ public function testMapClassToType(): void new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $this->assertFalse($typeMapper->canMapClassToType(ClassC::class)); @@ -60,14 +51,10 @@ public function testMapClassToType(): void public function testMapNameToType(): void { - $objectType = new MutableObjectType([ - 'name' => 'Foobar' - ]); + $objectType = new MutableObjectType(['name' => 'Foobar']); $typeMapper = new StaticTypeMapper( - types: [ - ClassB::class => $objectType - ] + types: [ClassB::class => $objectType], ); $recursiveTypeMapper = new RecursiveTypeMapper( @@ -75,7 +62,7 @@ public function testMapNameToType(): void new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $this->assertTrue($recursiveTypeMapper->canMapNameToType('Foobar')); @@ -97,17 +84,12 @@ public function testMapNameToType2(): void $recursiveMapper->mapNameToType('NotExists'); } - public function testMapClassToInputType(): void { - $inputObjectType = new InputObjectType([ - 'name' => 'Foobar' - ]); + $inputObjectType = new InputObjectType(['name' => 'Foobar']); $typeMapper = new StaticTypeMapper( - inputTypes: [ - ClassB::class => $inputObjectType - ] + inputTypes: [ClassB::class => $inputObjectType], ); $recursiveTypeMapper = new RecursiveTypeMapper( @@ -115,7 +97,7 @@ public function testMapClassToInputType(): void new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $this->assertFalse($recursiveTypeMapper->canMapClassToInputType(ClassC::class)); @@ -126,33 +108,32 @@ public function testMapClassToInputType(): void protected $typeMapper; - protected function getTypeMapper() + protected function getTypeMapper(): RecursiveTypeMapper { if ($this->typeMapper === null) { $container = new LazyContainer([ - ClassAType::class => function () { + ClassAType::class => static function () { return new ClassAType(); }, - ClassBType::class => function () { + ClassBType::class => static function () { return new ClassBType(); - } + }, ]); $namingStrategy = new NamingStrategy(); - $compositeMapper = new CompositeTypeMapper(); $this->typeMapper = new RecursiveTypeMapper( $compositeMapper, new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $typeGenerator = new TypeGenerator($this->getAnnotationReader(), $namingStrategy, $this->getTypeRegistry(), $this->getRegistry(), $this->typeMapper, $this->getFieldsBuilder()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Interfaces\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), $namingStrategy, $this->typeMapper, new Psr16Cache(new NullAdapter())); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Interfaces\Types'), $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(), $namingStrategy, $this->typeMapper, new Psr16Cache(new NullAdapter())); $compositeMapper->addTypeMapper($mapper); } return $this->typeMapper; @@ -198,20 +179,14 @@ public function testGetOutputTypes(): void public function testDuplicateDetection(): void { - $objectType = new MutableObjectType([ - 'name' => 'Foobar' - ]); + $objectType = new MutableObjectType(['name' => 'Foobar']); $typeMapper1 = new StaticTypeMapper( - types: [ - ClassB::class => $objectType - ] + types: [ClassB::class => $objectType], ); $typeMapper2 = new StaticTypeMapper( - types: [ - ClassA::class => $objectType - ] + types: [ClassA::class => $objectType], ); $compositeTypeMapper = new CompositeTypeMapper(); @@ -223,7 +198,7 @@ public function testDuplicateDetection(): void new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $this->expectException(DuplicateMappingException::class); @@ -243,7 +218,7 @@ public function testMapNoTypes(): void new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $this->expectException(TypeNotFoundException::class); @@ -257,14 +232,14 @@ public function testMapNameToTypeDecorators(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $this->getRegistry(), new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new GlobTypeMapper($this->getNamespaceFactory()->createNamespace('TheCodingMachine\GraphQLite\Fixtures\Integration'), $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $this->getRegistry(), new \TheCodingMachine\GraphQLite\AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $recursiveTypeMapper = new RecursiveTypeMapper( $mapper, new NamingStrategy(), new Psr16Cache(new ArrayAdapter()), $this->getTypeRegistry(), - $this->getAnnotationReader() + $this->getAnnotationReader(), ); $type = $recursiveTypeMapper->mapNameToType('FilterInput'); diff --git a/tests/Mappers/StaticClassListTypeMapperTest.php b/tests/Mappers/StaticClassListTypeMapperTest.php index c69ede9964..c77beb3929 100644 --- a/tests/Mappers/StaticClassListTypeMapperTest.php +++ b/tests/Mappers/StaticClassListTypeMapperTest.php @@ -2,7 +2,6 @@ namespace TheCodingMachine\GraphQLite\Mappers; -use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\Cache\Simple\ArrayCache; @@ -22,7 +21,7 @@ public function testClassListException(): void $cache = new Psr16Cache(new ArrayAdapter()); - $mapper = new StaticClassListTypeMapper(['NotExistsClass'], $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getTypeMapper(), $cache); + $mapper = new StaticClassListTypeMapper(['NotExistsClass'], $typeGenerator, $inputTypeGenerator, $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(), new NamingStrategy(), $this->getTypeMapper(), $cache); $this->expectException(GraphQLRuntimeException::class); $this->expectExceptionMessage('Could not find class "NotExistsClass"'); diff --git a/tests/SchemaFactoryTest.php b/tests/SchemaFactoryTest.php index d57dc78a11..31666902a7 100644 --- a/tests/SchemaFactoryTest.php +++ b/tests/SchemaFactoryTest.php @@ -4,7 +4,6 @@ namespace TheCodingMachine\GraphQLite; -use Doctrine\Common\Annotations\AnnotationReader; use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; @@ -71,8 +70,7 @@ public function testSetters(): void $factory->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers'); $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); - $factory->setDoctrineAnnotationReader(new AnnotationReader()) - ->setAuthenticationService(new VoidAuthenticationService()) + $factory->setAuthenticationService(new VoidAuthenticationService()) ->setAuthorizationService(new VoidAuthorizationService()) ->setNamingStrategy(new NamingStrategy()) ->addTypeMapper(new CompositeTypeMapper()) diff --git a/website/docs/CHANGELOG.md b/website/docs/CHANGELOG.md index feb5972c91..fc3f57dba6 100644 --- a/website/docs/CHANGELOG.md +++ b/website/docs/CHANGELOG.md @@ -138,9 +138,9 @@ public function toGraphQLInputType(Type $type, ?InputType $subType, string $argu ### New features -- [@Input](annotations-reference.md#input-annotation) annotation is introduced as an alternative to `@Factory`. Now GraphQL input type can be created in the same manner as `@Type` in combination with `@Field` - [example](input-types.mdx#input-attribute). +- [@Input](annotations-reference.md#input-annotation) annotation is introduced as an alternative to `#[Factory]`. Now GraphQL input type can be created in the same manner as `#[Type]` in combination with `#[Field]` - [example](input-types.mdx#input-attribute). - New attributes has been added to [@Field](annotations-reference.md#field-annotation) annotation: `for`, `inputType` and `description`. -- The following annotations now can be applied to class properties directly: `@Field`, `@Logged`, `@Right`, `@FailWith`, `@HideIfUnauthorized` and `@Security`. +- The following annotations now can be applied to class properties directly: `#[Field]`, `#[Logged]`, `#[Right]`, `@FailWith`, `@HideIfUnauthorized` and `#[Security]`. ## 4.1.0 @@ -174,21 +174,21 @@ changed. ### New features -- You can directly [annotate a PHP interface with `@Type` to make it a GraphQL interface](inheritance-interfaces.mdx#mapping-interfaces) +- You can directly [annotate a PHP interface with `#[Type]` to make it a GraphQL interface](inheritance-interfaces.mdx#mapping-interfaces) - You can autowire services in resolvers, thanks to the new `@Autowire` annotation -- Added [user input validation](validation.mdx) (using the Symfony Validator or the Laravel validator or a custom `@Assertion` annotation +- Added [user input validation](validation.mdx) (using the Symfony Validator or the Laravel validator or a custom `#[Assertion]` annotation - Improved security handling: - Unauthorized access to fields can now generate GraphQL errors (rather that schema errors in GraphQLite v3) - - Added fine-grained security using the `@Security` annotation. A field can now be [marked accessible or not depending on the context](fine-grained-security.mdx). + - Added fine-grained security using the `#[Security]` annotation. A field can now be [marked accessible or not depending on the context](fine-grained-security.mdx). For instance, you can restrict access to the field "viewsCount" of the type `BlogPost` only for post that the current user wrote. - - You can now inject the current logged user in any query / mutation / field using the `@InjectUser` annotation + - You can now inject the current logged user in any query / mutation / field using the `#[InjectUser]` annotation - Performance: - You can inject the [Webonyx query plan in a parameter from a resolver](query-plan.mdx) - You can use the [dataloader pattern to improve performance drastically via the "prefetchMethod" attribute](prefetch-method.mdx) - Customizable error handling has been added: - You can throw [many errors in one exception](error-handling.mdx#many-errors-for-one-exception) with `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException` - You can force input types using `@UseInputType(for="$id", inputType="ID!")` -- You can extend an input types (just like you could extend an output type in v3) using [the new `@Decorate` annotation](extend-input-type.mdx) +- You can extend an input types (just like you could extend an output type in v3) using [the new `#[Decorate]` annotation](extend-input-type.mdx) - In a factory, you can [exclude some optional parameters from the GraphQL schema](input-types#ignoring-some-parameters) Many extension points have been added diff --git a/website/docs/README.mdx b/website/docs/README.mdx index 0293578109..0df34daa98 100644 --- a/website/docs/README.mdx +++ b/website/docs/README.mdx @@ -5,9 +5,6 @@ slug: / sidebar_label: GraphQLite --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -

GraphQLite logo

@@ -26,14 +23,6 @@ file uploads, security, validation, extendable types and more! First, declare a query in your controller: - - - ```php class ProductController { @@ -45,35 +34,8 @@ class ProductController } ``` - - - -```php -class ProductController -{ - /** - * @Query() - */ - public function product(string $id): Product - { - // Some code that looks for a product and returns it. - } -} -``` - - - - Then, annotate the `Product` class to declare what fields are exposed to the GraphQL API: - - - ```php #[Type] class Product @@ -87,29 +49,6 @@ class Product } ``` - - - -```php -/** - * @Type() - */ -class Product -{ - /** - * @Field() - */ - public function getName(): string - { - return $this->name; - } - // ... -} -``` - - - - That's it, you're good to go! Query and enjoy! ```graphql diff --git a/website/docs/annotations-reference.md b/website/docs/annotations-reference.md index e838cbf987..485b1f561c 100644 --- a/website/docs/annotations-reference.md +++ b/website/docs/annotations-reference.md @@ -1,15 +1,15 @@ --- id: annotations-reference -title: Annotations reference -sidebar_label: Annotations reference +title: Attributes reference +sidebar_label: Attributes reference --- -Note: all annotations are available both in a Doctrine annotation format (`@Query`) and in PHP 8 attribute format (`#[Query]`). +Note: all annotations are available in PHP 8 attribute format (`#[Query]`), support of Doctrine annotation format was dropped. See [Doctrine annotations vs PHP 8 attributes](doctrine-annotations-attributes.mdx) for more details. -## @Query +## #[Query] -The `@Query` annotation is used to declare a GraphQL query. +The `#[Query]` attribute is used to declare a GraphQL query. **Applies on**: controller methods. @@ -18,9 +18,9 @@ Attribute | Compulsory | Type | Definition name | *no* | string | The name of the query. If skipped, the name of the method is used instead. [outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. -## @Mutation +## #[Mutation] -The `@Mutation` annotation is used to declare a GraphQL mutation. +The `#[Mutation]` attribute is used to declare a GraphQL mutation. **Applies on**: controller methods. @@ -29,9 +29,9 @@ Attribute | Compulsory | Type | Definition name | *no* | string | The name of the mutation. If skipped, the name of the method is used instead. [outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. -## @Subscription +## #[Subscription] -The `@Subscription` annotation is used to declare a GraphQL subscription. +The `#[Subscription]` attribute is used to declare a GraphQL subscription. **Applies on**: controller methods. @@ -40,36 +40,36 @@ Attribute | Compulsory | Type | Definition name | *no* | string | The name of the subscription. If skipped, the name of the method is used instead. [outputType](custom-types.mdx) | *no* | string | Defines the GraphQL output type that will be sent for the subscription. -## @Type +## #[Type] -The `@Type` annotation is used to declare a GraphQL object type. This is used with standard output -types, as well as enum types. For input types, use the [@Input annotation](#input-annotation) directly on the input type or a [@Factory annoation](#factory-annotation) to make/return an input type. +The `#[Type]` attribute is used to declare a GraphQL object type. This is used with standard output +types, as well as enum types. For input types, use the [#[Input] attribute](#input-annotation) directly on the input type or a [#[Factory] attribute](#factory-annotation) to make/return an input type. **Applies on**: classes. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- -class | *no* | string | The targeted class/enum for the actual type. If no "class" attribute is passed, the type applies to the current class/enum. The current class/enum is assumed to be an entity (not service). If the "class" attribute *is passed*, [the class/enum annotated with `@Type` becomes a service](external-type-declaration.mdx). +class | *no* | string | The targeted class/enum for the actual type. If no "class" attribute is passed, the type applies to the current class/enum. The current class/enum is assumed to be an entity (not service). If the "class" attribute *is passed*, [the class/enum annotated with `#[Type]` becomes a service](external-type-declaration.mdx). name | *no* | string | The name of the GraphQL type generated. If not passed, the name of the class is used. If the class ends with "Type", the "Type" suffix is removed default | *no* | bool | Defaults to *true*. Whether the targeted PHP class should be mapped by default to this type. external | *no* | bool | Whether this is an [external type declaration](external-type-declaration.mdx) or not. You usually do not need to use this attribute since this value defaults to true if a "class" attribute is set. This is only useful if you are declaring a type with no PHP class mapping using the "name" attribute. -## @ExtendType +## #[ExtendType] -The `@ExtendType` annotation is used to add fields to an existing GraphQL object type. +The `#[ExtendType]` attribute is used to add fields to an existing GraphQL object type. **Applies on**: classes. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- -class | see below | string | The targeted class. [The class annotated with `@ExtendType` is a service](extend-type.mdx). +class | see below | string | The targeted class. [The class annotated with `#[ExtendType]` is a service](extend-type.mdx). name | see below | string | The targeted GraphQL output type. One and only one of "class" and "name" parameter can be passed at the same time. -## @Input +## #[Input] -The `@Input` annotation is used to declare a GraphQL input type. +The `#[Input]` attribute is used to declare a GraphQL input type. **Applies on**: classes. @@ -80,11 +80,11 @@ description | *no* | string | Description of the input type in the docu default | *no* | bool | Name of the input type represented in your GraphQL schema. Defaults to `true` *only if* the name is not specified. If `name` is specified, this will default to `false`, so must also be included for `true` when `name` is used. update | *no* | bool | Determines if the the input represents a partial update. When set to `true` all input fields will become optional and won't have default values thus won't be set on resolve if they are not specified in the query/mutation/subscription. This primarily applies to nullable fields. -## @Field +## #[Field] -The `@Field` annotation is used to declare a GraphQL field. +The `#[Field]` attribute is used to declare a GraphQL field. -**Applies on**: methods or properties of classes annotated with `@Type`, `@ExtendType` or `@Input`. +**Applies on**: methods or properties of classes annotated with `#[Type]`, `#[ExtendType]` or `#[Input]`. When it's applied on private or protected property, public getter or/and setter method is expected in the class accordingly whether it's used for output type or input type. For example if property name is `foo` then getter should be `getFoo()` or setter should be `setFoo($foo)`. Setter can be omitted if property related to the field is present in the constructor with the same name. @@ -96,11 +96,11 @@ description | *no* | string | Field description d [outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. [inputType](input-types.mdx) | *no* | string | Forces the GraphQL input type of a query. -## @SourceField +## #[SourceField] -The `@SourceField` annotation is used to declare a GraphQL field. +The `#[SourceField]` attribute is used to declare a GraphQL field. -**Applies on**: classes annotated with `@Type` or `@ExtendType`. +**Applies on**: classes annotated with `#[Type]` or `#[ExtendType]`. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- @@ -109,15 +109,15 @@ name | *yes* | string | The name of the field. phpType | *no* | string | The PHP type of the field (as you would write it in a Docblock) description | *no* | string | Field description displayed in the GraphQL docs. If it's empty PHP doc comment of the method in the source class is used instead. sourceName | *no* | string | The name of the property in the source class. If not set, the `name` will be used to get property value. -annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. Available in Doctrine annotations only (not available in the #SourceField PHP 8 attribute) +annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "#[Logged]" or "#[Right]" attribute as class here. **Note**: `outputType` and `phpType` are mutually exclusive. -## @MagicField +## #[MagicField] -The `@MagicField` annotation is used to declare a GraphQL field that originates from a PHP magic property (using `__get` magic method). +The `#[MagicField]` attribute is used to declare a GraphQL field that originates from a PHP magic property (using `__get` magic method). -**Applies on**: classes annotated with `@Type` or `@ExtendType`. +**Applies on**: classes annotated with `#[Type]` or `#[ExtendType]`. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- @@ -126,114 +126,114 @@ name | *yes* | string | The name of the field. phpType | *no*(*) | string | The PHP type of the field (as you would write it in a Docblock) description | *no* | string | Field description displayed in the GraphQL docs. If not set, no description will be shown. sourceName | *no* | string | The name of the property in the source class. If not set, the `name` will be used to get property value. -annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. Available in Doctrine annotations only (not available in the #MagicField PHP 8 attribute) +annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "#[Logged]" or "#[Right]" attribute as class here. (*) **Note**: `outputType` and `phpType` are mutually exclusive. You MUST provide one of them. -## @Prefetch +## #[Prefetch] Marks field parameter to be used for [prefetching](prefetch-method.mdx). -**Applies on**: parameters of methods annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: parameters of methods annotated with `#[Query]`, `#[Mutation]` or `#[Field]`. Attribute | Compulsory | Type | Definition ------------------------------|------------|----------|-------- callable | *no* | callable | Name of the prefetch method (in same class) or a full callable, either a static method or regular service from the container -## @Logged +## #[Logged] -The `@Logged` annotation is used to declare a Query/Mutation/Field is only visible to logged users. +The `#[Logged]` attribute is used to declare a Query/Mutation/Field is only visible to logged users. -**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `#[Query]`, `#[Mutation]` or `#[Field]`. -This annotation allows no attributes. +This attribute allows no arguments. -## @Right +## #[Right] -The `@Right` annotation is used to declare a Query/Mutation/Field is only visible to users with a specific right. +The `#[Right]` attribute is used to declare a Query/Mutation/Field is only visible to users with a specific right. -**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `#[Query]`, `#[Mutation]` or `#[Field]`. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- name | *yes* | string | The name of the right. -## @FailWith +## #[FailWith] -The `@FailWith` annotation is used to declare a default value to return in the user is not authorized to see a specific -query/mutation/subscription/field (according to the `@Logged` and `@Right` annotations). +The `#[FailWith]` attribute is used to declare a default value to return in the user is not authorized to see a specific +query/mutation/subscription/field (according to the `#[Logged]` and `#[Right]` attributes). -**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. +**Applies on**: methods or properties annotated with `#[Query]`, `#[Mutation]` or `#[Field]` and one of `#[Logged]` or `#[Right]` attributes. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- value | *yes* | mixed | The value to return if the user is not authorized. -## @HideIfUnauthorized +## #[HideIfUnauthorized] -
This annotation only works when a Schema is used to handle exactly one use request. +
This attribute only works when a Schema is used to handle exactly one use request. If you serve your GraphQL API from long-running standalone servers (like Laravel Octane, Swoole, RoadRunner etc) and -share the same Schema instance between multiple requests, please avoid using @HideIfUnauthorized.
+share the same Schema instance between multiple requests, please avoid using #[HideIfUnauthorized].
-The `@HideIfUnauthorized` annotation is used to completely hide the query/mutation/subscription/field if the user is not authorized -to access it (according to the `@Logged` and `@Right` annotations). +The `#[HideIfUnauthorized]` attribute is used to completely hide the query/mutation/subscription/field if the user is not authorized +to access it (according to the `#[Logged]` and `#[Right]` attributes). -**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. +**Applies on**: methods or properties annotated with `#[Query]`, `#[Mutation]` or `#[Field]` and one of `#[Logged]` or `#[Right]` attributes. -`@HideIfUnauthorized` and `@FailWith` are mutually exclusive. +`#[HideIfUnauthorized]` and `#[FailWith]` are mutually exclusive. -## @InjectUser +## #[InjectUser] -Use the `@InjectUser` annotation to inject an instance of the current user logged in into a parameter of your +Use the `#[InjectUser]` attribute to inject an instance of the current user logged in into a parameter of your query/mutation/subscription/field. See [the authentication and authorization page](authentication-authorization.mdx) for more details. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods annotated with `#[Query]`, `#[Mutation]` or `#[Field]`. Attribute | Compulsory | Type | Definition ---------------|------------|--------|-------- *for* | *yes* | string | The name of the PHP parameter -## @Security +## #[Security] -The `@Security` annotation can be used to check fin-grained access rights. +The `#[Security]` attribute can be used to check fin-grained access rights. It is very flexible: it allows you to pass an expression that can contains custom logic. See [the fine grained security page](fine-grained-security.mdx) for more details. -**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. +**Applies on**: methods or properties annotated with `#[Query]`, `#[Mutation]` or `#[Field]`. Attribute | Compulsory | Type | Definition ---------------|------------|--------|-------- *default* | *yes* | string | The security expression -## @Factory +## #[Factory] -The `@Factory` annotation is used to declare a factory that turns GraphQL input types into objects. +The `#[Factory]` attribute is used to declare a factory that turns GraphQL input types into objects. **Applies on**: methods from classes in the "types" namespace. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- name | *no* | string | The name of the input type. If skipped, the name of class returned by the factory is used instead. -default | *no* | bool | If `true`, this factory will be used by default for its PHP return type. If set to `false`, you must explicitly [reference this factory using the `@Parameter` annotation](input-types.mdx#declaring-several-input-types-for-the-same-php-class). +default | *no* | bool | If `true`, this factory will be used by default for its PHP return type. If set to `false`, you must explicitly [reference this factory using the `#[Parameter]` attribute](input-types.mdx#declaring-several-input-types-for-the-same-php-class). -## @UseInputType +## #[UseInputType] Used to override the GraphQL input type of a PHP parameter. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` annotation. +**Applies on**: methods annotated with `#[Query]`, `#[Mutation]` or `#[Field]` attribute. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- *for* | *yes* | string | The name of the PHP parameter *inputType* | *yes* | string | The GraphQL input type to force for this input field -## @Decorate +## #[Decorate] -The `@Decorate` annotation is used [to extend/modify/decorate an input type declared with the `@Factory` annotation](extend-input-type.mdx). +The `#[Decorate]` attribute is used [to extend/modify/decorate an input type declared with the `#[Factory]` attribute](extend-input-type.mdx). **Applies on**: methods from classes in the "types" namespace. @@ -241,20 +241,20 @@ Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- name | *yes* | string | The GraphQL input type name extended by this decorator. -## @Autowire +## #[Autowire] [Resolves a PHP parameter from the container](autowiring.mdx). -Useful to inject services directly into `@Field` method arguments. +Useful to inject services directly into `#[Field]` method arguments. -**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` annotation. +**Applies on**: methods annotated with `#[Query]`, `#[Mutation]` or `#[Field]` attribute. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- *for* | *yes* | string | The name of the PHP parameter *identifier* | *no* | string | The identifier of the service to fetch. This is optional. Please avoid using this attribute as this leads to a "service locator" anti-pattern. -## @HideParameter +## #[HideParameter] Removes [an argument from the GraphQL schema](input-types.mdx#ignoring-some-parameters). @@ -262,7 +262,7 @@ Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- *for* | *yes* | string | The name of the PHP parameter to hide -## @Cost +## #[Cost] Sets complexity and multipliers on fields for [automatic query complexity](operation-complexity.md#static-request-analysis). @@ -272,13 +272,13 @@ Attribute | Compulsory | Type | Definition *multipliers* | *no* | array\ | Names of fields by value of which complexity will be multiplied *defaultMultiplier* | *no* | int | Default multiplier value if all multipliers are missing/null -## @Validate +## #[Validate] -
This annotation is only available in the GraphQLite Laravel package
+
This attribute is only available in the GraphQLite Laravel package
[Validates a user input in Laravel](laravel-package-advanced.mdx). -**Applies on**: methods annotated with `@Query`, `@Mutation`, `@Field`, `@Factory` or `@Decorator` annotation. +**Applies on**: methods annotated with `#[Query]`, `#[Mutation]`, `#[Field]`, `#[Factory]` or `#[Decorator]` attribute. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- @@ -288,26 +288,26 @@ Attribute | Compulsory | Type | Definition Sample: ```php -@Validate(for="$email", rule="email|unique:users") +#[Validate(for: "$email", rule: "email|unique:users")] ``` -## @Assertion +## #[Assertion] [Validates a user input](validation.mdx). -The `@Assertion` annotation is available in the *thecodingmachine/graphqlite-symfony-validator-bridge* third party package. +The `#[Assertion]` attribute is available in the *thecodingmachine/graphqlite-symfony-validator-bridge* third party package. It is available out of the box if you use the Symfony bundle. -**Applies on**: methods annotated with `@Query`, `@Mutation`, `@Field`, `@Factory` or `@Decorator` annotation. +**Applies on**: methods annotated with `#[Query]`, `#[Mutation]`, `#[Field]`, `#[Factory]` or `#[Decorator]` attribute. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- *for* | *yes* | string | The name of the PHP parameter -*constraint* | *yes | annotation | One (or many) Symfony validation annotations. +*constraint* | *yes | annotation | One (or many) Symfony validation attributes. ## ~~@EnumType~~ -*Deprecated: Use [PHP 8.1's native Enums](https://www.php.net/manual/en/language.types.enumerations.php) instead with a [@Type](#type-annotation).* +*Deprecated: Use [PHP 8.1's native Enums](https://www.php.net/manual/en/language.types.enumerations.php) instead with a [#[Type]](#type-annotation).* The `@EnumType` annotation is used to change the name of a "Enum" type. Note that if you do not want to change the name, the annotation is optionnal. Any object extending `MyCLabs\Enum\Enum` diff --git a/website/docs/argument-resolving.md b/website/docs/argument-resolving.md index 35556c66d0..8125964dae 100644 --- a/website/docs/argument-resolving.md +++ b/website/docs/argument-resolving.md @@ -24,8 +24,8 @@ As an example, GraphQLite uses *parameter middlewares* internally to: In the query above, the `$info` argument is filled with the Webonyx `ResolveInfo` class thanks to the [`ResolveInfoParameterHandler parameter middleware`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ResolveInfoParameterHandler.php) -- Inject a service from the container when you use the `@Autowire` annotation -- Perform validation with the `@Validate` annotation (in Laravel package) +- Inject a service from the container when you use the `#[Autowire]` attribute +- Perform validation with the `#[Validate]` attribute (in Laravel package) @@ -54,23 +54,22 @@ interface ParameterMiddlewareInterface Then, resolution actually happen by executing the resolver (this is the second pass). -## Annotations parsing +## Attributes parsing -If you plan to use annotations while resolving arguments, your annotation should extend the [`ParameterAnnotationInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Annotations/ParameterAnnotationInterface.php) +If you plan to use attributes while resolving arguments, your attribute class should extend the [`ParameterAnnotationInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Annotations/ParameterAnnotationInterface.php) -For instance, if we want GraphQLite to inject a service in an argument, we can use `@Autowire(for="myService")`. +For instance, if we want GraphQLite to inject a service in an argument, we can use `#[Autowire]`. -For PHP 8 attributes, we only need to put declare the annotation can target parameters: `#[Attribute(Attribute::TARGET_PARAMETER)]`. +We only need to put declare the annotation can target parameters: `#[Attribute(Attribute::TARGET_PARAMETER)]`. -The annotation looks like this: +The class looks like this: ```php use Attribute; /** - * Use this annotation to autowire a service from the container into a given parameter of a field/query/mutation. + * Use this attribute to autowire a service from the container into a given parameter of a field/query/mutation. * - * @Annotation */ #[Attribute(Attribute::TARGET_PARAMETER)] class Autowire implements ParameterAnnotationInterface diff --git a/website/docs/authentication-authorization.mdx b/website/docs/authentication-authorization.mdx index f716d02d30..9842e2a3d3 100644 --- a/website/docs/authentication-authorization.mdx +++ b/website/docs/authentication-authorization.mdx @@ -4,18 +4,15 @@ title: Authentication and authorization sidebar_label: Authentication and authorization --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - You might not want to expose your GraphQL API to anyone. Or you might want to keep some queries/mutations/subscriptions or fields reserved to some users. GraphQLite offers some control over what a user can do with your API. You can restrict access to resources: -- based on authentication using the [`@Logged` annotation](#logged-and-right-annotations) (restrict access to logged users) -- based on authorization using the [`@Right` annotation](#logged-and-right-annotations) (restrict access to logged users with certain rights). -- based on fine-grained authorization using the [`@Security` annotation](fine-grained-security.mdx) (restrict access for some given resources to some users). +- based on authentication using the [`#[Logged]` attribute](#logged-and-right-annotations) (restrict access to logged users) +- based on authorization using the [`#[Right]` attribute](#logged-and-right-annotations) (restrict access to logged users with certain rights). +- based on fine-grained authorization using the [`#[Security]` attribute](fine-grained-security.mdx) (restrict access for some given resources to some users).
GraphQLite does not have its own security mechanism. @@ -23,16 +20,9 @@ resources: See Connecting GraphQLite to your framework's security module.
-## `@Logged` and `@Right` annotations +## `#[Logged]` and `#[Right]` attributes -GraphQLite exposes two annotations (`@Logged` and `@Right`) that you can use to restrict access to a resource. - - +GraphQLite exposes two attributes (`#[Logged]` and `#[Right]`) that you can use to restrict access to a resource. ```php namespace App\Controller; @@ -56,43 +46,14 @@ class UserController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Query; -use TheCodingMachine\GraphQLite\Annotations\Logged; -use TheCodingMachine\GraphQLite\Annotations\Right; - -class UserController -{ - /** - * @Query - * @Logged - * @Right("CAN_VIEW_USER_LIST") - * @return User[] - */ - public function users(int $limit, int $offset): array - { - // ... - } -} -``` - - - - - In the example above, the query `users` will only be available if the user making the query is logged AND if he has the `CAN_VIEW_USER_LIST` right. -`@Logged` and `@Right` annotations can be used next to: +`#[Logged]` and `#[Right]` attributes can be used next to: -* `@Query` annotations -* `@Mutation` annotations -* `@Field` annotations +* `#[Query]` attributes +* `#[Mutation]` attributes +* `#[Field]` attributes
By default, if a user tries to access an unauthorized query/mutation/subscription/field, an error is @@ -102,17 +63,9 @@ has the `CAN_VIEW_USER_LIST` right. ## Not throwing errors If you do not want an error to be thrown when a user attempts to query a field/query/mutation/subscription -they have no access to, you can use the `@FailWith` annotation. - -The `@FailWith` annotation contains the value that will be returned for users with insufficient rights. +they have no access to, you can use the `#[FailWith]` attribute. - - +The `#[FailWith]` attribute contains the value that will be returned for users with insufficient rights. ```php class UserController @@ -134,43 +87,9 @@ class UserController } ``` - - - -```php -class UserController -{ - /** - * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", - * the value returned will be "null". - * - * @Query - * @Logged - * @Right("CAN_VIEW_USER_LIST") - * @FailWith(null) - * @return User[] - */ - public function users(int $limit, int $offset): array - { - // ... - } -} -``` - - - - ## Injecting the current user as a parameter -Use the `@InjectUser` annotation to get an instance of the current user logged in. - - - +Use the `#[InjectUser]` attribute to get an instance of the current user logged in. ```php namespace App\Controller; @@ -181,9 +100,9 @@ use TheCodingMachine\GraphQLite\Annotations\InjectUser; class ProductController { /** - * @Query * @return Product */ + #[Query] public function product( int $id, #[InjectUser] @@ -195,41 +114,15 @@ class ProductController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Query; -use TheCodingMachine\GraphQLite\Annotations\InjectUser; - -class ProductController -{ - /** - * @Query - * @InjectUser(for="$user") - * @return Product - */ - public function product(int $id, User $user): Product - { - // ... - } -} -``` - - - - -The `@InjectUser` annotation can be used next to: +The `#[InjectUser]` attribute can be used next to: -* `@Query` annotations -* `@Mutation` annotations -* `@Field` annotations +* `#[Query]` attributes +* `#[Mutation]` attributes +* `#[Field]` attributes The object injected as the current user depends on your framework. It is in fact the object returned by the ["authentication service" configured in GraphQLite](implementing-security.md). If user is not authenticated and -parameter's type is not nullable, an authorization exception is thrown, similar to `@Logged` annotation. +parameter's type is not nullable, an authorization exception is thrown, similar to `#[Logged]` attribute. ## Hiding fields / queries / mutations / subscriptions @@ -237,15 +130,7 @@ By default, a user analysing the GraphQL schema can see all queries/mutations/su Some will be available to him and some won't. If you want to add an extra level of security (or if you want your schema to be kept secret to unauthorized users), -you can use the `@HideIfUnauthorized` annotation. Beware of [it's limitations](annotations-reference.md). - - - +you can use the `#[HideIfUnauthorized]` attribute. Beware of [it's limitations](annotations-reference.md). ```php class UserController @@ -268,33 +153,6 @@ class UserController } ``` - - - -```php -class UserController -{ - /** - * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", - * the schema will NOT contain the "users" query at all (so trying to call the - * "users" query will result in a GraphQL "query not found" error. - * - * @Query - * @Logged - * @Right("CAN_VIEW_USER_LIST") - * @HideIfUnauthorized() - * @return User[] - */ - public function users(int $limit, int $offset): array - { - // ... - } -} -``` - - - - While this is the most secured mode, it can have drawbacks when working with development tools (you need to be logged as admin to fetch the complete schema). diff --git a/website/docs/autowiring.mdx b/website/docs/autowiring.mdx index c38bb73d77..825241afe0 100644 --- a/website/docs/autowiring.mdx +++ b/website/docs/autowiring.mdx @@ -4,14 +4,11 @@ title: Autowiring services sidebar_label: Autowiring services --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - GraphQLite can automatically inject services in your fields/queries/mutations signatures. Some of your fields may be computed. In order to compute these fields, you might need to call a service. -Most of the time, your `@Type` annotation will be put on a model. And models do not have access to services. +Most of the time, your `#[Type]` attribute will be put on a model. And models do not have access to services. Hopefully, if you add a type-hinted service in your field's declaration, GraphQLite will automatically fill it with the service instance. @@ -20,14 +17,6 @@ the service instance. Let's assume you are running an international store. You have a `Product` class. Each product has many names (depending on the language of the user). - - - ```php namespace App\Entities; @@ -53,39 +42,6 @@ class Product } ``` - - - -```php -namespace App\Entities; - -use TheCodingMachine\GraphQLite\Annotations\Autowire; -use TheCodingMachine\GraphQLite\Annotations\Field; -use TheCodingMachine\GraphQLite\Annotations\Type; - -use Symfony\Component\Translation\TranslatorInterface; - -/** - * @Type() - */ -class Product -{ - // ... - - /** - * @Field() - * @Autowire(for="$translator") - */ - public function getName(TranslatorInterface $translator): string - { - return $translator->trans('product_name_'.$this->id); - } -} -``` - - - - When GraphQLite queries the name, it will automatically fetch the translator service.
As with most autowiring solutions, GraphQLite assumes that the service identifier @@ -130,30 +86,10 @@ By type-hinting against an interface, your code remains testable and is decouple Optionally, you can specify the identifier of the service you want to fetch from the controller: - - - ```php #[Autowire(identifier: "translator")] ``` - - - -```php -/** - * @Autowire(for="$translator", identifier="translator") - */ -``` - - - -
While GraphQLite offers the possibility to specify the name of the service to be autowired, we would like to emphasize that this is highly discouraged. Hard-coding a container identifier in the code of your class is akin to using the "service locator" pattern, which is known to be an diff --git a/website/docs/custom-types.mdx b/website/docs/custom-types.mdx index 800ab2846c..527e91623c 100644 --- a/website/docs/custom-types.mdx +++ b/website/docs/custom-types.mdx @@ -4,21 +4,10 @@ title: Custom types sidebar_label: Custom types --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - In some special cases, you want to override the GraphQL return type that is attributed by default by GraphQLite. For instance: - - - ```php #[Type(class: Product::class)] class ProductType @@ -31,28 +20,6 @@ class ProductType } ``` - - - -```php -/** - * @Type(class=Product::class) - */ -class ProductType -{ - /** - * @Field - */ - public function getId(Product $source): string - { - return $source->getId(); - } -} -``` - - - - In the example above, GraphQLite will generate a GraphQL schema with a field `id` of type `string`: ```graphql @@ -66,42 +33,22 @@ is an `ID` or not. You can help GraphQLite by manually specifying the output type to use: - - - ```php #[Field(outputType: "ID")] ``` - - - -```php - /** - * @Field(name="id", outputType="ID") - */ -``` - - - - ## Usage The `outputType` attribute will map the return value of the method to the output type passed in parameter. You can use the `outputType` attribute in the following annotations: -* `@Query` -* `@Mutation` -* `@Subscription` -* `@Field` -* `@SourceField` -* `@MagicField` +* `#[Query]` +* `#[Mutation]` +* `#[Subscription]` +* `#[Field]` +* `#[SourceField]` +* `#[MagicField]` ## Registering a custom output type (advanced) diff --git a/website/docs/doctrine-annotations-attributes.mdx b/website/docs/doctrine-annotations-attributes.mdx index 72725a39b2..058c276a95 100644 --- a/website/docs/doctrine-annotations-attributes.mdx +++ b/website/docs/doctrine-annotations-attributes.mdx @@ -4,48 +4,12 @@ title: Doctrine annotations VS PHP8 attributes sidebar_label: Annotations VS Attributes --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - GraphQLite is heavily relying on the concept of annotations (also called attributes in PHP 8+). ## Doctrine annotations -
- Deprecated! Doctrine annotations are deprecated in favor of native PHP 8 attributes. Support will be dropped in a future release. -
- -Historically, attributes were not available in PHP and PHP developers had to "trick" PHP to get annotation support. This was the purpose of the [doctrine/annotation](https://www.doctrine-project.org/projects/doctrine-annotations/en/latest/index.html) library. - -Using Doctrine annotations, you write annotations in your docblocks: - -```php -use TheCodingMachine\GraphQLite\Annotations\Type; - -/** - * @Type - */ -class MyType -{ -} -``` - -Please note that: - -- The annotation is added in a **docblock** (a comment starting with "`/**`") -- The `Type` part is actually a class. It must be declared in the `use` statements at the top of your file. - - -
- Heads up! -

Some IDEs provide support for Doctrine annotations:

- - - We strongly recommend using an IDE that has Doctrine annotations support. +
+ Unsupported! Doctrine annotations are replaced in favor of native PHP 8 attributes.
## PHP 8 attributes @@ -71,19 +35,10 @@ They support the same attributes too. A few notable differences: -- PHP 8 attributes do not support nested attributes (unlike Doctrine annotations). This means there is no equivalent to the `annotations` attribute of `@MagicField` and `@SourceField`. - PHP 8 attributes can be written at the parameter level. Any attribute targeting a "parameter" must be written at the parameter level. Let's take an example with the [`#Autowire` attribute](autowiring.mdx): - - - ```php #[Field] public function getProduct(#[Autowire] ProductRepository $productRepository) : Product { @@ -91,23 +46,6 @@ public function getProduct(#[Autowire] ProductRepository $productRepository) : P } ``` - - - -```php -/** - * @Field - * @Autowire(for="$productRepository") - */ -public function getProduct(ProductRepository $productRepository) : Product { - //... -} -``` - - - - - ## Migrating from Doctrine annotations to PHP 8 attributes The good news is that you can easily migrate from Doctrine annotations to PHP 8 attributes using the amazing, [Rector library](https://github.com/rectorphp/rector). To do so, you'll want to use the following rector configuration: diff --git a/website/docs/error-handling.mdx b/website/docs/error-handling.mdx index 231f07db72..549a9c15c9 100644 --- a/website/docs/error-handling.mdx +++ b/website/docs/error-handling.mdx @@ -4,9 +4,6 @@ title: Error handling sidebar_label: Error handling --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - In GraphQL, when an error occurs, the server must add an "error" entry in the response. ```json @@ -142,14 +139,6 @@ throw only one exception. If you want to display several exceptions, you can bundle these exceptions in a `GraphQLAggregateException` that you can throw. - - - ```php use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException; @@ -171,35 +160,6 @@ public function createProduct(string $name, float $price): Product } ``` - - - -```php -use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException; - -/** - * @Query - */ -public function createProduct(string $name, float $price): Product -{ - $exceptions = new GraphQLAggregateException(); - - if ($name === '') { - $exceptions->add(new GraphQLException('Name cannot be empty', 400, null, 'VALIDATION')); - } - if ($price <= 0) { - $exceptions->add(new GraphQLException('Price must be positive', 400, null, 'VALIDATION')); - } - - if ($exceptions->hasExceptions()) { - throw $exceptions; - } -} -``` - - - - ## Webonyx exceptions GraphQLite is based on the wonderful webonyx/GraphQL-PHP library. Therefore, the Webonyx exception mechanism can diff --git a/website/docs/extend-input-type.mdx b/website/docs/extend-input-type.mdx index 767856e32f..abe2818dd9 100644 --- a/website/docs/extend-input-type.mdx +++ b/website/docs/extend-input-type.mdx @@ -4,36 +4,25 @@ title: Extending an input type sidebar_label: Extending an input type --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - Available in GraphQLite 4.0+ -
If you are not familiar with the @Factory tag, read first the "input types" guide.
+
If you are not familiar with the #[Factory] tag, read first the "input types" guide.
Fields exposed in a GraphQL input type do not need to be all part of the factory method. -Just like with output type (that can be [extended using the `ExtendType` annotation](extend-type.mdx)), you can extend/modify -an input type using the `@Decorate` annotation. +Just like with output type (that can be [extended using the `ExtendType` attribute](extend-type.mdx)), you can extend/modify +an input type using the `#[Decorate]` attribute. -Use the `@Decorate` annotation to add additional fields to an input type that is already declared by a `@Factory` annotation, +Use the `#[Decorate]` attribute to add additional fields to an input type that is already declared by a `#[Factory]` attribute, or to modify the returned object.
- The @Decorate annotation is very useful in scenarios where you cannot touch the @Factory method. - This can happen if the @Factory method is defined in a third-party library or if the @Factory method is part + The #[Decorate] attribute is very useful in scenarios where you cannot touch the #[Factory] method. + This can happen if the #[Factory] method is defined in a third-party library or if the #[Factory] method is part of auto-generated code.
-Let's assume you have a `Filter` class used as an input type. You most certainly have a `@Factory` to create the input type. - - - +Let's assume you have a `Filter` class used as an input type. You most certainly have a `#[Factory]` to create the input type. ```php class MyFactory @@ -49,39 +38,9 @@ class MyFactory } ``` - - - -```php -class MyFactory -{ - /** - * @Factory() - */ - public function createFilter(string $name): Filter - { - // Let's assume you have a flexible 'Filter' class that can accept any kind of filter - $filter = new Filter(); - $filter->addFilter('name', $name); - return $filter; - } -} -``` - - - - Assuming you **cannot** modify the code of this factory, you can still modify the GraphQL input type generated by adding a "decorator" around the factory. - - - ```php class MyDecorator { @@ -94,26 +53,6 @@ class MyDecorator } ``` - - - -```php -class MyDecorator -{ - /** - * @Decorate(inputTypeName="FilterInput") - */ - public function addTypeFilter(Filter $filter, string $type): Filter - { - $filter->addFilter('type', $type); - return $filter; - } -} -``` - - - - In the example above, the "Filter" input type is modified. We add an additional "type" field to the input type. A few things to notice: @@ -121,8 +60,8 @@ A few things to notice: - The decorator takes the object generated by the factory as first argument - The decorator MUST return an object of the same type (or a sub-type) - The decorator CAN contain additional parameters. They will be added to the fields of the GraphQL input type. -- The `@Decorate` annotation must contain a `inputTypeName` attribute that contains the name of the GraphQL input type - that is decorated. If you did not specify this name in the `@Factory` annotation, this is by default the name of the +- The `#[Decorate]` attribute must contain a `inputTypeName` attribute that contains the name of the GraphQL input type + that is decorated. If you did not specify this name in the `#[Factory]` attribute, this is by default the name of the PHP class + "Input" (for instance: "Filter" => "FilterInput") diff --git a/website/docs/extend-type.mdx b/website/docs/extend-type.mdx index c8efb8d920..44d5ac9327 100644 --- a/website/docs/extend-type.mdx +++ b/website/docs/extend-type.mdx @@ -4,12 +4,10 @@ title: Extending a type sidebar_label: Extending a type --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; Fields exposed in a GraphQL type do not need to be all part of the same class. -Use the `@ExtendType` annotation to add additional fields to a type that is already declared. +Use the `#[ExtendType]` attribute to add additional fields to a type that is already declared.
Extending a type has nothing to do with type inheritance. @@ -20,14 +18,6 @@ Use the `@ExtendType` annotation to add additional fields to a type that is alre Let's assume you have a `Product` class. In order to get the name of a product, there is no `getName()` method in the product because the name needs to be translated in the correct language. You have a `TranslationService` to do that. - - - ```php namespace App\Entities; @@ -53,57 +43,12 @@ class Product } ``` - - - -```php -namespace App\Entities; - -use TheCodingMachine\GraphQLite\Annotations\Field; -use TheCodingMachine\GraphQLite\Annotations\Type; - -/** - * @Type() - */ -class Product -{ - // ... - - /** - * @Field() - */ - public function getId(): string - { - return $this->id; - } - - /** - * @Field() - */ - public function getPrice(): ?float - { - return $this->price; - } -} -``` - - - - ```php // You need to use a service to get the name of the product in the correct language. $name = $translationService->getProductName($productId, $language); ``` -Using `@ExtendType`, you can add an additional `name` field to your product: - - - +Using `#[ExtendType]`, you can add an additional `name` field to your product: ```php namespace App\Types; @@ -130,68 +75,13 @@ class ProductType } ``` - - - -```php -namespace App\Types; - -use TheCodingMachine\GraphQLite\Annotations\ExtendType; -use TheCodingMachine\GraphQLite\Annotations\Field; -use App\Entities\Product; - -/** - * @ExtendType(class=Product::class) - */ -class ProductType -{ - private $translationService; - - public function __construct(TranslationServiceInterface $translationService) - { - $this->translationService = $translationService; - } - - /** - * @Field() - */ - public function getName(Product $product, string $language): string - { - return $this->translationService->getProductName($product->getId(), $language); - } -} -``` - - - - Let's break this sample: - - - ```php #[ExtendType(class: Product::class)] ``` - - - -```php -/** - * @ExtendType(class=Product::class) - */ -``` - - - - -With the `@ExtendType` annotation, we tell GraphQLite that we want to add fields in the GraphQL type mapped to +With the `#[ExtendType]` attribute, we tell GraphQLite that we want to add fields in the GraphQL type mapped to the `Product` PHP class. ```php @@ -218,14 +108,6 @@ If you are using the Symfony bundle (or a framework with autowiring like Laravel is usually not an issue as the container will automatically create the controller entry if you do not explicitly declare it.
- - - ```php #[Field] public function getName(Product $product, string $language): string @@ -234,23 +116,7 @@ public function getName(Product $product, string $language): string } ``` - - - -```php -/** - * @Field() - */ -public function getName(Product $product, string $language): string -{ - return $this->translationService->getProductName($product->getId(), $language); -} -``` - - - - -The `@Field` annotation is used to add the "name" field to the `Product` type. +The `#[Field]` attribute is used to add the "name" field to the `Product` type. Take a close look at the signature. The first parameter is the "resolved object" we are working on. Any additional parameters are used as arguments. diff --git a/website/docs/external-type-declaration.mdx b/website/docs/external-type-declaration.mdx index d11cd9ad77..428fad7f8e 100644 --- a/website/docs/external-type-declaration.mdx +++ b/website/docs/external-type-declaration.mdx @@ -4,28 +4,17 @@ title: External type declaration sidebar_label: External type declaration --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -In some cases, you cannot or do not want to put an annotation on a domain class. +In some cases, you cannot or do not want to put an attribute on a domain class. For instance: * The class you want to annotate is part of a third party library and you cannot modify it -* You are doing domain-driven design and don't want to clutter your domain object with annotations from the view layer +* You are doing domain-driven design and don't want to clutter your domain object with attributes from the view layer * etc. -## `@Type` annotation with the `class` attribute - -GraphQLite allows you to use a *proxy* class thanks to the `@Type` annotation with the `class` attribute: +## `#[Type]` attribute with the `class` attribute - - +GraphQLite allows you to use a *proxy* class thanks to the `#[Type]` attribute with the `class` attribute: ```php namespace App\Types; @@ -45,34 +34,6 @@ class ProductType } ``` - - - -```php -namespace App\Types; - -use TheCodingMachine\GraphQLite\Annotations\Type; -use TheCodingMachine\GraphQLite\Annotations\Field; -use App\Entities\Product; - -/** - * @Type(class=Product::class) - */ -class ProductType -{ - /** - * @Field() - */ - public function getId(Product $product): string - { - return $product->getId(); - } -} -``` - - - - The `ProductType` class must be in the *types* namespace. You configured this namespace when you installed GraphQLite. The `ProductType` class is actually a **service**. You can therefore inject dependencies in it. @@ -82,19 +43,11 @@ If you are using the Symfony bundle (or a framework with autowiring like Laravel is usually not an issue as the container will automatically create the controller entry if you do not explicitly declare it.
-In methods with a `@Field` annotation, the first parameter is the *resolved object* we are working on. Any additional parameters are used as arguments. - -## `@SourceField` annotation +In methods with a `#[Field]` attribute, the first parameter is the *resolved object* we are working on. Any additional parameters are used as arguments. -If you don't want to rewrite all *getters* of your base class, you may use the `@SourceField` annotation: +## `#[SourceField]` attribute - - +If you don't want to rewrite all *getters* of your base class, you may use the `#[SourceField]` attribute: ```php use TheCodingMachine\GraphQLite\Annotations\Type; @@ -109,43 +62,14 @@ class ProductType } ``` - - - -```php -use TheCodingMachine\GraphQLite\Annotations\Type; -use TheCodingMachine\GraphQLite\Annotations\SourceField; -use App\Entities\Product; - -/** - * @Type(class=Product::class) - * @SourceField(name="name") - * @SourceField(name="price") - */ -class ProductType -{ -} -``` - - - - By doing so, you let GraphQLite know that the type exposes the `getName` method of the underlying `Product` object. Internally, GraphQLite will look for methods named `name()`, `getName()` and `isName()`). You can set different name to look for with `sourceName` attribute. -## `@MagicField` annotation - -If your object has no getters, but instead uses magic properties (using the magic `__get` method), you should use the `@MagicField` annotation: +## `#[MagicField]` attribute - - +If your object has no getters, but instead uses magic properties (using the magic `__get` method), you should use the `#[MagicField]` attribute: ```php use TheCodingMachine\GraphQLite\Annotations\Type; @@ -163,30 +87,6 @@ class ProductType } ``` - - - -```php -use TheCodingMachine\GraphQLite\Annotations\Type; -use TheCodingMachine\GraphQLite\Annotations\SourceField; -use App\Entities\Product; - -/** - * @Type() - * @MagicField(name="name", outputType="String!") - * @MagicField(name="price", outputType="Float") - */ -class ProductType -{ - public function __get(string $property) { - // return some magic property - } -} -``` - - - - By doing so, you let GraphQLite know that the type exposes "name" and the "price" magic properties of the underlying `Product` object. You can set different name to look for with `sourceName` attribute. @@ -197,7 +97,7 @@ of each property manually. ### Authentication and authorization -You may also check for logged users or users with a specific right using the "annotations" property. +You may also check for logged users or users with a specific right using the "annotations" argument. ```php use TheCodingMachine\GraphQLite\Annotations\Type; @@ -207,32 +107,20 @@ use TheCodingMachine\GraphQLite\Annotations\Right; use TheCodingMachine\GraphQLite\Annotations\FailWith; use App\Entities\Product; -/** - * @Type(class=Product::class) - * @SourceField(name="name") - * @SourceField(name="price", annotations={@Logged, @Right(name="CAN_ACCESS_Price", @FailWith(null)})) - */ +#[Type(class: Product::class)] +#[SourceField(name: "name")] +#[SourceField(name: "price", annotations: [new Logged(), new Right("CAN_ACCESS_Price"), new FailWith(null)])] class ProductType extends AbstractAnnotatedObjectType { } ``` -Any annotations described in the [Authentication and authorization page](authentication-authorization.mdx), or any annotation this is actually a ["field middleware"](field-middlewares.md) can be used in the `@SourceField` "annotations" attribute. - -
Heads up! The "annotation" attribute in @SourceField and @MagicField is only available as a Doctrine annotations. You cannot use it in PHP 8 attributes (because PHP 8 attributes cannot be nested)
+Any attributes described in the [Authentication and authorization page](authentication-authorization.mdx), or any attribute this is actually a ["field middleware"](field-middlewares.md) can be used in the `#[SourceField]` "annotations" argument. -## Declaring fields dynamically (without annotations) +## Declaring fields dynamically (without attributes) -In some very particular cases, you might not know exactly the list of `@SourceField` annotations at development time. -If you need to decide the list of `@SourceField` at runtime, you can implement the `FromSourceFieldsInterface`: - - - +In some very particular cases, you might not know exactly the list of `#[SourceField]` attributes at development time. +If you need to decide the list of `#[SourceField]` at runtime, you can implement the `FromSourceFieldsInterface`: ```php use TheCodingMachine\GraphQLite\FromSourceFieldsInterface; @@ -251,38 +139,7 @@ class ProductType implements FromSourceFieldsInterface // You may want to enable fields conditionally based on feature flags... if (ENABLE_STATUS_GLOBALLY) { return [ - new SourceField(['name'=>'status', 'logged'=>true]), - ]; - } else { - return []; - } - } -} -``` - - - - -```php -use TheCodingMachine\GraphQLite\FromSourceFieldsInterface; - -/** - * @Type(class=Product::class) - */ -class ProductType implements FromSourceFieldsInterface -{ - /** - * Dynamically returns the array of source fields - * to be fetched from the original object. - * - * @return SourceFieldInterface[] - */ - public function getSourceFields(): array - { - // You may want to enable fields conditionally based on feature flags... - if (ENABLE_STATUS_GLOBALLY) { - return [ - new SourceField(['name'=>'status', 'logged'=>true]), + new SourceField(['name'=>'status', 'annotations'=>[new Logged()]]), ]; } else { return []; @@ -290,6 +147,3 @@ class ProductType implements FromSourceFieldsInterface } } ``` - - - diff --git a/website/docs/field-middlewares.md b/website/docs/field-middlewares.md index 18eac15b30..df7b2bfaee 100644 --- a/website/docs/field-middlewares.md +++ b/website/docs/field-middlewares.md @@ -1,15 +1,15 @@ --- id: field-middlewares -title: Adding custom annotations with Field middlewares -sidebar_label: Custom annotations +title: Adding custom attributes with Field middlewares +sidebar_label: Custom attributes --- Available in GraphQLite 4.0+ -Just like the `@Logged` or `@Right` annotation, you can develop your own annotation that extends/modifies the behaviour of a field/query/mutation. +Just like the `#[Logged]` or `#[Right]` attribute, you can develop your own attribute that extends/modifies the behaviour of a field/query/mutation.
- If you want to create an annotation that targets a single argument (like @AutoWire(for="$service")), you should rather check the documentation about custom argument resolving + If you want to create an attribute that targets a single argument (like #[AutoWire]), you should rather check the documentation about custom argument resolving
## Field middlewares @@ -63,16 +63,16 @@ If you want the field to purely disappear, your middleware can return `null`, al field middlewares only get called once per Schema instance. If you use a long-running server (like Laravel Octane, Swoole, RoadRunner etc) and share the same Schema instance across requests, you will not be able to hide fields based on request data. -## Annotations parsing +## Attributes parsing Take a look at the `QueryFieldDescriptor::getMiddlewareAnnotations()`. -It returns the list of annotations applied to your field that implements the `MiddlewareAnnotationInterface`. +It returns the list of attributes applied to your field that implements the `MiddlewareAnnotationInterface`. -Let's imagine you want to add a `@OnlyDebug` annotation that displays a field/query/mutation only in debug mode (and +Let's imagine you want to add a `#[OnlyDebug]` attribute that displays a field/query/mutation only in debug mode (and hides the field in production). That could be useful, right? -First, we have to define the annotation. Annotations are handled by the great [doctrine/annotations](https://www.doctrine-project.org/projects/doctrine-annotations/en/1.6/index.html) library (for PHP 7+) and/or by PHP 8 attributes. +First, we have to define the attribute. ```php title="OnlyDebug.php" namespace App\Annotations; @@ -80,19 +80,15 @@ namespace App\Annotations; use Attribute; use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotationInterface; -/** - * @Annotation - * @Target({"METHOD", "ANNOTATION"}) - */ #[Attribute(Attribute::TARGET_METHOD)] class OnlyDebug implements MiddlewareAnnotationInterface { } ``` -Apart from being a classical annotation/attribute, this class implements the `MiddlewareAnnotationInterface`. This interface is a "marker" interface. It does not have any methods. It is just used to tell GraphQLite that this annotation is to be used by middlewares. +Apart from being a classical attribute, this class implements the `MiddlewareAnnotationInterface`. This interface is a "marker" interface. It does not have any methods. It is just used to tell GraphQLite that this attribute is to be used by middlewares. -Now, we can write a middleware that will act upon this annotation. +Now, we can write a middleware that will act upon this attribute. ```php namespace App\Middlewares; @@ -103,7 +99,7 @@ use GraphQL\Type\Definition\FieldDefinition; use TheCodingMachine\GraphQLite\QueryFieldDescriptor; /** - * Middleware in charge of hiding a field if it is annotated with @OnlyDebug and the DEBUG constant is not set + * Middleware in charge of hiding a field if it is annotated with #[OnlyDebug] and the DEBUG constant is not set */ class OnlyDebugFieldMiddleware implements FieldMiddlewareInterface { @@ -117,7 +113,7 @@ class OnlyDebugFieldMiddleware implements FieldMiddlewareInterface $onlyDebug = $annotations->getAnnotationByType(OnlyDebug::class); if ($onlyDebug !== null && !DEBUG) { - // If the onlyDebug annotation is present, returns null. + // If the onlyDebug attribute is present, returns null. // Returning null will hide the field. return null; } diff --git a/website/docs/file-uploads.mdx b/website/docs/file-uploads.mdx index 8c0782f89b..487a413948 100644 --- a/website/docs/file-uploads.mdx +++ b/website/docs/file-uploads.mdx @@ -4,9 +4,6 @@ title: File uploads sidebar_label: File uploads --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - GraphQL does not support natively the notion of file uploads, but an extension to the GraphQL protocol was proposed to add support for [multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec). @@ -40,14 +37,6 @@ for more information on how to integrate it in your framework. To handle an uploaded file, you type-hint against the PSR-7 `UploadedFileInterface`: - - - ```php class MyController { @@ -60,26 +49,6 @@ class MyController } ``` - - - -```php -class MyController -{ - /** - * @Mutation - */ - public function saveDocument(string $name, UploadedFileInterface $file): Document - { - // Some code that saves the document. - $file->moveTo($someDir); - } -} -``` - - - - Of course, you need to use a GraphQL client that is compatible with multipart requests. See [jaydenseric/graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec#client) for a list of compatible clients. The GraphQL client must send the file using the Upload type. diff --git a/website/docs/fine-grained-security.mdx b/website/docs/fine-grained-security.mdx index f5d2fff032..3fac998ff4 100644 --- a/website/docs/fine-grained-security.mdx +++ b/website/docs/fine-grained-security.mdx @@ -4,29 +4,19 @@ title: Fine grained security sidebar_label: Fine grained security --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -If the [`@Logged` and `@Right` annotations](authentication-authorization.mdx#logged-and-right-annotations) are not -granular enough for your needs, you can use the advanced `@Security` annotation. +If the [`#[Logged]` and `#[Right]` attributes](authentication-authorization.mdx#logged-and-right-annotations) are not +granular enough for your needs, you can use the advanced `#[Security]` attribute. -Using the `@Security` annotation, you can write an *expression* that can contain custom logic. For instance: +Using the `#[Security]` attribute, you can write an *expression* that can contain custom logic. For instance: - Check that a user can access a given resource - Check that a user has one right or another right - ... -## Using the @Security annotation +## Using the #[Security] attribute -The `@Security` annotation is very flexible: it allows you to pass an expression that can contains custom logic: - - - +The `#[Security]` attribute is very flexible: it allows you to pass an expression that can contains custom logic: ```php use TheCodingMachine\GraphQLite\Annotations\Security; @@ -41,33 +31,12 @@ public function getPost(Post $post): array } ``` - - - -```php -use TheCodingMachine\GraphQLite\Annotations\Security; - -// ... - -/** - * @Query - * @Security("is_granted('ROLE_ADMIN') or is_granted('POST_SHOW', post)") - */ -public function getPost(Post $post): array -{ - // ... -} -``` - - - - -The *expression* defined in the `@Security` annotation must conform to [Symfony's Expression Language syntax](https://symfony.com/doc/4.4/components/expression_language/syntax.html) +The *expression* defined in the `#[Security]` attribute must conform to [Symfony's Expression Language syntax](https://symfony.com/doc/4.4/components/expression_language/syntax.html)
- If you are a Symfony user, you might already be used to the @Security annotation. Most of the inspiration - of this annotation comes from Symfony. Warning though! GraphQLite's @Security annotation and - Symfony's @Security annotation are slightly different. Especially, the two annotations do not live + If you are a Symfony user, you might already be used to the #[Security] attribute. Most of the inspiration + of this attribute comes from Symfony. Warning though! GraphQLite's #[Security] attribute and + Symfony's #[Security] attribute are slightly different. Especially, the two attributes do not live in the same namespace!
@@ -75,62 +44,18 @@ The *expression* defined in the `@Security` annotation must conform to [Symfony' Use the `is_granted` function to check if a user has a special right. - - - ```php #[Security("is_granted('ROLE_ADMIN')")] ``` - - - -```php -@Security("is_granted('ROLE_ADMIN')") -``` - - - - is similar to - - - ```php #[Right("ROLE_ADMIN")] ``` - - - -```php -@Right("ROLE_ADMIN") -``` - - - - In addition, the `is_granted` function accepts a second optional parameter: the "scope" of the right. - - - ```php #[Query] #[Security("is_granted('POST_SHOW', post)")] @@ -140,37 +65,12 @@ public function getPost(Post $post): array } ``` - - - -```php -/** - * @Query - * @Security("is_granted('POST_SHOW', post)") - */ -public function getPost(Post $post): array -{ - // ... -} -``` - - - - In the example above, the `getPost` method can be called only if the logged user has the 'POST_SHOW' permission on the `$post` object. You can notice that the `$post` object comes from the parameters. ## Accessing method parameters -All parameters passed to the method can be accessed in the `@Security` expression. - - - +All parameters passed to the method can be accessed in the `#[Security]` expression. ```php #[Query] @@ -181,38 +81,12 @@ public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDat } ``` - - - -```php -/** - * @Query - * @Security("startDate < endDate", statusCode=400, message="End date must be after start date") - */ -public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDate): array -{ - // ... -} -``` - - - - - -In the example above, we tweak a bit the Security annotation purpose to do simple input validation. +In the example above, we tweak a bit the Security attribute purpose to do simple input validation. ## Setting HTTP code and error message You can use the `statusCode` and `message` attributes to set the HTTP code and GraphQL error message. - - - ```php #[Query] #[Security(expression: "is_granted('POST_SHOW', post)", statusCode: 404, message: "Post not found (let's pretend the post does not exists!)")] @@ -222,23 +96,6 @@ public function getPost(Post $post): array } ``` - - - -```php -/** - * @Query - * @Security("is_granted('POST_SHOW', post)", statusCode=404, message="Post not found (let's pretend the post does not exists!)") - */ -public function getPost(Post $post): array -{ - // ... -} -``` - - - - Note: since a single GraphQL call contain many errors, 2 errors might have conflicting HTTP status code. The resulting status code is up to the GraphQL middleware you use. Most of the time, the status code with the higher error code will be returned. @@ -248,14 +105,6 @@ higher error code will be returned. If you do not want an error to be thrown when the security condition is not met, you can use the `failWith` attribute to set a default value. - - - ```php #[Query] #[Security(expression: "is_granted('CAN_SEE_MARGIN', this)", failWith: null)] @@ -265,25 +114,8 @@ public function getMargin(): float } ``` - - - -```php -/** - * @Field - * @Security("is_granted('CAN_SEE_MARGIN', this)", failWith=null) - */ -public function getMargin(): float -{ - // ... -} -``` - - - - -The `failWith` attribute behaves just like the [`@FailWith` annotation](authentication-authorization.mdx#not-throwing-errors) -but for a given `@Security` annotation. +The `failWith` attribute behaves just like the [`#[FailWith]` attribute](authentication-authorization.mdx#not-throwing-errors) +but for a given `#[Security]` attribute. You cannot use the `failWith` attribute along `statusCode` or `message` attributes. @@ -292,13 +124,6 @@ You cannot use the `failWith` attribute along `statusCode` or `message` attribut You can use the `user` variable to access the currently logged user. You can use the `is_logged()` function to check if a user is logged or not. - - ```php #[Query] @@ -309,35 +134,10 @@ public function getNSFWImages(): array } ``` - - - -```php -/** - * @Query - * @Security("is_logged() && user.age > 18") - */ -public function getNSFWImages(): array -{ - // ... -} -``` - - - - ## Accessing the current object You can use the `this` variable to access any (public) property / method of the current class. - - - ```php class Post { #[Field] @@ -354,61 +154,19 @@ class Post { } ``` - - - -```php -class Post { - /** - * @Field - * @Security("this.canAccessBody(user)") - */ - public function getBody(): array - { - // ... - } - - public function canAccessBody(User $user): bool - { - // Some custom logic here - } -} -``` - - - - ## Available scope -The `@Security` annotation can be used in any query, mutation or field, so anywhere you have a `@Query`, `@Mutation` -or `@Field` annotation. +The `#[Security]` attribute can be used in any query, mutation or field, so anywhere you have a `#[Query]`, `#[Mutation]` +or `#[Field]` attribute. ## How to restrict access to a given resource The `is_granted` method can be used to restrict access to a specific resource. - - - ```php #[Security("is_granted('POST_SHOW', post)")] ``` - - - -```php -@Security("is_granted('POST_SHOW', post)") -``` - - - - If you are wondering how to configure these fine-grained permissions, this is not something that GraphQLite handles itself. Instead, this depends on the framework you are using. diff --git a/website/docs/implementing-security.md b/website/docs/implementing-security.md index ce3c8fb43f..88bbd8f7f2 100644 --- a/website/docs/implementing-security.md +++ b/website/docs/implementing-security.md @@ -50,8 +50,8 @@ You need to write classes that implement these interfaces. Then, you must regist It you are [using the `SchemaFactory`](other-frameworks.mdx), you can register your classes using: ```php -// Configure an authentication service (to resolve the @Logged annotations). +// Configure an authentication service (to resolve the #[Logged] attribute). $schemaFactory->setAuthenticationService($myAuthenticationService); -// Configure an authorization service (to resolve the @Right annotations). +// Configure an authorization service (to resolve the #[Right] attribute). $schemaFactory->setAuthorizationService($myAuthorizationService); ``` diff --git a/website/docs/inheritance-interfaces.mdx b/website/docs/inheritance-interfaces.mdx index 05f0404a0e..5467492c52 100644 --- a/website/docs/inheritance-interfaces.mdx +++ b/website/docs/inheritance-interfaces.mdx @@ -4,23 +4,12 @@ title: Inheritance and interfaces sidebar_label: Inheritance and interfaces --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## Modeling inheritance Some of your entities may extend other entities. GraphQLite will do its best to represent this hierarchy of objects in GraphQL using interfaces. Let's say you have two classes, `Contact` and `User` (which extends `Contact`): - - - ```php #[Type] class Contact @@ -35,40 +24,8 @@ class User extends Contact } ``` - - - -```php -/** - * @Type - */ -class Contact -{ - // ... -} - -/** - * @Type - */ -class User extends Contact -{ - // ... -} -``` - - - - Now, let's assume you have a query that returns a contact: - - - ```php class ContactController { @@ -80,25 +37,6 @@ class ContactController } ``` - - - -```php -class ContactController -{ - /** - * @Query() - */ - public function getContact(): Contact - { - // ... - } -} -``` - - - - When writing your GraphQL query, you are able to use fragments to retrieve fields from the `User` type: ```graphql @@ -135,15 +73,7 @@ available in the `Contact` type. ## Mapping interfaces -If you want to create a pure GraphQL interface, you can also add a `@Type` annotation on a PHP interface. - - - +If you want to create a pure GraphQL interface, you can also add a `#[Type]` attribute on a PHP interface. ```php #[Type] @@ -154,25 +84,6 @@ interface UserInterface } ``` - - - -```php -/** - * @Type - */ -interface UserInterface -{ - /** - * @Field - */ - public function getUserName(): string; -} -``` - - - - This will automatically create a GraphQL interface whose description is: ```graphql @@ -186,14 +97,6 @@ interface UserInterface { You don't have to do anything special to implement an interface in your GraphQL types. Simply "implement" the interface in PHP and you are done! - - - ```php #[Type] class User implements UserInterface @@ -202,22 +105,6 @@ class User implements UserInterface } ``` - - - -```php -/** - * @Type - */ -class User implements UserInterface -{ - public function getUserName(): string; -} -``` - - - - This will translate in GraphQL schema as: ```graphql @@ -230,21 +117,13 @@ type User implements UserInterface { } ``` -Please note that you do not need to put the `@Field` annotation again in the implementing class. +Please note that you do not need to put the `#[Field]` attribute again in the implementing class. ### Interfaces without an explicit implementing type -You don't have to explicitly put a `@Type` annotation on the class implementing the interface (though this +You don't have to explicitly put a `#[Type]` attribute on the class implementing the interface (though this is usually a good idea). - - - ```php /** * Look, this class has no #Type attribute @@ -266,39 +145,10 @@ class UserController } ``` - - - -```php -/** - * Look, this class has no @Type annotation - */ -class User implements UserInterface -{ - public function getUserName(): string; -} -``` - -```php -class UserController -{ - /** - * @Query() - */ - public function getUser(): UserInterface // This will work! - { - // ... - } -} -``` - - - -
If GraphQLite cannot find a proper GraphQL Object type implementing an interface, it will create an object type "on the fly".
-In the example above, because the `User` class has no `@Type` annotations, GraphQLite will +In the example above, because the `User` class has no `#[Type]` attribute, GraphQLite will create a `UserImpl` type that implements `UserInterface`. ```graphql diff --git a/website/docs/input-types.mdx b/website/docs/input-types.mdx index f2c62afd40..c0efa59e87 100644 --- a/website/docs/input-types.mdx +++ b/website/docs/input-types.mdx @@ -4,21 +4,10 @@ title: Input types sidebar_label: Input types --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - Let's assume you are developing an API that returns a list of cities around a location. Your GraphQL query might look like this: - - - ```php class MyController { @@ -56,49 +45,6 @@ class Location } ``` - - - -```php -class MyController -{ - /** - * @Query - * @return City[] - */ - public function getCities(Location $location, float $radius): array - { - // Some code that returns an array of cities. - } -} - -// Class Location is a simple value-object. -class Location -{ - private $latitude; - private $longitude; - - public function __construct(float $latitude, float $longitude) - { - $this->latitude = $latitude; - $this->longitude = $longitude; - } - - public function getLatitude(): float - { - return $this->latitude; - } - - public function getLongitude(): float - { - return $this->longitude; - } -} -``` - - - - If you try to run this code, you will get the following error: ``` @@ -115,14 +61,6 @@ There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` Using the `#[Input]` attribute, we can transform the `Location` class, in the example above, into an input type. Just add the `#[Field]` attribute to the corresponding properties: - - - ```php #[Input] class Location @@ -160,88 +98,25 @@ class Location } ``` - - - -```php -/** - * @Input - */ -class Location -{ - - /** - * @Field - * @var string|null - */ - private ?string $name = null; - - /** - * @Field - * @var float - */ - private $latitude; - - /** - * @Field - * @var float - */ - private $longitude; - - public function __construct(float $latitude, float $longitude) - { - $this->latitude = $latitude; - $this->longitude = $longitude; - } - - public function setName(string $name): void - { - $this->name = $name; - } - - public function getLatitude(): float - { - return $this->latitude; - } - - public function getLongitude(): float - { - return $this->longitude; - } -} -``` - - - - Now if you call the `getCities` query, from the controller in the first example, the `Location` object will be automatically instantiated with the user provided, `latitude` / `longitude` properties, and passed to the controller as a parameter. There are some important things to notice: -- The `@Field` annotation is recognized on properties for Input Type, as well as setters. +- The `#[Field]` attribute is recognized on properties for Input Type, as well as setters. - There are 3 ways for fields to be resolved: - Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above. - If properties are public, they will be just set without any additional effort - no constructor required. - - For private or protected properties implemented, a public setter is required (if they are not set via the constructor). For example `setLatitude(float $latitude)`. You can also put the `@Field` annotation on the setter, instead of the property, allowing you to have use many other attributes (`Security`, `Right`, `Autowire`, etc.). + - For private or protected properties implemented, a public setter is required (if they are not set via the constructor). For example `setLatitude(float $latitude)`. You can also put the `#[Field]` attribute on the setter, instead of the property, allowing you to have use many other attributes (`Security`, `Right`, `Autowire`, etc.). - For validation of these Input Types, see the [Custom InputType Validation section](validation#custom-inputtype-validation). - It's advised to use the `#[Input]` attribute on DTO style input type objects and not directly on your model objects. Using it on your model objects can cause coupling in undesirable ways. ### Multiple Input Types from the same class -Simple usage of the `@Input` annotation on a class creates a GraphQL input named by class name + "Input" suffix if a class name does not end with it already. Ex. `LocationInput` for `Location` class. +Simple usage of the `#[Input]` attribute on a class creates a GraphQL input named by class name + "Input" suffix if a class name does not end with it already. Ex. `LocationInput` for `Location` class. -You can add multiple `@Input` annotations to the same class, give them different names and link different fields. +You can add multiple `#[Input]` attributed to the same class, give them different names and link different fields. Consider the following example: - - - ```php #[Input(name: 'CreateUserInput', default: true)] #[Input(name: 'UpdateUserInput', update: true)] @@ -269,53 +144,6 @@ class UserInput } ``` - - - -```php -/** - * @Input(name="CreateUserInput", default=true) - * @Input(name="UpdateUserInput", update=true) - */ -class UserInput -{ - - /** - * @Field() - * @var string - */ - public $username; - - /** - * @Field(for="CreateUserInput") - * @var string - */ - public string $email; - - /** - * @Field(for="CreateUserInput", inputType="String!") - * @Field(for="UpdateUserInput", inputType="String") - * @var string|null - */ - public $password; - - /** @var int|null */ - protected $age; - - /** - * @Field() - * @param int|null $age - */ - public function setAge(?int $age): void - { - $this->age = $age; - } -} -``` - - - - There are 2 input types added to the `UserInput` class: `CreateUserInput` and `UpdateUserInput`. A few notes: - `CreateUserInput` input will be used by default for this class. - Field `username` is created for both input types, and it is required because the property type is not nullable. @@ -335,19 +163,11 @@ A **Factory** is a method that takes in parameter all the fields of the input ty Here is an example of factory: - - - ```php class MyFactory { /** - * The Factory annotation will create automatically a LocationInput input type in GraphQL. + * The Factory attribute will create automatically a LocationInput input type in GraphQL. */ #[Factory] public function createLocation(float $latitude, float $longitude): Location @@ -357,27 +177,6 @@ class MyFactory } ``` - - - -```php -class MyFactory -{ - /** - * The Factory annotation will create automatically a LocationInput input type in GraphQL. - * - * @Factory() - */ - public function createLocation(float $latitude, float $longitude): Location - { - return new Location($latitude, $longitude); - } -} -``` - - - - and now, you can run query like this: ```graphql @@ -394,7 +193,7 @@ query { } ``` -- Factories must be declared with the **@Factory** annotation. +- Factories must be declared with the **#[Factory]** attribute. - The parameters of the factories are the field of the GraphQL input type A few important things to notice: @@ -409,14 +208,6 @@ The GraphQL input type name is derived from the return type of the factory. Given the factory below, the return type is "Location", therefore, the GraphQL input type will be named "LocationInput". - - - ```php #[Factory] public function createLocation(float $latitude, float $longitude): Location @@ -425,48 +216,12 @@ public function createLocation(float $latitude, float $longitude): Location } ``` - - - -```php -/** - * @Factory() - */ -public function createLocation(float $latitude, float $longitude): Location -{ - return new Location($latitude, $longitude); -} -``` - - - - -In case you want to override the input type name, you can use the "name" attribute of the @Factory annotation: - - - +In case you want to override the input type name, you can use the "name" attribute of the #[Factory] attribute: ```php #[Factory(name: 'MyNewInputName', default: true)] ``` - - - -```php -/** - * @Factory(name="MyNewInputName", default=true) - */ -``` - - - - Note that you need to add the "default" attribute is you want your factory to be used by default (more on this in the next chapter). @@ -475,18 +230,10 @@ to you, so there is no real reason to customize it. ### Forcing an input type -You can use the `@UseInputType` annotation to force an input type of a parameter. +You can use the `#[UseInputType]` attribute to force an input type of a parameter. Let's say you want to force a parameter to be of type "ID", you can use this: - - - ```php #[Factory] #[UseInputType(for: "$id", inputType:"ID!")] @@ -496,41 +243,16 @@ public function getProductById(string $id): Product } ``` - - - -```php -/** - * @Factory() - * @UseInputType(for="$id", inputType="ID!") - */ -public function getProductById(string $id): Product -{ - return $this->productRepository->findById($id); -} -``` - - - - ### Declaring several input types for the same PHP class Available in GraphQLite 4.0+ There are situations where a given PHP class might use one factory or another depending on the context. This is often the case when your objects map database entities. -In these cases, you can use combine the use of `@UseInputType` and `@Factory` annotation to achieve your goal. +In these cases, you can use combine the use of `#[UseInputType]` and `#[Factory]` attribute to achieve your goal. Here is an annotated sample: - - - ```php /** * This class contains 2 factories to create Product objects. @@ -584,66 +306,6 @@ class ProductController } ``` - - - -```php -/** - * This class contains 2 factories to create Product objects. - * The "getProduct" method is used by default to map "Product" classes. - * The "createProduct" method will generate another input type named "CreateProductInput" - */ -class ProductFactory -{ - // ... - - /** - * This factory will be used by default to map "Product" classes. - * @Factory(name="ProductRefInput", default=true) - */ - public function getProduct(string $id): Product - { - return $this->productRepository->findById($id); - } - /** - * We specify a name for this input type explicitly. - * @Factory(name="CreateProductInput", default=false) - */ - public function createProduct(string $name, string $type): Product - { - return new Product($name, $type); - } -} - -class ProductController -{ - /** - * The "createProduct" factory will be used for this mutation. - * - * @Mutation - * @UseInputType(for="$product", inputType="CreateProductInput!") - */ - public function saveProduct(Product $product): Product - { - // ... - } - - /** - * The default "getProduct" factory will be used for this query. - * - * @Query - * @return Color[] - */ - public function availableColors(Product $product): array - { - // ... - } -} -``` - - - - ### Ignoring some parameters Available in GraphQLite 4.0+ @@ -654,14 +316,6 @@ Image your `getProductById` has an additional `lazyLoad` parameter. This paramet directly the function in PHP because you can have some level of optimisation on your code. But it is not something that you want to expose in the GraphQL API. Let's hide it! - - - ```php #[Factory] public function getProductById( @@ -674,23 +328,6 @@ public function getProductById( } ``` - - - -```php -/** - * @Factory() - * @HideParameter(for="$lazyLoad") - */ -public function getProductById(string $id, bool $lazyLoad = true): Product -{ - return $this->productRepository->findById($id, $lazyLoad); -} -``` - - - - -With the `@HideParameter` annotation, you can choose to remove from the GraphQL schema any argument. +With the `#[HideParameter]` attribute, you can choose to remove from the GraphQL schema any argument. To be able to hide an argument, the argument must have a default value. diff --git a/website/docs/internals.md b/website/docs/internals.md index 65624fcc49..93c7e958b8 100644 --- a/website/docs/internals.md +++ b/website/docs/internals.md @@ -92,7 +92,7 @@ Class type mappers are mapping PHP classes to GraphQL object types. GraphQLite provide 3 default implementations: - `CompositeTypeMapper`: a type mapper that delegates mapping to other type mappers using the Composite Design Pattern. -- `GlobTypeMapper`: scans classes in a directory for the `@Type` or `@ExtendType` annotation and maps those to GraphQL types +- `GlobTypeMapper`: scans classes in a directory for the `#[Type]` or `#[ExtendType]` attribute and maps those to GraphQL types - `PorpaginasTypeMapper`: maps and class implementing the Porpaginas `Result` interface to a [special paginated type](pagination.mdx). ### Registering a type mapper in Symfony @@ -124,9 +124,9 @@ Let's have a look at a simple query: ```php /** - * @Query * @return Product[] */ +#[Query] public function products(ResolveInfo $info): array ``` diff --git a/website/docs/laravel-package-advanced.mdx b/website/docs/laravel-package-advanced.mdx index 4f63bedcf8..f4f9918427 100644 --- a/website/docs/laravel-package-advanced.mdx +++ b/website/docs/laravel-package-advanced.mdx @@ -4,9 +4,6 @@ title: "Laravel package: advanced usage" sidebar_label: Laravel specific features --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -
Be advised! This documentation will be removed in a future release. For current and up-to-date Laravel extension specific documentation, please see the Github repository.
@@ -15,17 +12,9 @@ The Laravel package comes with a number of features to ease the integration of G ## Support for Laravel validation rules -The GraphQLite Laravel package comes with a special `@Validate` annotation to use Laravel validation rules in your +The GraphQLite Laravel package comes with a special `#[Validate]` attribute to use Laravel validation rules in your input types. - - - ```php use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate; @@ -44,30 +33,7 @@ class MyController } ``` - - - -```php -use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate; - -class MyController -{ - /** - * @Mutation - * @Validate(for="$email", rule="email|unique:users") - * @Validate(for="$password", rule="gte:8") - */ - public function createUser(string $email, string $password): User - { - // ... - } -} -``` - - - - -You can use the `@Validate` annotation in any query / mutation / field / factory / decorator. +You can use the `#[Validate]` attribute in any query / mutation / field / factory / decorator. If a validation fails to pass, the message will be printed in the "errors" section and you will get a HTTP 400 status code: @@ -99,14 +65,6 @@ You can use any validation rule described in [the Laravel documentation](https:/ In your query, if you explicitly return an object that extends the `Illuminate\Pagination\LengthAwarePaginator` class, the query result will be wrapped in a "paginator" type. - - - ```php class MyController { @@ -121,27 +79,6 @@ class MyController } ``` - - - -```php -class MyController -{ - /** - * @Query - * @return Product[] - */ - public function products(): Illuminate\Pagination\LengthAwarePaginator - { - return Product::paginate(15); - } -} -``` - - - - - Notice that: - the method return type MUST BE `Illuminate\Pagination\LengthAwarePaginator` or a class extending `Illuminate\Pagination\LengthAwarePaginator` @@ -177,14 +114,6 @@ products { Note: if you are using `simplePaginate` instead of `paginate`, you can type hint on the `Illuminate\Pagination\Paginator` class. - - - ```php class MyController { @@ -199,46 +128,18 @@ class MyController } ``` - - - -```php -class MyController -{ - /** - * @Query - * @return Product[] - */ - public function products(): Illuminate\Pagination\Paginator - { - return Product::simplePaginate(15); - } -} -``` - - - - The behaviour will be exactly the same except you will be missing the `totalCount` and `lastPage` fields. ## Using GraphQLite with Eloquent efficiently -In GraphQLite, you are supposed to put a `@Field` annotation on each getter. +In GraphQLite, you are supposed to put a `#[Field]` attribute on each getter. Eloquent uses PHP magic properties to expose your database records. Because Eloquent relies on magic properties, it is quite rare for an Eloquent model to have proper getters and setters. -So we need to find a workaround. GraphQLite comes with a `@MagicField` annotation to help you +So we need to find a workaround. GraphQLite comes with a `#[MagicField]` attribute to help you working with magic properties. - - - ```php #[Type] #[MagicField(name: "id", outputType: "ID!")] @@ -249,24 +150,6 @@ class Product extends Model } ``` - - - -```php -/** - * @Type() - * @MagicField(name="id", outputType="ID!") - * @MagicField(name="name", phpType="string") - * @MagicField(name="categories", phpType="Category[]") - */ -class Product extends Model -{ -} -``` - - - - Please note that since the properties are "magic", they don't have a type. Therefore, you need to pass either the "outputType" attribute with the GraphQL type matching the property, or the "phpType" attribute with the PHP type matching the property. @@ -288,7 +171,7 @@ class User extends Model } ``` -It would be tempting to put a `@Field` annotation on the `phone()` method, but this will not work. Indeed, +It would be tempting to put a `#[Field]` attribute on the `phone()` method, but this will not work. Indeed, the `phone()` method does not return a `App\Phone` object. It is the `phone` magic property that returns it. In short: @@ -299,9 +182,8 @@ In short: ```php class User extends Model { - /** - * @Field - */ + + #[Field] public function phone() { return $this->hasOne('App\Phone'); @@ -315,9 +197,7 @@ class User extends Model This works: ```php -/** - * @MagicField(name="phone", phpType="App\\Phone") - */ +#[MagicField(name: "phone", phpType:"App\\Phone")] class User extends Model { public function phone() diff --git a/website/docs/migrating.md b/website/docs/migrating.md index aa181f7fcc..2753878265 100644 --- a/website/docs/migrating.md +++ b/website/docs/migrating.md @@ -21,14 +21,14 @@ $ composer require ecodev/graphql-upload If you are a "regular" GraphQLite user, migration to v4 should be straightforward: -- Annotations are mostly untouched. The only annotation that is changed is the `@SourceField` annotation. - - Check your code for every places where you use the `@SourceField` annotation: +- Annotations are mostly untouched. The only annotation that is changed is the `#[SourceField]` annotation. + - Check your code for every places where you use the `#[SourceField]` annotation: - The "id" attribute has been remove (`@SourceField(id=true)`). Instead, use `@SourceField(outputType="ID")` - The "logged", "right" and "failWith" attributes have been removed (`@SourceField(logged=true)`). - Instead, use the annotations attribute with the same annotations you use for the `@Field` annotation: + Instead, use the annotations attribute with the same annotations you use for the `#[Field]` annotation: `@SourceField(annotations={@Logged, @FailWith(null)})` - - If you use magic property and were creating a getter for every magic property (to put a `@Field` annotation on it), - you can now replace this getter with a `@MagicField` annotation. + - If you use magic property and were creating a getter for every magic property (to put a `#[Field]` annotation on it), + you can now replace this getter with a `#[MagicField]` annotation. - In GraphQLite v3, the default was to hide a field from the schema if a user has no access to it. In GraphQLite v4, the default is to still show this field, but to throw an error if the user makes a query on it (this way, the schema is the same for all users). If you want the old mode, use the new diff --git a/website/docs/multiple-output-types.mdx b/website/docs/multiple-output-types.mdx index c404c9a5b6..2f0d5b1e8f 100644 --- a/website/docs/multiple-output-types.mdx +++ b/website/docs/multiple-output-types.mdx @@ -4,9 +4,6 @@ title: Mapping multiple output types for the same class sidebar_label: Class with multiple output types --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - Available in GraphQLite 4.0+ In most cases, you have one PHP class and you want to map it to one GraphQL output type. @@ -14,23 +11,13 @@ In most cases, you have one PHP class and you want to map it to one GraphQL outp But in very specific cases, you may want to use different GraphQL output type for the same class. For instance, depending on the context, you might want to prevent the user from accessing some fields of your object. -To do so, you need to create 2 output types for the same PHP class. You typically do this using the "default" attribute of the `@Type` annotation. +To do so, you need to create 2 output types for the same PHP class. You typically do this using the "default" attribute of the `#[Type]` attribute. ## Example Here is an example. Say we are manipulating products. When I query a `Product` details, I want to have access to all fields. But for some reason, I don't want to expose the price field of a product if I query the list of all products. - - - - - ```php #[Type] class Product @@ -51,51 +38,8 @@ class Product } ``` - - - - - -```php -/** - * @Type() - */ -class Product -{ - // ... - - /** - * @Field() - */ - public function getName(): string - { - return $this->name; - } - - /** - * @Field() - */ - public function getPrice(): ?float - { - return $this->price; - } -} -``` - - - - - The `Product` class is declaring a classic GraphQL output type named "Product". - - - ```php #[Type(class: Product::class, name: "LimitedProduct", default: false)] #[SourceField(name: "name")] @@ -105,27 +49,8 @@ class LimitedProductType } ``` - - - -```php -/** - * @Type(class=Product::class, name="LimitedProduct", default=false) - * @SourceField(name="name") - */ -class LimitedProductType -{ - // ... -} -``` - - - - - - The `LimitedProductType` also declares an ["external" type](external-type-declaration.mdx) mapping the `Product` class. -But pay special attention to the `@Type` annotation. +But pay special attention to the `#[Type]` attribute. First of all, we specify `name="LimitedProduct"`. This is useful to avoid having colliding names with the "Product" GraphQL output type that is already declared. @@ -135,14 +60,6 @@ This type will only be used when we explicitly request it. Finally, we can write our requests: - - - ```php class ProductController { @@ -162,35 +79,7 @@ class ProductController } ``` - - - -```php -class ProductController -{ - /** - * This field will use the default type. - * - * @Field - */ - public function getProduct(int $id): Product { /* ... */ } - - /** - * Because we use the "outputType" attribute, this field will use the other type. - * - * @Field(outputType="[LimitedProduct!]!") - * @return Product[] - */ - public function getProducts(): array { /* ... */ } -} -``` - - - - - - -Notice how the "outputType" attribute is used in the `@Field` annotation to force the output type. +Notice how the "outputType" attribute is used in the `#[Field]` attribute to force the output type. Is a result, when the end user calls the `product` query, we will have the possibility to fetch the `name` and `price` fields, but if he calls the `products` query, each product in the list will have a `name` field but no `price` field. We managed @@ -198,59 +87,19 @@ to successfully expose a different set of fields based on the query context. ## Extending a non-default type -If you want to extend a type using the `@ExtendType` annotation and if this type is declared as non-default, +If you want to extend a type using the `#[ExtendType]` attribute and if this type is declared as non-default, you need to target the type by name instead of by class. So instead of writing: - - - ```php #[ExtendType(class: Product::class)] ``` - - - -```php -/** - * @ExtendType(class=Product::class) - */ -``` - - - - you will write: - - - ```php #[ExtendType(name: "LimitedProduct")] ``` - - - -```php -/** - * @ExtendType(name="LimitedProduct") - */ -``` - - - - -Notice how we use the "name" attribute instead of the "class" attribute in the `@ExtendType` annotation. +Notice how we use the "name" attribute instead of the "class" attribute in the `#[ExtendType]` attribute. diff --git a/website/docs/mutations.mdx b/website/docs/mutations.mdx index 16ab03a72a..614705e6f6 100644 --- a/website/docs/mutations.mdx +++ b/website/docs/mutations.mdx @@ -4,23 +4,12 @@ title: Mutations sidebar_label: Mutations --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - In GraphQLite, mutations are created [like queries](queries.mdx). -To create a mutation, you must annotate a method in a controller with the `@Mutation` annotation. +To create a mutation, you must annotate a method in a controller with the `#[Mutation]` attribute. For instance: - - - ```php namespace App\Controller; @@ -36,25 +25,3 @@ class ProductController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Mutation; - -class ProductController -{ - /** - * @Mutation - */ - public function saveProduct(int $id, string $name, ?float $price = null): Product - { - // Some code that saves a product. - } -} -``` - - - diff --git a/website/docs/other-frameworks.mdx b/website/docs/other-frameworks.mdx index 4957449cc7..2bd411b0e1 100644 --- a/website/docs/other-frameworks.mdx +++ b/website/docs/other-frameworks.mdx @@ -4,9 +4,6 @@ title: Getting started with any framework sidebar_label: "Other frameworks / No framework" --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## Installation Open a terminal in your current project directory and run: @@ -50,9 +47,9 @@ or the [StandardServer class](https://webonyx.github.io/graphql-php/executing-qu The `SchemaFactory` class also comes with a number of methods that you can use to customize your GraphQLite settings. ```php -// Configure an authentication service (to resolve the @Logged annotations). +// Configure an authentication service (to resolve the #[Logged] attributes). $factory->setAuthenticationService(new VoidAuthenticationService()); -// Configure an authorization service (to resolve the @Right annotations). +// Configure an authorization service (to resolve the #[Right] attributes). $factory->setAuthorizationService(new VoidAuthorizationService()); // Change the naming convention of GraphQL types globally. $factory->setNamingStrategy(new NamingStrategy()); @@ -302,15 +299,6 @@ The application will look into the `App\Controllers` namespace for GraphQLite co It assumes that the container has an entry whose name is the controller's fully qualified class name. - - - - ```php title="src/Controllers/MyController.php" namespace App\Controllers; @@ -326,30 +314,6 @@ class MyController } ``` - - - -```php title="src/Controllers/MyController.php" -namespace App\Controllers; - -use TheCodingMachine\GraphQLite\Annotations\Query; - -class MyController -{ - /** - * @Query - */ - public function hello(string $name): string - { - return 'Hello '.$name; - } -} -``` - - - - - ```php title="config/container.php" use App\Controllers\MyController; diff --git a/website/docs/pagination.mdx b/website/docs/pagination.mdx index 66b58c863b..44ad427c53 100644 --- a/website/docs/pagination.mdx +++ b/website/docs/pagination.mdx @@ -4,9 +4,6 @@ title: Paginating large result sets sidebar_label: Pagination --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - It is quite common to have to paginate over large result sets. GraphQLite offers a simple way to do that using [Porpaginas](https://github.com/beberlei/porpaginas). @@ -29,14 +26,6 @@ $ composer require beberlei/porpaginas In your query, simply return a class that implements `Porpaginas\Result`: - - - ```php class MyController { @@ -54,29 +43,6 @@ class MyController } ``` - - - -```php -class MyController -{ - /** - * @Query - * @return Product[] - */ - public function products(): Porpaginas\Result - { - // Some code that returns a list of products - - // If you are using Doctrine, something like: - return new Porpaginas\Doctrine\ORM\ORMQueryResult($doctrineQuery); - } -} -``` - - - - Notice that: - the method return type MUST BE `Porpaginas\Result` or a class implementing `Porpaginas\Result` diff --git a/website/docs/prefetch-method.mdx b/website/docs/prefetch-method.mdx index 1c24e7099f..a12cb50891 100644 --- a/website/docs/prefetch-method.mdx +++ b/website/docs/prefetch-method.mdx @@ -4,9 +4,6 @@ title: Prefetching records sidebar_label: Prefetching records --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## The problem GraphQL naive implementations often suffer from the "N+1" problem. @@ -43,14 +40,6 @@ Instead, GraphQLite offers an easier to implement solution: the ability to fetch ## The "prefetch" method - - - ```php #[Type] class PostType { @@ -80,45 +69,7 @@ class PostType { } ``` - - - -```php -/** - * @Type - */ -class PostType { - /** - * @Field(prefetchMethod="prefetchUsers") - * @param mixed $prefetchedUsers - * @return User - */ - public function getUser($prefetchedUsers): User - { - // This method will receive the $prefetchedUsers as first argument. This is the return value of the "prefetchUsers" method below. - // Using this prefetched list, it should be easy to map it to the post - } - - /** - * @param Post[] $posts - * @return mixed - */ - public function prefetchUsers(iterable $posts) - { - // This function is called only once per GraphQL request - // with the list of posts. You can fetch the list of users - // associated with this posts in a single request, - // for instance using a "IN" query in SQL or a multi-fetch - // in your cache back-end. - } -} -``` - - - - - -When a "#[Prefetch]" attribute is detected on a parameter of "@Field" annotation, the method is called automatically. +When a `#[Prefetch]` attribute is detected on a parameter of `#[Field]` attribute, the method is called automatically. The prefetch callable must be one of the following: - a static method in the same class: `#[Prefetch('prefetchMethod')]` - a static method in a different class: `#[Prefetch([OtherClass::class, 'prefetchMethod')]` @@ -127,18 +78,10 @@ The first argument of the method is always an array of instances of the main typ ## Input arguments -Field arguments can be set either on the @Field annotated method OR/AND on the prefetch methods. +Field arguments can be set either on the `#[Field]` annotated method OR/AND on the prefetch methods. For instance: - - - ```php #[Type] class PostType { @@ -163,36 +106,3 @@ class PostType { } } ``` - - - - -```php -/** - * @Type - */ -class PostType { - /** - * @Field(prefetchMethod="prefetchComments") - * @param mixed $prefetchedComments - * @return Comment[] - */ - public function getComments($prefetchedComments): array - { - // ... - } - - /** - * @param Post[] $posts - * @return mixed - */ - public function prefetchComments(iterable $posts, bool $hideSpam, int $filterByScore) - { - // Parameters passed after the first parameter (hideSpam, filterByScore...) are automatically exposed - // as GraphQL arguments for the "comments" field. - } -} -``` - - - diff --git a/website/docs/queries.mdx b/website/docs/queries.mdx index 34abab54dd..138e4812eb 100644 --- a/website/docs/queries.mdx +++ b/website/docs/queries.mdx @@ -4,9 +4,6 @@ title: Queries sidebar_label: Queries --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - In GraphQLite, GraphQL queries are created by writing methods in *controller* classes. Those classes must be in the controllers namespaces which has been defined when you configured GraphQLite. @@ -14,15 +11,7 @@ For instance, in Symfony, the controllers namespace is `App\Controller` by defau ## Simple query -In a controller class, each query method must be annotated with the `@Query` annotation. For instance: - - - +In a controller class, each query method must be annotated with the `#[Query]` attribute. For instance: ```php namespace App\Controller; @@ -39,29 +28,6 @@ class MyController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Query; - -class MyController -{ - /** - * @Query - */ - public function hello(string $name): string - { - return 'Hello ' . $name; - } -} -``` - - - - This query is equivalent to the following [GraphQL type language](https://graphql.org/learn/schema/#type-language): ```graphql @@ -74,13 +40,11 @@ As you can see, GraphQLite will automatically do the mapping between PHP types a
Heads up! If you are not using a framework with an autowiring container (like Symfony or Laravel), please be aware that the MyController class must exist in the container of your application. Furthermore, the identifier of the controller in the container MUST be the fully qualified class name of controller.
-## About annotations / attributes +## About attributes -GraphQLite relies a lot on annotations (we call them attributes since PHP 8). +GraphQLite relies a lot on attributes. -It supports both the old "Doctrine annotations" style (`@Query`) and the new PHP 8 attributes (`#[Query]`). - -Read the [Doctrine annotations VS attributes](doctrine-annotations-attributes.mdx) documentation if you are not familiar with this concept. +It supports the new PHP 8 attributes (`#[Query]`), the "Doctrine annotations" style (`#[Query]`) was dropped. ## Testing the query @@ -104,14 +68,6 @@ So far, we simply declared a query. But we did not yet declare a type. Let's assume you want to return a product: - - - ```php namespace App\Controller; @@ -127,41 +83,9 @@ class ProductController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Query; - -class ProductController -{ - /** - * @Query - */ - public function product(string $id): Product - { - // Some code that looks for a product and returns it. - } -} -``` - - - - - As the `Product` class is not a scalar type, you must tell GraphQLite how to handle it: - - - ```php namespace App\Entities; @@ -187,46 +111,9 @@ class Product } ``` - - - -```php -namespace App\Entities; - -use TheCodingMachine\GraphQLite\Annotations\Field; -use TheCodingMachine\GraphQLite\Annotations\Type; - -/** - * @Type() - */ -class Product -{ - // ... - - /** - * @Field() - */ - public function getName(): string - { - return $this->name; - } - - /** - * @Field() - */ - public function getPrice(): ?float - { - return $this->price; - } -} -``` - - - - -The `@Type` annotation is used to inform GraphQLite that the `Product` class is a GraphQL type. +The `#[Type]` attribute is used to inform GraphQLite that the `Product` class is a GraphQL type. -The `@Field` annotation is used to define the GraphQL fields. This annotation must be put on a **public method**. +The `#[Field]` attribute is used to define the GraphQL fields. This attribute must be put on a **public method**. The `Product` class must be in one of the *types* namespaces. As for *controller* classes, you configured this namespace when you installed GraphQLite. By default, in Symfony, the allowed types namespaces are `App\Entity` and `App\Types`. @@ -243,9 +130,9 @@ Type Product {

If you are used to Domain driven design, you probably realize that the Product class is part of your domain.

-

GraphQL annotations are adding some serialization logic that is out of scope of the domain. - These are just annotations and for most project, this is the fastest and easiest route.

-

If you feel that GraphQL annotations do not belong to the domain, or if you cannot modify the class +

GraphQL attributes are adding some serialization logic that is out of scope of the domain. + These are just attributes and for most project, this is the fastest and easiest route.

+

If you feel that GraphQL attributes do not belong to the domain, or if you cannot modify the class directly (maybe because it is part of a third party library), there is another way to create types without annotating the domain class. We will explore that in the next chapter.

diff --git a/website/docs/query-plan.mdx b/website/docs/query-plan.mdx index 98838399d0..094b77c55e 100644 --- a/website/docs/query-plan.mdx +++ b/website/docs/query-plan.mdx @@ -4,9 +4,6 @@ title: Query plan sidebar_label: Query plan --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## The problem GraphQL naive implementations often suffer from the "N+1" problem. @@ -43,14 +40,6 @@ With GraphQLite, you can answer this question by tapping into the `ResolveInfo` Available in GraphQLite 4.0+ - - - ```php use GraphQL\Type\Definition\ResolveInfo; @@ -72,34 +61,6 @@ class ProductsController } ``` - - - -```php -use GraphQL\Type\Definition\ResolveInfo; - -class ProductsController -{ - /** - * @Query - * @return Product[] - */ - public function products(ResolveInfo $info): array - { - if (isset($info->getFieldSelection()['manufacturer']) { - // Let's perform a request with a JOIN on manufacturer - } else { - // Let's perform a request without a JOIN on manufacturer - } - // ... - } -} -``` - - - - - `ResolveInfo` is a class provided by Webonyx/GraphQL-PHP (the low-level GraphQL library used by GraphQLite). It contains info about the query and what fields are requested. Using `ResolveInfo::getFieldSelection` you can analyze the query and decide whether you should perform additional "JOINS" in your query or not. diff --git a/website/docs/symfony-bundle-advanced.mdx b/website/docs/symfony-bundle-advanced.mdx index d96b9fae68..c34c6d7628 100644 --- a/website/docs/symfony-bundle-advanced.mdx +++ b/website/docs/symfony-bundle-advanced.mdx @@ -4,9 +4,6 @@ title: "Symfony bundle: advanced usage" sidebar_label: Symfony specific features --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; -
Be advised! This documentation will be removed in a future release. For current and up-to-date Symfony bundle specific documentation, please see the Github repository.
@@ -114,15 +111,7 @@ This interface is automatically mapped to a type with 2 fields: - `userName: String!` - `roles: [String!]!` -If you want to get more fields, just add the `@Type` annotation to your user class: - - - +If you want to get more fields, just add the `#[Type]` attribute to your user class: ```php #[Type] @@ -137,29 +126,6 @@ class User implements UserInterface } ``` - - - -```php -/** - * @Type - */ -class User implements UserInterface -{ - /** - * @Field - */ - public function getEmail() : string - { - // ... - } - -} -``` - - - - You can now query this field using an [inline fragment](https://graphql.org/learn/queries/#inline-fragments): ```graphql @@ -192,14 +158,6 @@ Most of the time, getting the request object is irrelevant. Indeed, it is GraphQ manage it for you. Sometimes yet, fetching the request can be needed. In those cases, simply type-hint on the request in any parameter of your query/mutation/field. - - - ```php use Symfony\Component\HttpFoundation\Request; @@ -208,22 +166,4 @@ public function getUser(int $id, Request $request): User { // The $request object contains the Symfony Request. } -``` - - - - -```php -use Symfony\Component\HttpFoundation\Request; - -/** - * @Query - */ -public function getUser(int $id, Request $request): User -{ - // The $request object contains the Symfony Request. -} -``` - - - +``` \ No newline at end of file diff --git a/website/docs/type-mapping.mdx b/website/docs/type-mapping.mdx index 2051dd9559..26127719d6 100644 --- a/website/docs/type-mapping.mdx +++ b/website/docs/type-mapping.mdx @@ -4,9 +4,6 @@ title: Type mapping sidebar_label: Type mapping --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - As explained in the [queries](queries.mdx) section, the job of GraphQLite is to create GraphQL types from PHP types. ## Scalar mapping @@ -20,14 +17,6 @@ Scalar PHP types can be type-hinted to the corresponding GraphQL types: For instance: - - - ```php namespace App\Controller; @@ -43,40 +32,9 @@ class MyController } ``` - - - -```php -namespace App\Controller; - -use TheCodingMachine\GraphQLite\Annotations\Query; - -class MyController -{ - /** - * @Query - */ - public function hello(string $name): string - { - return 'Hello ' . $name; - } -} -``` - - - - ## Class mapping -When returning a PHP class in a query, you must annotate this class using `@Type` and `@Field` annotations: - - - +When returning a PHP class in a query, you must annotate this class using `#[Type]` and `#[Field]` attributes: ```php namespace App\Entities; @@ -103,90 +61,24 @@ class Product } ``` - - - -```php -namespace App\Entities; - -use TheCodingMachine\GraphQLite\Annotations\Field; -use TheCodingMachine\GraphQLite\Annotations\Type; - -/** - * @Type() - */ -class Product -{ - // ... - - /** - * @Field() - */ - public function getName(): string - { - return $this->name; - } - - /** - * @Field() - */ - public function getPrice(): ?float - { - return $this->price; - } -} -``` - - - - **Note:** The GraphQL output type name generated by GraphQLite is equal to the class name of the PHP class. So if your PHP class is `App\Entities\Product`, then the GraphQL type will be named "Product". In case you have several types with the same class name in different namespaces, you will face a naming collision. Hopefully, you can force the name of the GraphQL output type using the "name" attribute: - - - ```php #[Type(name: "MyProduct")] class Product { /* ... */ } ``` - - - -```php -/** - * @Type(name="MyProduct") - */ -class Product { /* ... */ } -``` - - - - -
You can also put a @Type annotation on a PHP interface + ## Array mapping You can type-hint against arrays (or iterators) as long as you add a detailed `@return` statement in the PHPDoc. - - - ```php /** * @return User[] <=== we specify that the array is an array of User objects. @@ -198,23 +90,6 @@ public function users(int $limit, int $offset): array } ``` - - - -```php -/** - * @Query - * @return User[] <=== we specify that the array is an array of User objects. - */ -public function users(int $limit, int $offset): array -{ - // Some code that returns an array of "users". -} -``` - - - - ## ID mapping GraphQL comes with a native `ID` type. PHP has no such type. @@ -223,14 +98,6 @@ There are two ways with GraphQLite to handle such type. ### Force the outputType - - - ```php #[Field(outputType: "ID")] public function getId(): string @@ -239,36 +106,12 @@ public function getId(): string } ``` - - - -```php -/** - * @Field(outputType="ID") - */ -public function getId(): string -{ - // ... -} -``` - - - - -Using the `outputType` attribute of the `@Field` annotation, you can force the output type to `ID`. +Using the `outputType` attribute of the `#[Field]` attribute, you can force the output type to `ID`. You can learn more about forcing output types in the [custom types section](custom-types.mdx). ### ID class - - - ```php use TheCodingMachine\GraphQLite\Types\ID; @@ -279,34 +122,8 @@ public function getId(): ID } ``` - - - -```php -use TheCodingMachine\GraphQLite\Types\ID; - -/** - * @Field - */ -public function getId(): ID -{ - // ... -} -``` - - - - Note that you can also use the `ID` class as an input type: - - - ```php use TheCodingMachine\GraphQLite\Types\ID; @@ -317,24 +134,6 @@ public function save(ID $id, string $name): Product } ``` - - - -```php -use TheCodingMachine\GraphQLite\Types\ID; - -/** - * @Mutation - */ -public function save(ID $id, string $name): Product -{ - // ... -} -``` - - - - ## Date mapping Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty to add one, with sensible defaults. @@ -342,14 +141,6 @@ Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty When used as an output type, `DateTimeImmutable` or `DateTimeInterface` PHP classes are automatically mapped to this `DateTime` GraphQL type. - - - ```php #[Field] public function getDate(): \DateTimeInterface @@ -358,22 +149,6 @@ public function getDate(): \DateTimeInterface } ``` - - - -```php -/** - * @Field - */ -public function getDate(): \DateTimeInterface -{ - return $this->date; -} -``` - - - - The `date` field will be of type `DateTime`. In the returned JSON response to a query, the date is formatted as a string in the **ISO8601** format (aka ATOM format). @@ -385,14 +160,6 @@ in the **ISO8601** format (aka ATOM format). Union types for return are supported in GraphQLite as of version 6.0: - - - ```php #[Query] public function companyOrContact(int $id): Company|Contact @@ -401,23 +168,6 @@ public function companyOrContact(int $id): Company|Contact } ``` - - - -```php -/** - * @Query - * @return Company|Contact - */ -public function companyOrContact(int $id) -{ - // Some code that returns a company or a contact. -} -``` - - - - ## Enum types PHP 8.1 introduced native support for Enums. GraphQLite now also supports native enums as of version 5.1. @@ -455,7 +205,7 @@ query users($status: Status!) {} ``` By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes -that live in different namespaces with the same class name), you can solve it using the `name` property on the `@Type` annotation: +that live in different namespaces with the same class name), you can solve it using the `name` property on the `#[Type]` attribute: ```php namespace Model\User; @@ -467,7 +217,6 @@ enum Status: string } ``` - ### Enum types with myclabs/php-enum
@@ -482,14 +231,6 @@ $ composer require myclabs/php-enum Now, any class extending the `MyCLabs\Enum\Enum` class will be mapped to a GraphQL enum: - - - ```php use MyCLabs\Enum\Enum; @@ -517,39 +258,6 @@ public function users(StatusEnum $status): array } ``` - - - -```php -use MyCLabs\Enum\Enum; - -class StatusEnum extends Enum -{ - private const ON = 'on'; - private const OFF = 'off'; - private const PENDING = 'pending'; -} -``` - -```php -/** - * @Query - * @return User[] - */ -public function users(StatusEnum $status): array -{ - if ($status == StatusEnum::ON()) { - // Note that the "magic" ON() method returns an instance of the StatusEnum class. - // Also, note that we are comparing this instance using "==" (using "===" would fail as we have 2 different instances here) - // ... - } - // ... -} -``` - - - - ```graphql query users($status: StatusEnum!) {} users(status: $status) { @@ -559,15 +267,7 @@ query users($status: StatusEnum!) {} ``` By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes -that live in different namespaces with the same class name), you can solve it using the `@EnumType` annotation: - - - +that live in different namespaces with the same class name), you can solve it using the `#[EnumType]` attribute: ```php use TheCodingMachine\GraphQLite\Annotations\EnumType; @@ -579,24 +279,6 @@ class StatusEnum extends Enum } ``` - - - -```php -use TheCodingMachine\GraphQLite\Annotations\EnumType; - -/** - * @EnumType(name="UserStatus") - */ -class StatusEnum extends Enum -{ - // ... -} -``` - - - -
GraphQLite must be able to find all the classes extending the "MyCLabs\Enum" class in your project. By default, GraphQLite will look for "Enum" classes in the namespaces declared for the types. For this reason, your enum classes MUST be in one of the namespaces declared for the types in your GraphQLite @@ -612,25 +294,21 @@ namespace App\Entities; use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type() - */ +#[Type] class Product { // ... - /** - * @Field() - */ + #[Field] public function getName(): string { return $this->name; } /** - * @Field() * @deprecated use field `name` instead */ + #[Field] public function getProductName(): string { return $this->name; diff --git a/website/docs/validation.mdx b/website/docs/validation.mdx index dfb2c925dd..3b8ac86ac2 100644 --- a/website/docs/validation.mdx +++ b/website/docs/validation.mdx @@ -4,9 +4,6 @@ title: Validation sidebar_label: User input validation --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - GraphQLite does not handle user input validation by itself. It is out of its scope. However, it can integrate with your favorite framework validation mechanism. The way you validate user input will @@ -31,18 +28,9 @@ GraphQLite provides a bridge to use the [Symfony validator](https://symfony.com/ ### Using the Symfony validator bridge -Usually, when you use the Symfony validator component, you put annotations in your entities and you validate those entities +Usually, when you use the Symfony validator component, you put attributes in your entities and you validate those entities using the `Validator` object. - - - ```php title="UserController.php" use Symfony\Component\Validator\Validator\ValidatorInterface; use TheCodingMachine\GraphQLite\Validator\ValidationFailedException @@ -73,55 +61,8 @@ class UserController } ``` - - - -```php title="UserController.php" -use Symfony\Component\Validator\Validator\ValidatorInterface; -use TheCodingMachine\GraphQLite\Validator\ValidationFailedException - -class UserController -{ - private $validator; - - public function __construct(ValidatorInterface $validator) - { - $this->validator = $validator; - } - - /** - * @Mutation - */ - public function createUser(string $email, string $password): User - { - $user = new User($email, $password); - - // Let's validate the user - $errors = $this->validator->validate($user); - - // Throw an appropriate GraphQL exception if validation errors are encountered - ValidationFailedException::throwException($errors); - - // No errors? Let's continue and save the user - // ... - } -} -``` - - - - Validation rules are added directly to the object in the domain model: - - - ```php title="User.php" use Symfony\Component\Validator\Constraints as Assert; @@ -146,41 +87,6 @@ class User } ``` - - - -```php title="User.php" -use Symfony\Component\Validator\Constraints as Assert; - -class User -{ - /** - * @Assert\Email( - * message = "The email '{{ value }}' is not a valid email.", - * checkMX = true - * ) - */ - private $email; - - /** - * The NotCompromisedPassword assertion asks the "HaveIBeenPawned" service if your password has already leaked or not. - * @Assert\NotCompromisedPassword - */ - private $password; - - public function __construct(string $email, string $password) - { - $this->email = $email; - $this->password = $password; - } - - // ... -} -``` - - - - If a validation fails, GraphQLite will return the failed validations in the "errors" section of the JSON response: ```json @@ -212,35 +118,32 @@ on your domain objects. Only use this technique if you want to validate user inp in a domain object.
-Use the `@Assertion` annotation to validate directly the user input. +Use the `#[Assertion]` attribute to validate directly the user input. ```php use Symfony\Component\Validator\Constraints as Assert; use TheCodingMachine\GraphQLite\Validator\Annotations\Assertion; +use TheCodingMachine\GraphQLite\Annotations\Query; -/** - * @Query - * @Assertion(for="email", constraint=@Assert\Email()) - */ +#[Query] +#[Assertion(for: "email", constraint: new Assert\Email())] public function findByMail(string $email): User { // ... } ``` -Notice that the "constraint" parameter contains an annotation (it is an annotation wrapped in an annotation). +Notice that the "constraint" parameter contains an attribute (it is an attribute wrapped in an attribute). You can also pass an array to the `constraint` parameter: ```php -@Assertion(for="email", constraint={@Assert\NotBlank(), @Assert\Email()}) +#[Assertion(for: "email", constraint: [new Assert\NotBlack(), new Assert\Email()])] ``` -
Heads up! The "@Assertion" annotation is only available as a Doctrine annotations. You cannot use it as a PHP 8 attributes
- ## Custom InputType Validation -GraphQLite also supports a fully custom validation implementation for all input types defined with an `@Input` annotation or PHP8 `#[Input]` attribute. This offers a way to validate input types before they're available as a method parameter of your query and mutation controllers. This way, when you're using your query or mutation controllers, you can feel confident that your input type objects have already been validated. +GraphQLite also supports a fully custom validation implementation for all input types defined with an `#[Input]` attribute. This offers a way to validate input types before they're available as a method parameter of your query and mutation controllers. This way, when you're using your query or mutation controllers, you can feel confident that your input type objects have already been validated.

It's important to note that this validation implementation does not validate input types created with a factory. If you are creating an input type with a factory, or using primitive parameters in your query/mutation controllers, you should be sure to validate these independently. This is strictly for input type objects.

@@ -248,7 +151,7 @@ GraphQLite also supports a fully custom validation implementation for all input

You can use one of the framework validation libraries listed above or implement your own validation for these cases. If you're using input type objects for most all of your query and mutation controllers, then there is little additional validation concerns with regards to user input. There are many reasons why you should consider defaulting to an InputType object, as opposed to individual arguments, for your queries and mutations. This is just one additional perk.

-To get started with validation on input types defined by an `@Input` annotation, you'll first need to register your validator with the `SchemaFactory`. +To get started with validation on input types defined by an `#[Input]` attribute, you'll first need to register your validator with the `SchemaFactory`. ```php $factory = new SchemaFactory($cache, $this->container); @@ -278,7 +181,7 @@ interface InputTypeValidatorInterface } ``` -The interface is quite simple. Handle all of your own validation logic in the `validate` method. For example, you might use Symfony's annotation based validation in addition to some other custom validation logic. It's really up to you on how you wish to handle your own validation. The `validate` method will receive the input type object populated with the user input. +The interface is quite simple. Handle all of your own validation logic in the `validate` method. For example, you might use Symfony's attribute based validation in addition to some other custom validation logic. It's really up to you on how you wish to handle your own validation. The `validate` method will receive the input type object populated with the user input. You'll notice that the `validate` method has a `void` return. The purpose here is to encourage you to throw an Exception or handle validation output however you best see fit. GraphQLite does it's best to stay out of your way and doesn't make attempts to handle validation output. You can, however, throw an instance of `TheCodingMachine\GraphQLite\Exceptions\GraphQLException` or `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException` as usual (see [Error Handling](error-handling) for more details).