Fixed bug that GraphQL cannot work. (#6894)

This commit is contained in:
李铭昕 2024-06-18 18:34:53 +08:00 committed by GitHub
parent be50ab279f
commit 52898c9004
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 92 additions and 3052 deletions

View File

@ -317,6 +317,7 @@ jobs:
- name: Run Test Cases
run: |
./.travis/requirement.install.sh
composer remove thecodingmachine/graphqlite --dev -W
composer require "psr/simple-cache:${{ matrix.psr-version }}"
composer update -oW
composer analyse src/cache

View File

@ -91,6 +91,7 @@
"symfony/serializer": "^5.0 || ^6.0",
"symfony/uid": "^5.0 || ^6.0",
"symfony/var-dumper": "^5.0 || ^6.0",
"thecodingmachine/graphqlite": "^7.0",
"twig/twig": "^3.0",
"vlucas/phpdotenv": "^5.0",
"zircote/swagger-php": "^4.6"

View File

@ -24,7 +24,6 @@ parameters:
- %currentWorkingDirectory%/src/database-pgsql/*
- %currentWorkingDirectory%/src/db/src/PgSQL/*
- %currentWorkingDirectory%/src/filesystem/*
- %currentWorkingDirectory%/src/graphql/*
- %currentWorkingDirectory%/src/grpc/*
- %currentWorkingDirectory%/src/grpc-server/*
- %currentWorkingDirectory%/src/ide-helper/*
@ -84,4 +83,4 @@ parameters:
- message: '#Method .* should return .* but returns#'
path: src/collection/src/*.php
- message: '#has no type specified#'
path: src/nacos/src/Protobuf/*.php
path: src/nacos/src/Protobuf/*.php

View File

@ -15,7 +15,7 @@
"hyperf/contract": "~3.1.0",
"hyperf/di": "~3.1.0",
"swow/psr7-plus": "^1.0",
"thecodingmachine/graphqlite": "^3.0"
"thecodingmachine/graphqlite": "^7.0"
},
"suggest": {
"hyperf/http-message": "Required to use GraphQLMiddleware.",

View File

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\Di\ReflectionManager;
use Hyperf\GraphQL\ClassCollector;
use ReflectionProperty;
trait AnnotationTrait
{
public function toArray(): array
{
$properties = ReflectionManager::reflectClass(static::class)->getProperties(ReflectionProperty::IS_PUBLIC);
$result = [];
foreach ($properties as $property) {
$result[$property->getName()] = $property->getValue($this);
}
return $result;
}
public function collectClass(string $className): void
{
AnnotationCollector::collectClass($className, static::class, $this);
ClassCollector::collect($className);
}
public function collectMethod(string $className, ?string $target): void
{
AnnotationCollector::collectMethod($className, $target, static::class, $this);
ClassCollector::collect($className);
}
public function collectProperty(string $className, ?string $target): void
{
AnnotationCollector::collectProperty($className, $target, static::class, $this);
ClassCollector::collect($className);
}
protected function bindMainProperty(string $key, array $value)
{
if (isset($value['value'])) {
$this->{$key} = $value['value'];
}
}
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"CLASS"})
* @Attributes({
* @Attribute("class", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_CLASS)]
class ExtendType extends \TheCodingMachine\GraphQLite\Annotations\ExtendType implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("name", type="string")
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Factory extends \TheCodingMachine\GraphQLite\Annotations\Factory implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
*/
#[Attribute(Attribute::TARGET_METHOD)]
class FailWith extends \TheCodingMachine\GraphQLite\Annotations\FailWith implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("outputType", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Field extends \TheCodingMachine\GraphQLite\Annotations\Field implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Logged extends \TheCodingMachine\GraphQLite\Annotations\Logged implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("outputType", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Mutation extends \TheCodingMachine\GraphQLite\Annotations\Mutation implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("outputType", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Query extends \TheCodingMachine\GraphQLite\Annotations\Query implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"ANNOTATION", "METHOD"})
* @Attributes({
* @Attribute("name", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Right extends \TheCodingMachine\GraphQLite\Annotations\Right implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"CLASS"})
* @Attributes({
* @Attribute("name", type="string"),
* @Attribute("logged", type="bool"),
* @Attribute("right", type="TheCodingMachine\GraphQLite\Annotations\Right"),
* @Attribute("outputType", type="string"),
* @Attribute("isId", type="bool"),
* @Attribute("failWith", type="mixed"),
* })
*/
#[Attribute(Attribute::TARGET_CLASS)]
class SourceField extends \TheCodingMachine\GraphQLite\Annotations\SourceField implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL\Annotation;
use Attribute;
use Hyperf\Di\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target({"CLASS"})
* @Attributes({
* @Attribute("class", type="string"),
* })
*/
#[Attribute(Attribute::TARGET_CLASS)]
class Type extends \TheCodingMachine\GraphQLite\Annotations\Type implements AnnotationInterface
{
use AnnotationTrait;
}

View File

@ -1,247 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Doctrine\Common\Annotations\AnnotationException;
use Doctrine\Common\Annotations\Reader;
use Hyperf\GraphQL\Annotation\ExtendType;
use Hyperf\GraphQL\Annotation\Factory;
use Hyperf\GraphQL\Annotation\FailWith;
use Hyperf\GraphQL\Annotation\Logged;
use Hyperf\GraphQL\Annotation\Right;
use Hyperf\GraphQL\Annotation\SourceField;
use Hyperf\GraphQL\Annotation\Type;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
use function array_filter;
use function in_array;
use function strpos;
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 Reader
*/
private $reader;
/**
* Classes in those namespaces MUST have valid annotations (otherwise, an error is thrown).
*
* @var string[]
*/
private $strictNamespaces;
/**
* If true, no exceptions will be thrown for incorrect annotations in code coming from the "vendor/" directory.
*
* @var string
*/
private $mode;
/**
* @var array
*/
private $methodAnnotationCache = [];
/**
* AnnotationReader constructor.
* @param string $mode One of self::LAX_MODE or self::STRICT_MODE
*/
public function __construct(Reader $reader, string $mode = self::STRICT_MODE, array $strictNamespaces = [])
{
$this->reader = $reader;
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');
}
$this->mode = $mode;
$this->strictNamespaces = $strictNamespaces;
}
public function getTypeAnnotation(ReflectionClass $refClass): ?Type
{
try {
/** @var null|Type $type */
$type = $this->getClassAnnotation($refClass, Type::class);
if ($type !== null && $type->isSelfType()) {
$type->setClass($refClass->getName());
}
} catch (ClassNotFoundException $e) {
throw ClassNotFoundException::wrapException($e, $refClass->getName());
}
return $type;
}
public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
{
try {
/** @var null|ExtendType $extendType */
$extendType = $this->getClassAnnotation($refClass, ExtendType::class);
} catch (ClassNotFoundException $e) {
throw ClassNotFoundException::wrapExceptionForExtendTag($e, $refClass->getName());
}
return $extendType;
}
public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest
{
return $this->getMethodAnnotation($refMethod, $annotationName);
}
public function getLoggedAnnotation(ReflectionMethod $refMethod): ?Logged
{
return $this->getMethodAnnotation($refMethod, Logged::class);
}
public function getRightAnnotation(ReflectionMethod $refMethod): ?Right
{
return $this->getMethodAnnotation($refMethod, Right::class);
}
public function getFailWithAnnotation(ReflectionMethod $refMethod): ?FailWith
{
return $this->getMethodAnnotation($refMethod, FailWith::class);
}
/**
* @return SourceField[]
*/
public function getSourceFields(ReflectionClass $refClass): array
{
return $this->getClassAnnotations($refClass, SourceField::class);
}
public function getFactoryAnnotation(ReflectionMethod $refMethod): ?Factory
{
return $this->getMethodAnnotation($refMethod, Factory::class);
}
/**
* Returns the class annotations. Finds in the parents too.
*
* @return object[]
*/
public function getClassAnnotations(ReflectionClass $refClass, string $annotationClass): array
{
$toAddAnnotations = [];
do {
try {
$allAnnotations = $this->reader->getClassAnnotations($refClass);
$toAddAnnotations[] = array_filter($allAnnotations, function ($annotation) use ($annotationClass): bool {
return $annotation instanceof $annotationClass;
});
} catch (AnnotationException $e) {
if ($this->mode === self::STRICT_MODE) {
throw $e;
}
if ($this->mode === self::LAX_MODE) {
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment(), $refClass->getName())) {
throw $e;
}
}
}
$refClass = $refClass->getParentClass();
} while ($refClass);
if (! empty($toAddAnnotations)) {
return array_merge(...$toAddAnnotations);
}
return [];
}
/**
* Returns a class annotation. Finds in the parents if not found in the main class.
*
* @return null|object
*/
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass)
{
do {
$type = null;
try {
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;
case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refClass->getDocComment(), $refClass->getName())) {
throw $e;
}
return null;
default:
throw new RuntimeException("Unexpected mode '{$this->mode}'."); // @codeCoverageIgnore
}
}
if ($type !== null) {
return $type;
}
$refClass = $refClass->getParentClass();
} while ($refClass);
return null;
}
/**
* Returns a method annotation and handles correctly errors.
*
* @return null|object
*/
private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass)
{
$cacheKey = $refMethod->getDeclaringClass()->getName() . '::' . $refMethod->getName() . '_' . $annotationClass;
if (isset($this->methodAnnotationCache[$cacheKey])) {
return $this->methodAnnotationCache[$cacheKey];
}
try {
return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass);
} catch (AnnotationException $e) {
switch ($this->mode) {
case self::STRICT_MODE:
throw $e;
case self::LAX_MODE:
if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment(), $refMethod->getDeclaringClass()->getName())) {
throw $e;
}
return null;
default:
throw new RuntimeException("Unexpected mode '{$this->mode}'."); // @codeCoverageIgnore
}
}
}
/**
* Returns true if the annotation class name is part of the docblock comment.
*/
private function isErrorImportant(string $annotationClass, string $docComment, string $className): bool
{
foreach ($this->strictNamespaces as $strictNamespace) {
if (strpos($className, $strictNamespace) === 0) {
return true;
}
}
$shortAnnotationClass = substr($annotationClass, strrpos($annotationClass, '\\') + 1);
return strpos($docComment, '@' . $shortAnnotationClass) !== false;
}
}

View File

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Hyperf\Di\MetadataCollector;
class ClassCollector extends MetadataCollector
{
protected static array $container = [];
public static function collect(string $class)
{
if (! in_array($class, self::$container)) {
self::$container[] = $class;
}
}
public static function getClasses()
{
return self::$container;
}
}

View File

@ -12,18 +12,7 @@ declare(strict_types=1);
namespace Hyperf\GraphQL;
use Doctrine\Common\Annotations\Reader;
use GraphQL\Type\Schema;
use TheCodingMachine\GraphQLite\Hydrators\FactoryHydrator;
use TheCodingMachine\GraphQLite\Hydrators\HydratorInterface;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\NamingStrategy;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\QueryProviderInterface;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
use TheCodingMachine\GraphQLite\Security\FailAuthenticationService;
use TheCodingMachine\GraphQLite\Security\FailAuthorizationService;
class ConfigProvider
{
@ -31,23 +20,13 @@ class ConfigProvider
{
return [
'dependencies' => [
Schema::class => \TheCodingMachine\GraphQLite\Schema::class,
QueryProviderInterface::class => QueryProvider::class,
RecursiveTypeMapperInterface::class => RecursiveTypeMapperFactory::class,
Reader::class => ReaderFactory::class,
HydratorInterface::class => FactoryHydrator::class,
AuthenticationServiceInterface::class => FailAuthenticationService::class,
AuthorizationServiceInterface::class => FailAuthorizationService::class,
NamingStrategyInterface::class => NamingStrategy::class,
Schema::class => SchemaFactory::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
'collectors' => [
ClassCollector::class,
],
],
],
];

View File

@ -1,799 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type as GraphQLType;
use Hyperf\GraphQL\Annotation\Field;
use Hyperf\GraphQL\Annotation\Logged;
use Hyperf\GraphQL\Annotation\Mutation;
use Hyperf\GraphQL\Annotation\Query;
use Iterator;
use IteratorAggregate;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\Array_;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\Compound;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Iterable_;
use phpDocumentor\Reflection\Types\Mixed_;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use phpDocumentor\Reflection\Types\String_;
use Psr\Http\Message\UploadedFileInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use TheCodingMachine\GraphQLite\Annotations\SourceField;
use TheCodingMachine\GraphQLite\FieldNotFoundException;
use TheCodingMachine\GraphQLite\FromSourceFieldsInterface;
use TheCodingMachine\GraphQLite\GraphQLException;
use TheCodingMachine\GraphQLite\InvalidDocBlockException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\MissingAnnotationException;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\QueryField;
use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
use TheCodingMachine\GraphQLite\TypeMappingException;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\CustomTypesRegistry;
use TheCodingMachine\GraphQLite\Types\DateTimeType;
use TheCodingMachine\GraphQLite\Types\ID;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Types\UnionType;
use function array_merge;
use function get_parent_class;
use function iterator_to_array;
use function ucfirst;
/**
* A class in charge if returning list of fields for queries / mutations / entities / input types.
*/
class FieldsBuilder
{
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var RecursiveTypeMapperInterface
*/
private $typeMapper;
/**
* @var ArgumentResolver
*/
private $argumentResolver;
/**
* @var AuthenticationServiceInterface
*/
private $authenticationService;
/**
* @var AuthorizationServiceInterface
*/
private $authorizationService;
/**
* @var CachedDocBlockFactory
*/
private $cachedDocBlockFactory;
/**
* @var TypeResolver
*/
private $typeResolver;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
public function __construct(
AnnotationReader $annotationReader,
RecursiveTypeMapperInterface $typeMapper,
ArgumentResolver $argumentResolver,
AuthenticationServiceInterface $authenticationService,
AuthorizationServiceInterface $authorizationService,
TypeResolver $typeResolver,
CachedDocBlockFactory $cachedDocBlockFactory,
NamingStrategyInterface $namingStrategy
) {
$this->annotationReader = $annotationReader;
$this->typeMapper = $typeMapper;
$this->argumentResolver = $argumentResolver;
$this->authenticationService = $authenticationService;
$this->authorizationService = $authorizationService;
$this->typeResolver = $typeResolver;
$this->cachedDocBlockFactory = $cachedDocBlockFactory;
$this->namingStrategy = $namingStrategy;
}
// TODO: Add RecursiveTypeMapper in the list of parameters for getQueries and REMOVE the ControllerQueryProviderFactory.
/**
* @param object $controller
* @return QueryField[]
* @throws ReflectionException
*/
public function getQueries($controller): array
{
return $this->getFieldsByAnnotations($controller, Query::class, false);
}
/**
* @param object $controller
* @return QueryField[]
* @throws ReflectionException
*/
public function getMutations($controller): array
{
return $this->getFieldsByAnnotations($controller, Mutation::class, false);
}
/**
* @param mixed $controller
* @return array<string, QueryField> queryField indexed by name
*/
public function getFields($controller): array
{
$fieldAnnotations = $this->getFieldsByAnnotations($controller, Field::class, true);
$refClass = new ReflectionClass($controller);
/** @var SourceField[] $sourceFields */
$sourceFields = $this->annotationReader->getSourceFields($refClass);
if ($controller instanceof FromSourceFieldsInterface) {
$sourceFields = array_merge($sourceFields, $controller->getSourceFields());
}
$fieldsFromSourceFields = $this->getQueryFieldsFromSourceFields($sourceFields, $refClass);
$fields = [];
foreach ($fieldAnnotations as $field) {
$fields[$field->name] = $field;
}
foreach ($fieldsFromSourceFields as $field) {
$fields[$field->name] = $field;
}
return $fields;
}
/**
* Track Field annotation in a self targeted type.
*
* @return array<string, QueryField> queryField indexed by name
*/
public function getSelfFields(string $className): array
{
$fieldAnnotations = $this->getFieldsByAnnotations(null, Field::class, false, $className);
$refClass = new ReflectionClass($className);
/** @var SourceField[] $sourceFields */
$sourceFields = $this->annotationReader->getSourceFields($refClass);
$fieldsFromSourceFields = $this->getQueryFieldsFromSourceFields($sourceFields, $refClass);
$fields = [];
foreach ($fieldAnnotations as $field) {
$fields[$field->name] = $field;
}
foreach ($fieldsFromSourceFields as $field) {
$fields[$field->name] = $field;
}
return $fields;
}
/**
* @param ReflectionMethod $refMethod a method annotated with a Factory annotation
* @return array<string, array<int, mixed>> returns an array of fields as accepted by the InputObjectType constructor
*/
public function getInputFields(ReflectionMethod $refMethod): array
{
$docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
// $docBlockComment = $docBlockObj->getSummary()."\n".$docBlockObj->getDescription()->render();
$parameters = $refMethod->getParameters();
return $this->mapParameters($parameters, $docBlockObj);
}
/**
* @param object $controller
* @param bool $injectSource whether to inject the source object or not as the first argument
* @return QueryField[]
* @throws CannotMapTypeExceptionInterface
* @throws ReflectionException
*/
private function getFieldsByAnnotations($controller, string $annotationName, bool $injectSource, ?string $sourceClassName = null): array
{
if ($sourceClassName !== null) {
$refClass = new ReflectionClass($sourceClassName);
} else {
$refClass = new ReflectionClass($controller);
}
$queryList = [];
$oldDeclaringClass = null;
$context = null;
$closestMatchingTypeClass = null;
if ($annotationName === Field::class) {
$parent = get_parent_class($refClass->getName());
if ($parent !== null) {
$closestMatchingTypeClass = $this->typeMapper->findClosestMatchingParent((string) $parent);
}
}
foreach ($refClass->getMethods() as $refMethod) {
if ($closestMatchingTypeClass !== null && $closestMatchingTypeClass === $refMethod->getDeclaringClass()->getName()) {
// Optimisation: no need to fetch annotations from parent classes that are ALREADY GraphQL types.
// We will merge the fields anyway.
break;
}
// First, let's check the "Query" or "Mutation" or "Field" annotation
$queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);
if ($queryAnnotation !== null) {
$unauthorized = false;
if (! $this->isAuthorized($refMethod)) {
$failWith = $this->annotationReader->getFailWithAnnotation($refMethod);
if ($failWith === null) {
continue;
}
$unauthorized = true;
}
$docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
$docBlockComment = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render();
$methodName = $refMethod->getName();
$name = $queryAnnotation->getName() ?: $this->namingStrategy->getFieldNameFromMethodName($methodName);
$parameters = $refMethod->getParameters();
if ($injectSource === true) {
$first_parameter = array_shift($parameters);
// TODO: check that $first_parameter type is correct.
}
$args = $this->mapParameters($parameters, $docBlockObj);
if ($queryAnnotation->getOutputType()) {
try {
$type = $this->typeResolver->mapNameToOutputType($queryAnnotation->getOutputType());
} catch (CannotMapTypeExceptionInterface $e) {
throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod);
}
} else {
$type = $this->mapReturnType($refMethod, $docBlockObj);
}
if ($unauthorized) {
$failWithValue = $failWith->getValue();
$callable = function () use ($failWithValue) {
return $failWithValue;
};
if ($failWithValue === null && $type instanceof NonNull) {
$type = $type->getWrappedType();
}
$queryList[] = new QueryField($name, $type, $args, $callable, null, $this->argumentResolver, $docBlockComment, $injectSource);
} else {
$callable = [$controller, $methodName];
if ($sourceClassName !== null) {
$queryList[] = new QueryField($name, $type, $args, null, $callable[1], $this->argumentResolver, $docBlockComment, $injectSource);
} else {
$queryList[] = new QueryField($name, $type, $args, $callable, null, $this->argumentResolver, $docBlockComment, $injectSource);
}
}
}
}
return $queryList;
}
/**
* @return GraphQLType|OutputType
*/
private function mapReturnType(ReflectionMethod $refMethod, DocBlock $docBlockObj): GraphQLType
{
$returnType = $refMethod->getReturnType();
if ($returnType !== null) {
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
$phpdocType = $typeResolver->resolve((string) $returnType);
$phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass());
} else {
$phpdocType = new Mixed_();
}
$docBlockReturnType = $this->getDocBlocReturnType($docBlockObj, $refMethod);
try {
/** @var GraphQLType|OutputType $type */
$type = $this->mapType($phpdocType, $docBlockReturnType, $returnType ? $returnType->allowsNull() : false, false);
} catch (TypeMappingException $e) {
throw TypeMappingException::wrapWithReturnInfo($e, $refMethod);
} catch (CannotMapTypeExceptionInterface $e) {
throw CannotMapTypeException::wrapWithReturnInfo($e, $refMethod);
}
return $type;
}
private function getDocBlocReturnType(DocBlock $docBlock, ReflectionMethod $refMethod): ?Type
{
/** @var Return_[] $returnTypeTags */
$returnTypeTags = $docBlock->getTagsByName('return');
if (count($returnTypeTags) > 1) {
throw InvalidDocBlockException::tooManyReturnTags($refMethod);
}
$docBlockReturnType = null;
if (isset($returnTypeTags[0])) {
$docBlockReturnType = $returnTypeTags[0]->getType();
}
return $docBlockReturnType;
}
/**
* @param array<int, SourceFieldInterface> $sourceFields
* @return QueryField[]
* @throws CannotMapTypeException
* @throws CannotMapTypeExceptionInterface
* @throws ReflectionException
*/
private function getQueryFieldsFromSourceFields(array $sourceFields, ReflectionClass $refClass): array
{
if (empty($sourceFields)) {
return [];
}
$typeField = $this->annotationReader->getTypeAnnotation($refClass);
$extendTypeField = $this->annotationReader->getExtendTypeAnnotation($refClass);
if ($typeField !== null) {
$objectClass = $typeField->getClass();
} elseif ($extendTypeField !== null) {
$objectClass = $extendTypeField->getClass();
} else {
throw MissingAnnotationException::missingTypeExceptionToUseSourceField();
}
$objectRefClass = new ReflectionClass($objectClass);
$oldDeclaringClass = null;
$context = null;
$queryList = [];
foreach ($sourceFields as $sourceField) {
// Ignore the field if we must be logged.
$right = $sourceField->getRight();
$unauthorized = false;
if (($sourceField->isLogged() && ! $this->authenticationService->isLogged())
|| ($right !== null && ! $this->authorizationService->isAllowed($right->getName()))) {
if (! $sourceField->canFailWith()) {
continue;
}
$unauthorized = true;
}
try {
$refMethod = $this->getMethodFromPropertyName($objectRefClass, $sourceField->getName());
} catch (FieldNotFoundException $e) {
throw FieldNotFoundException::wrapWithCallerInfo($e, $refClass->getName());
}
$methodName = $refMethod->getName();
$docBlockObj = $this->cachedDocBlockFactory->getDocBlock($refMethod);
$docBlockComment = $docBlockObj->getSummary() . "\n" . $docBlockObj->getDescription()->render();
$args = $this->mapParameters($refMethod->getParameters(), $docBlockObj);
if ($sourceField->isId()) {
$type = GraphQLType::id();
if (! $refMethod->getReturnType()->allowsNull()) {
$type = GraphQLType::nonNull($type);
}
} elseif ($sourceField->getOutputType()) {
try {
$type = $this->typeResolver->mapNameToOutputType($sourceField->getOutputType());
} catch (CannotMapTypeExceptionInterface $e) {
throw CannotMapTypeException::wrapWithSourceField($e, $refClass, $sourceField);
}
} else {
$type = $this->mapReturnType($refMethod, $docBlockObj);
}
if (! $unauthorized) {
$queryList[] = new QueryField($sourceField->getName(), $type, $args, null, $methodName, $this->argumentResolver, $docBlockComment, false);
} else {
$failWithValue = $sourceField->getFailWith();
$callable = function () use ($failWithValue) {
return $failWithValue;
};
if ($failWithValue === null && $type instanceof NonNull) {
$type = $type->getWrappedType();
}
$queryList[] = new QueryField($sourceField->getName(), $type, $args, $callable, null, $this->argumentResolver, $docBlockComment, false);
}
}
return $queryList;
}
private function getMethodFromPropertyName(ReflectionClass $reflectionClass, string $propertyName): ReflectionMethod
{
if ($reflectionClass->hasMethod($propertyName)) {
$methodName = $propertyName;
} else {
$upperCasePropertyName = ucfirst($propertyName);
if ($reflectionClass->hasMethod('get' . $upperCasePropertyName)) {
$methodName = 'get' . $upperCasePropertyName;
} elseif ($reflectionClass->hasMethod('is' . $upperCasePropertyName)) {
$methodName = 'is' . $upperCasePropertyName;
} else {
throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName);
}
}
return $reflectionClass->getMethod($methodName);
}
/**
* Checks the Logged and Right annotations.
*/
private function isAuthorized(ReflectionMethod $reflectionMethod): bool
{
$loggedAnnotation = $this->annotationReader->getLoggedAnnotation($reflectionMethod);
if ($loggedAnnotation !== null && ! $this->authenticationService->isLogged()) {
return false;
}
$rightAnnotation = $this->annotationReader->getRightAnnotation($reflectionMethod);
if ($rightAnnotation !== null && ! $this->authorizationService->isAllowed($rightAnnotation->getName())) {
return false;
}
return true;
}
/**
* Note: there is a bug in $refMethod->allowsNull that forces us to use $standardRefMethod->allowsNull instead.
*
* @param ReflectionParameter[] $refParameters
* @return array[] An array of ['type'=>Type, 'defaultValue'=>val]
* @throws MissingTypeHintException
*/
private function mapParameters(array $refParameters, DocBlock $docBlock): array
{
$args = [];
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
foreach ($refParameters as $parameter) {
$parameterType = $parameter->getType();
$allowsNull = $parameterType === null ? true : $parameterType->allowsNull();
$type = (string) $parameterType;
if ($type === '') {
throw MissingTypeHintException::missingTypeHint($parameter);
}
$phpdocType = $typeResolver->resolve($type);
$phpdocType = $this->resolveSelf($phpdocType, $parameter->getDeclaringClass());
/** @var DocBlock\Tags\Param[] $paramTags */
$paramTags = $docBlock->getTagsByName('param');
$docBlockType = null;
foreach ($paramTags as $paramTag) {
if ($paramTag->getVariableName() === $parameter->getName()) {
$docBlockType = $paramTag->getType();
break;
}
}
try {
$arr = [
'type' => $this->mapType($phpdocType, $docBlockType, $allowsNull || $parameter->isDefaultValueAvailable(), true),
];
} catch (TypeMappingException $e) {
throw TypeMappingException::wrapWithParamInfo($e, $parameter);
} catch (CannotMapTypeExceptionInterface $e) {
throw CannotMapTypeException::wrapWithParamInfo($e, $parameter);
}
if ($parameter->allowsNull()) {
$arr['defaultValue'] = null;
}
if ($parameter->isDefaultValueAvailable()) {
$arr['defaultValue'] = $parameter->getDefaultValue();
}
$args[$parameter->getName()] = $arr;
}
return $args;
}
private function mapType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType
{
$graphQlType = null;
if ($type instanceof Array_ || $type instanceof Iterable_ || $type instanceof Mixed_) {
$graphQlType = $this->mapDocBlockType($type, $docBlockType, $isNullable, $mapToInputType);
} else {
try {
$graphQlType = $this->toGraphQlType($type, null, $mapToInputType);
if (! $isNullable) {
$graphQlType = GraphQLType::nonNull($graphQlType);
}
} catch (CannotMapTypeExceptionInterface|TypeMappingException $e) {
// Is the type iterable? If yes, let's analyze the docblock
// TODO: it would be better not to go through an exception for this.
if ($type instanceof Object_) {
$fqcn = (string) $type->getFqsen();
$refClass = new ReflectionClass($fqcn);
// Note : $refClass->isIterable() is only accessible in PHP 7.2
if ($refClass->implementsInterface(Iterator::class) || $refClass->implementsInterface(IteratorAggregate::class)) {
$graphQlType = $this->mapIteratorDocBlockType($type, $docBlockType, $isNullable);
} else {
throw $e;
}
} else {
throw $e;
}
}
}
return $graphQlType;
}
private function mapDocBlockType(Type $type, ?Type $docBlockType, bool $isNullable, bool $mapToInputType): GraphQLType
{
if ($docBlockType === null) {
throw TypeMappingException::createFromType($type);
}
if (! $isNullable) {
// Let's check a "null" value in the docblock
$isNullable = $this->isNullable($docBlockType);
}
$filteredDocBlockTypes = $this->typesWithoutNullable($docBlockType);
if (empty($filteredDocBlockTypes)) {
throw TypeMappingException::createFromType($type);
}
$unionTypes = [];
$lastException = null;
foreach ($filteredDocBlockTypes as $singleDocBlockType) {
try {
$unionTypes[] = $this->toGraphQlType($this->dropNullableType($singleDocBlockType), null, $mapToInputType);
} catch (CannotMapTypeExceptionInterface|TypeMappingException $e) {
// We have several types. It is ok not to be able to match one.
$lastException = $e;
}
}
if (empty($unionTypes) && $lastException !== null) {
throw $lastException;
}
if (count($unionTypes) === 1) {
$graphQlType = $unionTypes[0];
} else {
$graphQlType = new UnionType($unionTypes, $this->typeMapper);
}
/* elseif (count($filteredDocBlockTypes) === 1) {
$graphQlType = $this->toGraphQlType($filteredDocBlockTypes[0], $mapToInputType);
} else {
throw new GraphQLException('Union types are not supported (yet)');
//$graphQlTypes = array_map([$this, 'toGraphQlType'], $filteredDocBlockTypes);
//$$graphQlType = new UnionType($graphQlTypes);
}*/
if (! $isNullable) {
$graphQlType = GraphQLType::nonNull($graphQlType);
}
return $graphQlType;
}
/**
* Maps a type where the main PHP type is an iterator.
*/
private function mapIteratorDocBlockType(Type $type, ?Type $docBlockType, bool $isNullable): GraphQLType
{
if ($docBlockType === null) {
throw TypeMappingException::createFromType($type);
}
if (! $isNullable) {
// Let's check a "null" value in the docblock
$isNullable = $this->isNullable($docBlockType);
}
$filteredDocBlockTypes = $this->typesWithoutNullable($docBlockType);
if (empty($filteredDocBlockTypes)) {
throw TypeMappingException::createFromType($type);
}
$unionTypes = [];
$lastException = null;
foreach ($filteredDocBlockTypes as $singleDocBlockType) {
try {
$singleDocBlockType = $this->getTypeInArray($singleDocBlockType);
if ($singleDocBlockType !== null) {
$subGraphQlType = $this->toGraphQlType($singleDocBlockType, null, false);
} else {
$subGraphQlType = null;
}
$unionTypes[] = $this->toGraphQlType($type, $subGraphQlType, false);
// TODO: add here a scan of the $type variable and do stuff if it is iterable.
// TODO: remove the iterator type if specified in the docblock (@return Iterator|User[])
// TODO: check there is at least one array (User[])
} catch (CannotMapTypeExceptionInterface|TypeMappingException $e) {
// We have several types. It is ok not to be able to match one.
$lastException = $e;
}
}
if (empty($unionTypes) && $lastException !== null) {
// We have an issue, let's try without the subType
return $this->mapDocBlockType($type, $docBlockType, $isNullable, false);
}
if (count($unionTypes) === 1) {
$graphQlType = $unionTypes[0];
} else {
$graphQlType = new UnionType($unionTypes, $this->typeMapper);
}
if (! $isNullable) {
$graphQlType = GraphQLType::nonNull($graphQlType);
}
return $graphQlType;
}
/**
* Casts a Type to a GraphQL type.
* Does not deal with nullable.
*
* @return GraphQLType (GraphQLType&InputType)|(GraphQLType&OutputType)
* @throws CannotMapTypeExceptionInterface
*/
private function toGraphQlType(Type $type, ?GraphQLType $subType, bool $mapToInputType): GraphQLType
{
if ($type instanceof Integer) {
return GraphQLType::int();
}
if ($type instanceof String_) {
return GraphQLType::string();
}
if ($type instanceof Boolean) {
return GraphQLType::boolean();
}
if ($type instanceof Float_) {
return GraphQLType::float();
}
if ($type instanceof Object_) {
$fqcn = (string) $type->getFqsen();
switch ($fqcn) {
case '\DateTimeImmutable':
case '\DateTimeInterface':
return DateTimeType::getInstance();
case '\\' . UploadedFileInterface::class:
return CustomTypesRegistry::getUploadType();
case '\DateTime':
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
case '\\' . ID::class:
return GraphQLType::id();
default:
$className = ltrim((string) $type->getFqsen(), '\\');
if ($mapToInputType) {
return $this->typeMapper->mapClassToInputType($className);
}
return $this->typeMapper->mapClassToInterfaceOrType($className, $subType);
}
} elseif ($type instanceof Array_) {
return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType)));
} else {
throw TypeMappingException::createFromType($type);
}
}
/**
* Removes "null" from the type (if it is compound). Return an array of types (not a Compound type).
*/
private function typesWithoutNullable(Type $docBlockTypeHint): array
{
if ($docBlockTypeHint instanceof Compound) {
$docBlockTypeHints = iterator_to_array($docBlockTypeHint);
} else {
$docBlockTypeHints = [$docBlockTypeHint];
}
return array_filter($docBlockTypeHints, function ($item) {
return ! $item instanceof Null_;
});
}
/**
* Drops "Nullable" types and return the core type.
*/
private function dropNullableType(Type $typeHint): Type
{
if ($typeHint instanceof Nullable) {
return $typeHint->getActualType();
}
return $typeHint;
}
/**
* Resolves a list type.
*/
private function getTypeInArray(Type $typeHint): ?Type
{
$typeHint = $this->dropNullableType($typeHint);
if (! $typeHint instanceof Array_) {
return null;
}
return $this->dropNullableType($typeHint->getValueType());
}
private function isNullable(Type $docBlockTypeHint): bool
{
if ($docBlockTypeHint instanceof Null_) {
return true;
}
if ($docBlockTypeHint instanceof Compound) {
foreach ($docBlockTypeHint as $type) {
if ($type instanceof Null_) {
return true;
}
}
}
return false;
}
/**
* Resolves "self" types into the class type.
*/
private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
{
if ($type instanceof Self_) {
return new Object_(new Fqsen('\\' . $reflectionClass->getName()));
}
return $type;
}
}

View File

@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use TheCodingMachine\GraphQLite\Hydrators\HydratorInterface;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory;
use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface;
use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
class FieldsBuilderFactory
{
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var HydratorInterface
*/
private $hydrator;
/**
* @var AuthenticationServiceInterface
*/
private $authenticationService;
/**
* @var AuthorizationServiceInterface
*/
private $authorizationService;
/**
* @var CachedDocBlockFactory
*/
private $cachedDocBlockFactory;
/**
* @var TypeResolver
*/
private $typeResolver;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
public function __construct(
AnnotationReader $annotationReader,
HydratorInterface $hydrator,
AuthenticationServiceInterface $authenticationService,
AuthorizationServiceInterface $authorizationService,
TypeResolver $typeResolver,
CachedDocBlockFactory $cachedDocBlockFactory,
NamingStrategyInterface $namingStrategy
) {
$this->annotationReader = $annotationReader;
$this->hydrator = $hydrator;
$this->authenticationService = $authenticationService;
$this->authorizationService = $authorizationService;
$this->typeResolver = $typeResolver;
$this->cachedDocBlockFactory = $cachedDocBlockFactory;
$this->namingStrategy = $namingStrategy;
}
public function buildFieldsBuilder(RecursiveTypeMapperInterface $typeMapper): FieldsBuilder
{
return new FieldsBuilder(
$this->annotationReader,
$typeMapper,
new ArgumentResolver($this->hydrator),
$this->authenticationService,
$this->authorizationService,
$this->typeResolver,
$this->cachedDocBlockFactory,
$this->namingStrategy
);
}
}

View File

@ -24,14 +24,8 @@ use Psr\Http\Server\RequestHandlerInterface;
class GraphQLMiddleware implements MiddlewareInterface
{
/**
* @var Schema
*/
protected $schema;
public function __construct(Schema $schema)
public function __construct(protected Schema $schema)
{
$this->schema = $schema;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
@ -42,7 +36,7 @@ class GraphQLMiddleware implements MiddlewareInterface
$input = $request->getParsedBody();
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
$variableValues = $input['variables'] ?? null;
$result = GraphQL::executeQuery($this->schema, $query, null, null, $variableValues);
return ResponseContext::get()->setBody(new SwooleStream(Json::encode($result)));

View File

@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use GraphQL\Type\Definition\InputObjectType;
use Psr\Container\ContainerInterface;
use ReflectionMethod;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
/**
* This class is in charge of creating Webonyx InputTypes from Factory annotations.
*/
class InputTypeGenerator
{
/**
* @var FieldsBuilderFactory
*/
private $fieldsBuilderFactory;
/**
* @var array<string, InputObjectType>
*/
private $cache = [];
/**
* @var ArgumentResolver
*/
private $argumentResolver;
/**
* @var InputTypeUtils
*/
private $inputTypeUtils;
public function __construct(
InputTypeUtils $inputTypeUtils,
FieldsBuilderFactory $fieldsBuilderFactory,
ArgumentResolver $argumentResolver
) {
$this->inputTypeUtils = $inputTypeUtils;
$this->fieldsBuilderFactory = $fieldsBuilderFactory;
$this->argumentResolver = $argumentResolver;
}
public function mapFactoryMethod(string $factory, string $methodName, RecursiveTypeMapperInterface $recursiveTypeMapper, ContainerInterface $container): InputObjectType
{
$method = new ReflectionMethod($factory, $methodName);
if ($method->isStatic()) {
$object = $factory;
} else {
$object = $container->get($factory);
}
[$inputName, $className] = $this->inputTypeUtils->getInputTypeNameAndClassName($method);
if (! isset($this->cache[$inputName])) {
// TODO: add comment argument.
$this->cache[$inputName] = new ResolvableInputObjectType($inputName, $this->fieldsBuilderFactory, $recursiveTypeMapper, $object, $methodName, $this->argumentResolver, null);
}
return $this->cache[$inputName];
}
}

View File

@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\TypeResolver;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use TheCodingMachine\GraphQLite\MissingTypeHintException;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
class InputTypeUtils
{
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
public function __construct(
AnnotationReader $annotationReader,
NamingStrategyInterface $namingStrategy
) {
$this->annotationReader = $annotationReader;
$this->namingStrategy = $namingStrategy;
}
/**
* Returns an array with 2 elements: [ $inputName, $className ].
*
* @return string[]
*/
public function getInputTypeNameAndClassName(ReflectionMethod $method): array
{
$fqsen = ltrim((string) $this->validateReturnType($method), '\\');
$factory = $this->annotationReader->getFactoryAnnotation($method);
if ($factory === null) {
throw new RuntimeException($method->getDeclaringClass()->getName() . '::' . $method->getName() . ' has no @Factory annotation.');
}
return [$this->namingStrategy->getInputTypeName($fqsen, $factory), $fqsen];
}
private function validateReturnType(ReflectionMethod $refMethod): Fqsen
{
$returnType = $refMethod->getReturnType();
if ($returnType === null) {
throw MissingTypeHintException::missingReturnType($refMethod);
}
if ($returnType->allowsNull()) {
throw MissingTypeHintException::nullableReturnType($refMethod);
}
$type = (string) $returnType;
$typeResolver = new TypeResolver();
$phpdocType = $typeResolver->resolve($type);
$phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass());
if (! $phpdocType instanceof Object_) {
throw MissingTypeHintException::invalidReturnType($refMethod);
}
return $phpdocType->getFqsen();
}
/**
* Resolves "self" types into the class type.
*/
private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
{
if ($type instanceof Self_) {
return new Object_(new Fqsen('\\' . $reflectionClass->getName()));
}
return $type;
}
}

View File

@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\GraphQL\Annotation\Mutation;
use Hyperf\GraphQL\Annotation\Query;
use Psr\Container\ContainerInterface;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\QueryField;
use TheCodingMachine\GraphQLite\QueryProviderInterface;
class QueryProvider implements QueryProviderInterface
{
/**
* @var FieldsBuilderFactory
*/
private $fieldsBuilderFactory;
/**
* @var RecursiveTypeMapperInterface
*/
private $recursiveTypeMapper;
/**
* @var ContainerInterface
*/
private $container;
public function __construct(FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper, ContainerInterface $container)
{
$this->fieldsBuilderFactory = $fieldsBuilderFactory;
$this->recursiveTypeMapper = $recursiveTypeMapper;
$this->container = $container;
}
/**
* @return QueryField[]
*/
public function getQueries(): array
{
$queryList = [];
$classes = AnnotationCollector::getMethodsByAnnotation(Query::class);
$classes = array_unique(array_column($classes, 'class'));
foreach ($classes as $className) {
$fieldsBuilder = $this->fieldsBuilderFactory->buildFieldsBuilder($this->recursiveTypeMapper);
$queryList = array_merge($queryList, $fieldsBuilder->getQueries($this->container->get($className)));
}
return $queryList;
}
/**
* @return QueryField[]
*/
public function getMutations(): array
{
$mutationList = [];
$classes = AnnotationCollector::getMethodsByAnnotation(Mutation::class);
$classes = array_unique(array_column($classes, 'class'));
foreach ($classes as $className) {
$fieldsBuilder = $this->fieldsBuilderFactory->buildFieldsBuilder($this->recursiveTypeMapper);
$mutationList = array_merge($mutationList, $fieldsBuilder->getMutations($this->container->get($className)));
}
return $mutationList;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Hyperf\Di\Annotation\AnnotationReader as HyperfAnnotationReader;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
class Reader implements \Doctrine\Common\Annotations\Reader
{
public function __construct(protected HyperfAnnotationReader $reader)
{
}
public function getClassAnnotations(ReflectionClass $class)
{
return [];
}
public function getClassAnnotation(ReflectionClass $class, $annotationName)
{
return null;
}
public function getMethodAnnotations(ReflectionMethod $method)
{
return [];
}
public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
{
return null;
}
public function getPropertyAnnotations(ReflectionProperty $property)
{
return [];
}
public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
{
return null;
}
}

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Cache\ApcuCache;
class ReaderFactory
{
public function __invoke()
{
AnnotationRegistry::registerLoader('class_exists');
$doctrineAnnotationReader = new AnnotationReader();
if (function_exists('apcu_fetch')) {
$doctrineAnnotationReader = new CachedReader($doctrineAnnotationReader, new ApcuCache(), true);
}
return $doctrineAnnotationReader;
}
}

View File

@ -1,81 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Doctrine\Common\Annotations\Reader;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Lock\Factory as LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\PorpaginasTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\TypeRegistry;
class RecursiveTypeMapperFactory
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var CacheInterface
*/
private $cache;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
/**
* @var TypeRegistry
*/
private $typeRegistry;
public function __construct(ContainerInterface $container, CacheInterface $cache, NamingStrategyInterface $namingStrategy, TypeRegistry $typeRegistry)
{
$this->cache = $cache;
$this->container = $container;
$this->namingStrategy = $namingStrategy;
$this->typeRegistry = $typeRegistry;
}
public function __invoke()
{
$annotationReader = new AnnotationReader($this->container->get(Reader::class), AnnotationReader::LAX_MODE);
$typeGenerator = $this->container->get(TypeGenerator::class);
$inputTypeGenerator = $this->container->get(InputTypeGenerator::class);
$inputTypeUtils = $this->container->get(InputTypeUtils::class);
$namingStrategy = $this->container->get(NamingStrategyInterface::class);
$lockStore = new FlockStore(sys_get_temp_dir());
$lockFactory = new LockFactory($lockStore);
$typeMappers[] = new TypeMapper(
'app',
$typeGenerator,
$inputTypeGenerator,
$inputTypeUtils,
$this->container,
$annotationReader,
$namingStrategy,
$lockFactory,
$this->cache
);
$typeMappers[] = new PorpaginasTypeMapper();
$compositeTypeMapper = new CompositeTypeMapper($typeMappers);
return new RecursiveTypeMapper($compositeTypeMapper, $this->namingStrategy, $this->cache, $this->typeRegistry);
}
}

View File

@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use GraphQL\Type\Definition\InputObjectType;
use ReflectionMethod;
use TheCodingMachine\GraphQLite\GraphQLException;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\ResolvableInputInterface;
use TheCodingMachine\GraphQLite\Types\ResolvableInputObjectType as TheCodingMachineResolvableInputObjectType;
use function get_class;
/**
* A GraphQL input object that can be resolved using a factory.
*/
class ResolvableInputObjectType extends TheCodingMachineResolvableInputObjectType implements ResolvableInputInterface
{
/**
* @var ArgumentResolver
*/
private $argumentResolver;
/**
* @var array<int, object|string>|callable
*/
private $resolve;
/**
* QueryField constructor.
* @param object|string $factory
*/
public function __construct(string $name, FieldsBuilderFactory $controllerQueryProviderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper, $factory, string $methodName, ArgumentResolver $argumentResolver, ?string $comment, array $additionalConfig = [])
{
$this->argumentResolver = $argumentResolver;
$this->resolve = [$factory, $methodName];
$fields = function () use ($controllerQueryProviderFactory, $factory, $methodName, $recursiveTypeMapper) {
$method = new ReflectionMethod($factory, $methodName);
$fieldProvider = $controllerQueryProviderFactory->buildFieldsBuilder($recursiveTypeMapper);
return $fieldProvider->getInputFields($method);
};
$config = [
'name' => $name,
'fields' => $fields,
];
if ($comment) {
$config['description'] = $comment;
}
$config += $additionalConfig;
InputObjectType::__construct($config);
}
/**
* @return object
*/
public function resolve(array $args)
{
$toPassArgs = [];
foreach ($this->getFields() as $name => $field) {
$type = $field->getType();
if (isset($args[$name])) {
$val = $this->argumentResolver->resolve($args[$name], $type);
} elseif ($field->defaultValueExists()) {
$val = $field->defaultValue;
} else {
throw new GraphQLException("Expected argument '{$name}' was not provided in GraphQL input type '" . $this->name . "' used in factory '" . get_class($this->resolve[0]) . '::' . $this->resolve[1] . "()'");
}
$toPassArgs[] = $val;
}
$resolve = $this->resolve;
return $resolve(...$toPassArgs);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use TheCodingMachine\GraphQLite\SchemaFactory as BaseSchemaFactory;
class SchemaFactory
{
public function __invoke(ContainerInterface $container)
{
$factory = new BaseSchemaFactory($container->get(CacheInterface::class), $container);
$factory->addTypeNamespace('App');
$factory->addControllerNamespace('App');
$factory->setDoctrineAnnotationReader($container->get(Reader::class));
return $factory->createSchema();
}
}

View File

@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
/**
* An object type built from the Type annotation.
*/
class TypeAnnotatedObjectType extends MutableObjectType
{
/**
* @var string
*/
private $className;
public function __construct(string $className, array $config)
{
$this->className = $className;
parent::__construct($config);
}
public static function createFromAnnotatedClass(string $typeName, string $className, $annotatedObject, FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper): self
{
return new self($className, [
'name' => $typeName,
'fields' => function () use ($annotatedObject, $recursiveTypeMapper, $className, $fieldsBuilderFactory) {
$parentClass = get_parent_class($className);
$parentType = null;
if ($parentClass !== false) {
if ($recursiveTypeMapper->canMapClassToType($parentClass)) {
$parentType = $recursiveTypeMapper->mapClassToType($parentClass, null);
}
}
$fieldProvider = $fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper);
if ($annotatedObject !== null) {
$fields = $fieldProvider->getFields($annotatedObject);
} else {
$fields = $fieldProvider->getSelfFields($className);
}
if ($parentType !== null) {
$finalFields = $parentType->getFields();
foreach ($fields as $name => $field) {
$finalFields[$name] = $field;
}
return $finalFields;
}
return $fields;
},
'interfaces' => function () use ($className, $recursiveTypeMapper) {
return $recursiveTypeMapper->findInterfaces($className);
},
]);
}
public function getMappedClassName(): string
{
return $this->className;
}
}

View File

@ -1,116 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\MissingAnnotationException;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\TypeRegistry;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
/**
* This class is in charge of creating Webonyx GraphQL types from annotated objects that do not extend the
* Webonyx ObjectType class.
*/
class TypeGenerator
{
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var FieldsBuilderFactory
*/
private $fieldsBuilderFactory;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
/**
* @var TypeRegistry
*/
private $typeRegistry;
/**
* @var ContainerInterface
*/
private $container;
public function __construct(
AnnotationReader $annotationReader,
FieldsBuilderFactory $fieldsBuilderFactory,
NamingStrategyInterface $namingStrategy,
TypeRegistry $typeRegistry,
ContainerInterface $container
) {
$this->annotationReader = $annotationReader;
$this->fieldsBuilderFactory = $fieldsBuilderFactory;
$this->namingStrategy = $namingStrategy;
$this->typeRegistry = $typeRegistry;
$this->container = $container;
}
/**
* @param string $annotatedObjectClassName the FQCN of an object with a Type annotation
* @throws ReflectionException
*/
public function mapAnnotatedObject(string $annotatedObjectClassName, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType
{
$refTypeClass = new ReflectionClass($annotatedObjectClassName);
$typeField = $this->annotationReader->getTypeAnnotation($refTypeClass);
if ($typeField === null) {
throw MissingAnnotationException::missingTypeException();
}
$typeName = $this->namingStrategy->getOutputTypeName($refTypeClass->getName(), $typeField);
if ($this->typeRegistry->hasType($typeName)) {
return $this->typeRegistry->getMutableObjectType($typeName);
}
if (! $typeField->isSelfType()) {
$annotatedObject = $this->container->get($annotatedObjectClassName);
} else {
$annotatedObject = null;
}
return TypeAnnotatedObjectType::createFromAnnotatedClass($typeName, $typeField->getClass(), $annotatedObject, $this->fieldsBuilderFactory, $recursiveTypeMapper);
}
/**
* @param object $annotatedObject an object with a ExtendType annotation
*/
public function extendAnnotatedObject($annotatedObject, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper)
{
$refTypeClass = new ReflectionClass($annotatedObject);
$extendTypeAnnotation = $this->annotationReader->getExtendTypeAnnotation($refTypeClass);
if ($extendTypeAnnotation === null) {
throw MissingAnnotationException::missingExtendTypeException();
}
$type->addFields(function () use ($annotatedObject, $recursiveTypeMapper) {
$fieldProvider = $this->fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper);
return $fieldProvider->getFields($annotatedObject);
});
}
}

View File

@ -1,865 +0,0 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\OutputType;
use Hyperf\GraphQL\Annotation\Type;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Symfony\Component\Lock\Factory as LockFactory;
use Symfony\Component\Lock\Lock;
use TheCodingMachine\GraphQLite\Annotations\ExtendType;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
use function array_keys;
use function class_exists;
use function filemtime;
/**
* Scans all the classes in a given namespace of the main project (not the vendor directory).
* Analyzes all classes and uses the Type annotation to find the types automatically.
*
* Assumes that the container contains a class whose identifier is the same as the class name.
*/
class TypeMapper implements TypeMapperInterface
{
/**
* @var string
*/
private $namespace;
/**
* @var AnnotationReader
*/
private $annotationReader;
/**
* @var CacheInterface
*/
private $cache;
/**
* @var null|int
*/
private $globTtl;
/**
* @var array<string,string> Maps a domain class to the GraphQL type annotated class
*/
private $mapClassToTypeArray = [];
/**
* @var array<string,array<string,string>> Maps a domain class to one or many type extenders (with the ExtendType annotation) The array of type extenders has a key and value equals to FQCN
*/
private $mapClassToExtendTypeArray = [];
/**
* @var array<string,string> Maps a GraphQL type name to the GraphQL type annotated class
*/
private $mapNameToType = [];
/**
* @var array<string,array<string,string>> Maps a GraphQL type name to one or many type extenders (with the ExtendType annotation) The array of type extenders has a key and value equals to FQCN
*/
private $mapNameToExtendType = [];
/**
* @var array<string,string[]> Maps a domain class to the factory method that creates the input type in the form [classname, methodname]
*/
private $mapClassToFactory = [];
/**
* @var array<string,string[]> Maps a GraphQL input type name to the factory method that creates the input type in the form [classname, methodname]
*/
private $mapInputNameToFactory = [];
/**
* @var ContainerInterface
*/
private $container;
/**
* @var TypeGenerator
*/
private $typeGenerator;
/**
* @var null|int
*/
private $mapTtl;
/**
* @var bool
*/
private $fullMapComputed = false;
/**
* @var bool
*/
private $fullMapClassToExtendTypeArrayComputed = false;
/**
* @var bool
*/
private $fullMapNameToExtendTypeArrayComputed = false;
/**
* @var NamingStrategyInterface
*/
private $namingStrategy;
/**
* @var InputTypeGenerator
*/
private $inputTypeGenerator;
/**
* @var InputTypeUtils
*/
private $inputTypeUtils;
/**
* The array of globbed classes.
* Only instantiable classes are returned.
* Key: fully qualified class name.
*
* @var array<string,ReflectionClass>
*/
private $classes;
/**
* @var bool
*/
private $recursive;
/**
* @var LockFactory
*/
private $lockFactory;
/**
* @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
*/
public function __construct(string $namespace, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, LockFactory $lockFactory, CacheInterface $cache, ?int $globTtl = 2, ?int $mapTtl = null, bool $recursive = true)
{
$this->namespace = $namespace;
$this->typeGenerator = $typeGenerator;
$this->container = $container;
$this->annotationReader = $annotationReader;
$this->namingStrategy = $namingStrategy;
$this->cache = $cache;
$this->globTtl = $globTtl;
$this->mapTtl = $mapTtl;
$this->inputTypeGenerator = $inputTypeGenerator;
$this->inputTypeUtils = $inputTypeUtils;
$this->recursive = $recursive;
$this->lockFactory = $lockFactory;
}
/**
* Returns true if this type mapper can map the $className FQCN to a GraphQL type.
*/
public function canMapClassToType(string $className): bool
{
$typeClassName = $this->getTypeFromCacheByObjectClass($className);
if ($typeClassName !== null) {
return true;
}
$map = $this->getMapClassToType();
return isset($map[$className]);
}
/**
* Maps a PHP fully qualified class name to a GraphQL type.
*
* @param string $className the exact class name to look for (this function does not look into parent classes)
* @param null|OutputType $subType an optional sub-type if the main class is an iterator that needs to be typed
* @throws CannotMapTypeExceptionInterface
*/
public function mapClassToType(string $className, ?OutputType $subType, RecursiveTypeMapperInterface $recursiveTypeMapper): MutableObjectType
{
$typeClassName = $this->getTypeFromCacheByObjectClass($className);
if ($typeClassName === null) {
$map = $this->getMapClassToType();
if (! isset($map[$className])) {
throw CannotMapTypeException::createForType($className);
}
$typeClassName = $map[$className];
}
return $this->typeGenerator->mapAnnotatedObject($typeClassName, $recursiveTypeMapper);
}
/**
* Returns the list of classes that have matching input GraphQL types.
*
* @return string[]
*/
public function getSupportedClasses(): array
{
return array_keys($this->getMapClassToType());
}
/**
* Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
*/
public function canMapClassToInputType(string $className): bool
{
$factory = $this->getFactoryFromCacheByObjectClass($className);
if ($factory !== null) {
return true;
}
$map = $this->getMapClassToFactory();
return isset($map[$className]);
}
/**
* Maps a PHP fully qualified class name to a GraphQL input type.
*
* @throws CannotMapTypeExceptionInterface
*/
public function mapClassToInputType(string $className, RecursiveTypeMapperInterface $recursiveTypeMapper): InputObjectType
{
$factory = $this->getFactoryFromCacheByObjectClass($className);
if ($factory === null) {
$map = $this->getMapClassToFactory();
if (! isset($map[$className])) {
throw CannotMapTypeException::createForInputType($className);
}
$factory = $map[$className];
}
return $this->inputTypeGenerator->mapFactoryMethod($factory[0], $factory[1], $recursiveTypeMapper, $this->container);
}
/**
* Returns a GraphQL type by name (can be either an input or output type).
*
* @param string $typeName The name of the GraphQL type
* @return \GraphQL\Type\Definition\Type&(InputType|OutputType)
* @throws CannotMapTypeExceptionInterface
* @throws ReflectionException
*/
public function mapNameToType(string $typeName, RecursiveTypeMapperInterface $recursiveTypeMapper): \GraphQL\Type\Definition\Type
{
$typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
if ($typeClassName === null) {
$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
if ($factory === null) {
$mapNameToType = $this->getMapNameToType();
if (isset($mapNameToType[$typeName])) {
$typeClassName = $mapNameToType[$typeName];
} else {
$mapInputNameToFactory = $this->getMapInputNameToFactory();
if (isset($mapInputNameToFactory[$typeName])) {
$factory = $mapInputNameToFactory[$typeName];
}
}
}
}
if (isset($typeClassName)) {
return $this->typeGenerator->mapAnnotatedObject($typeClassName, $recursiveTypeMapper);
}
if (isset($factory)) {
return $this->inputTypeGenerator->mapFactoryMethod($factory[0], $factory[1], $recursiveTypeMapper, $this->container);
}
throw CannotMapTypeException::createForName($typeName);
}
/**
* Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type.
*
* @param string $typeName The name of the GraphQL type
*/
public function canMapNameToType(string $typeName): bool
{
$typeClassName = $this->getTypeFromCacheByGraphQLTypeName($typeName);
if ($typeClassName !== null) {
return true;
}
$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
if ($factory !== null) {
return true;
}
$this->getMaps();
return isset($this->mapNameToType[$typeName]) || isset($this->mapInputNameToFactory[$typeName]);
}
/**
* Returns true if this type mapper can extend an existing type for the $className FQCN.
*/
public function canExtendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool
{
$extendTypeClassName = $this->getExtendTypesFromCacheByObjectClass($className);
if ($extendTypeClassName === null) {
$map = $this->getMapClassToExtendTypeArray();
}
return isset($this->mapClassToExtendTypeArray[$className]);
}
/**
* Extends the existing GraphQL type that is mapped to $className.
*
* @throws CannotMapTypeExceptionInterface
*/
public function extendTypeForClass(string $className, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void
{
$extendTypeClassNames = $this->getExtendTypesFromCacheByObjectClass($className);
if ($extendTypeClassNames === null) {
$this->getMapClassToExtendTypeArray();
}
if (! isset($this->mapClassToExtendTypeArray[$className])) {
throw CannotMapTypeException::createForExtendType($className, $type);
}
foreach ($this->mapClassToExtendTypeArray[$className] as $extendedTypeClass) {
$this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper);
}
}
/**
* Returns true if this type mapper can extend an existing type for the $typeName GraphQL type.
*/
public function canExtendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): bool
{
$typeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName);
if ($typeClassNames !== null) {
return true;
}
/*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
if ($factory !== null) {
return true;
}*/
$map = $this->getMapNameToExtendType($recursiveTypeMapper);
return isset($map[$typeName])/* || isset($this->mapInputNameToFactory[$typeName]) */;
}
/**
* Extends the existing GraphQL type that is mapped to the $typeName GraphQL type.
*
* @throws CannotMapTypeExceptionInterface
*/
public function extendTypeForName(string $typeName, MutableObjectType $type, RecursiveTypeMapperInterface $recursiveTypeMapper): void
{
$extendTypeClassNames = $this->getExtendTypesFromCacheByGraphQLTypeName($typeName);
if ($extendTypeClassNames === null) {
/*$factory = $this->getFactoryFromCacheByGraphQLInputTypeName($typeName);
if ($factory === null) {*/
$map = $this->getMapNameToExtendType($recursiveTypeMapper);
if (! isset($map[$typeName])) {
throw CannotMapTypeException::createForExtendName($typeName, $type);
}
$extendTypeClassNames = $map[$typeName];
// }
}
foreach ($extendTypeClassNames as $extendedTypeClass) {
$this->typeGenerator->extendAnnotatedObject($this->container->get($extendedTypeClass), $type, $recursiveTypeMapper);
}
/*if (isset($this->mapInputNameToFactory[$typeName])) {
$factory = $this->mapInputNameToFactory[$typeName];
return $this->inputTypeGenerator->mapFactoryMethod($this->container->get($factory[0]), $factory[1], $recursiveTypeMapper);
}*/
}
/**
* Returns an array of fully qualified class names.
*
* @return array<string, array<string,string>>
*/
private function getMaps(): array
{
if ($this->fullMapComputed === false) {
$namespace = str_replace('\\', '_', $this->namespace);
$keyClassCache = 'globTypeMapper_' . $namespace;
$keyNameCache = 'globTypeMapper_names_' . $namespace;
$keyInputClassCache = 'globInputTypeMapper_' . $namespace;
$keyInputNameCache = 'globInputTypeMapper_names_' . $namespace;
$this->mapClassToTypeArray = $this->cache->get($keyClassCache);
$this->mapNameToType = $this->cache->get($keyNameCache);
$this->mapClassToFactory = $this->cache->get($keyInputClassCache);
$this->mapInputNameToFactory = $this->cache->get($keyInputNameCache);
if ($this->mapClassToTypeArray === null
|| $this->mapNameToType === null
|| $this->mapClassToFactory === null
|| $this->mapInputNameToFactory === null
) {
$lock = $this->lockFactory->createLock('buildmap_' . $this->namespace, 5);
if (! $lock->acquire()) {
// Lock is being held right now. Generation is happening.
// Let's wait and fetch the result from the cache.
$lock->acquire(true);
$lock->release();
return $this->getMaps();
}
try {
$this->buildMap();
} finally {
$lock->release();
}
// This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
// Defaults to 2 seconds.
$this->cache->set($keyClassCache, $this->mapClassToTypeArray, $this->globTtl);
$this->cache->set($keyNameCache, $this->mapNameToType, $this->globTtl);
$this->cache->set($keyInputClassCache, $this->mapClassToFactory, $this->globTtl);
$this->cache->set($keyInputNameCache, $this->mapInputNameToFactory, $this->globTtl);
}
$this->fullMapComputed = true;
}
return [
'mapClassToTypeArray' => $this->mapClassToTypeArray,
'mapNameToType' => $this->mapNameToType,
'mapClassToFactory' => $this->mapClassToFactory,
'mapInputNameToFactory' => $this->mapInputNameToFactory,
];
}
private function getMapClassToType(): array
{
return $this->getMaps()['mapClassToTypeArray'];
}
private function getMapNameToType(): array
{
return $this->getMaps()['mapNameToType'];
}
private function getMapClassToFactory(): array
{
return $this->getMaps()['mapClassToFactory'];
}
private function getMapInputNameToFactory(): array
{
return $this->getMaps()['mapInputNameToFactory'];
}
private function getMapClassToExtendTypeArray(): array
{
if ($this->fullMapClassToExtendTypeArrayComputed === false) {
$namespace = str_replace('\\', '_', $this->namespace);
$keyExtendClassCache = 'globTypeMapperExtend_' . $namespace;
$this->mapClassToExtendTypeArray = $this->cache->get($keyExtendClassCache);
if ($this->mapClassToExtendTypeArray === null) {
$lock = $this->lockFactory->createLock('buildmapclassextend_' . $this->namespace, 5);
if (! $lock->acquire()) {
// Lock is being held right now. Generation is happening.
// Let's wait and fetch the result from the cache.
$lock->acquire(true);
$lock->release();
return $this->getMapClassToExtendTypeArray();
}
$lock->acquire(true);
try {
$this->buildMapClassToExtendTypeArray($lock);
} finally {
$lock->release();
}
// This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
// Defaults to 2 seconds.
$this->cache->set($keyExtendClassCache, $this->mapClassToExtendTypeArray, $this->globTtl);
}
$this->fullMapClassToExtendTypeArrayComputed = true;
}
return $this->mapClassToExtendTypeArray;
}
private function getMapNameToExtendType(RecursiveTypeMapperInterface $recursiveTypeMapper): array
{
if ($this->fullMapNameToExtendTypeArrayComputed === false) {
$namespace = str_replace('\\', '_', $this->namespace);
$keyExtendNameCache = 'globTypeMapperExtend_names_' . $namespace;
$this->mapNameToExtendType = $this->cache->get($keyExtendNameCache);
if ($this->mapNameToExtendType === null) {
$lock = $this->lockFactory->createLock('buildmapnameextend_' . $this->namespace, 5);
if (! $lock->acquire()) {
// Lock is being held right now. Generation is happening.
// Let's wait and fetch the result from the cache.
$lock->acquire(true);
$lock->release();
return $this->getMapNameToExtendType($recursiveTypeMapper);
}
$lock->acquire(true);
try {
$this->buildMapNameToExtendTypeArray($recursiveTypeMapper);
} finally {
$lock->release();
}
// This is a very short lived cache. Useful to avoid overloading a server in case of heavy load.
// Defaults to 2 seconds.
$this->cache->set($keyExtendNameCache, $this->mapNameToExtendType, $this->globTtl);
}
$this->fullMapNameToExtendTypeArrayComputed = true;
}
return $this->mapNameToExtendType;
}
/**
* Returns the array of globbed classes.
* Only instantiable classes are returned.
*
* @return array<string,ReflectionClass> Key: fully qualified class name
*/
private function getClassList(): array
{
if ($this->classes === null) {
$this->classes = [];
$classes = ClassCollector::getClasses();
foreach ($classes as $className) {
if (! class_exists($className)) {
continue;
}
$refClass = new ReflectionClass($className);
if (! $refClass->isInstantiable()) {
continue;
}
$this->classes[$className] = $refClass;
}
}
return $this->classes;
}
private function buildMap(): void
{
$this->mapClassToTypeArray = [];
$this->mapNameToType = [];
$this->mapClassToFactory = [];
$this->mapInputNameToFactory = [];
/** @var ReflectionClass[] $classes */
$classes = $this->getClassList();
foreach ($classes as $className => $refClass) {
$type = $this->annotationReader->getTypeAnnotation($refClass);
if ($type !== null) {
if (isset($this->mapClassToTypeArray[$type->getClass()])) {
throw DuplicateMappingException::createForType($type->getClass(), $this->mapClassToTypeArray[$type->getClass()], $className);
}
$this->storeTypeInCache($className, $type, $refClass->getFileName());
}
$isAbstract = $refClass->isAbstract();
foreach ($refClass->getMethods() as $method) {
if (! $method->isPublic() || ($isAbstract && ! $method->isStatic())) {
continue;
}
$factory = $this->annotationReader->getFactoryAnnotation($method);
if ($factory !== null) {
[$inputName, $className] = $this->inputTypeUtils->getInputTypeNameAndClassName($method);
if (isset($this->mapClassToFactory[$className])) {
throw DuplicateMappingException::createForFactory($className, $this->mapClassToFactory[$className][0], $this->mapClassToFactory[$className][1], $refClass->getName(), $method->name);
}
$this->storeInputTypeInCache($method, $inputName, $className, $refClass->getFileName());
}
}
}
}
private function buildMapClassToExtendTypeArray(Lock $lock): void
{
$lock->acquire(true);
try {
$this->mapClassToExtendTypeArray = [];
$classes = $this->getClassList();
foreach ($classes as $className => $refClass) {
$extendType = $this->annotationReader->getExtendTypeAnnotation($refClass);
if ($extendType !== null) {
$this->storeExtendTypeMapperByClassInCache($className, $extendType, $refClass->getFileName());
}
}
} finally {
$lock->release();
}
}
private function buildMapNameToExtendTypeArray(RecursiveTypeMapperInterface $recursiveTypeMapper): void
{
$this->mapNameToExtendType = [];
$classes = $this->getClassList();
foreach ($classes as $className => $refClass) {
$extendType = $this->annotationReader->getExtendTypeAnnotation($refClass);
if ($extendType !== null) {
$this->storeExtendTypeMapperByNameInCache($className, $extendType, $refClass->getFileName(), $recursiveTypeMapper);
}
}
}
/**
* Stores in cache the mapping TypeClass <=> Object class <=> GraphQL type name.
*/
private function storeTypeInCache(string $typeClassName, Type $type, string $typeFileName): void
{
$objectClassName = $type->getClass();
$this->mapClassToTypeArray[$objectClassName] = $typeClassName;
$this->cache->set('globTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $objectClassName), [
'filemtime' => filemtime($typeFileName),
'fileName' => $typeFileName,
'typeClass' => $typeClassName,
], $this->mapTtl);
$typeName = $this->namingStrategy->getOutputTypeName($typeClassName, $type);
$this->mapNameToType[$typeName] = $typeClassName;
$this->cache->set('globTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $typeName, [
'filemtime' => filemtime($typeFileName),
'fileName' => $typeFileName,
'typeClass' => $typeClassName,
], $this->mapTtl);
}
/**
* Stores in cache the mapping between InputType name <=> Object class.
*/
private function storeInputTypeInCache(ReflectionMethod $refMethod, string $inputName, string $className, string $fileName): void
{
$refArray = [$refMethod->getDeclaringClass()->getName(), $refMethod->getName()];
$this->mapClassToFactory[$className] = $refArray;
$this->cache->set('globInputTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $className), [
'filemtime' => filemtime($fileName),
'fileName' => $fileName,
'factory' => $refArray,
], $this->mapTtl);
$this->mapInputNameToFactory[$inputName] = $refArray;
$this->cache->set('globInputTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $inputName, [
'filemtime' => filemtime($fileName),
'fileName' => $fileName,
'factory' => $refArray,
], $this->mapTtl);
}
/**
* Stores in cache the mapping ExtendTypeClass <=> Object class.
*/
private function storeExtendTypeMapperByClassInCache(string $extendTypeClassName, ExtendType $extendType, string $typeFileName): void
{
$objectClassName = $extendType->getClass();
$this->mapClassToExtendTypeArray[$objectClassName][$extendTypeClassName] = $extendTypeClassName;
$this->cache->set('globExtendTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $objectClassName), [
'filemtime' => filemtime($typeFileName),
'fileName' => $typeFileName,
'extendTypeClasses' => $this->mapClassToExtendTypeArray[$objectClassName],
], $this->mapTtl);
}
/**
* Stores in cache the mapping ExtendTypeClass <=> name class.
*/
private function storeExtendTypeMapperByNameInCache(string $extendTypeClassName, ExtendType $extendType, string $typeFileName, RecursiveTypeMapperInterface $recursiveTypeMapper): void
{
$targetType = $recursiveTypeMapper->mapClassToType($extendType->getClass(), null);
$typeName = $targetType->name;
$this->mapNameToExtendType[$typeName][$extendTypeClassName] = $extendTypeClassName;
$this->cache->set('globExtendTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $typeName, [
'filemtime' => filemtime($typeFileName),
'fileName' => $typeFileName,
'extendTypeClasses' => $this->mapNameToExtendType[$typeName],
], $this->mapTtl);
}
private function getTypeFromCacheByObjectClass(string $className): ?string
{
if (isset($this->mapClassToTypeArray[$className])) {
return $this->mapClassToTypeArray[$className];
}
// Let's try from the cache
$item = $this->cache->get('globTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $className));
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'typeClass' => $typeClassName
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapClassToTypeArray[$className] = $typeClassName;
return $typeClassName;
}
}
// cache miss
return null;
}
private function getTypeFromCacheByGraphQLTypeName(string $graphqlTypeName): ?string
{
if (isset($this->mapNameToType[$graphqlTypeName])) {
return $this->mapNameToType[$graphqlTypeName];
}
// Let's try from the cache
$item = $this->cache->get('globTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $graphqlTypeName);
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'typeClass' => $typeClassName
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapNameToType[$graphqlTypeName] = $typeClassName;
return $typeClassName;
}
}
// cache miss
return null;
}
/**
* @return null|string[] A pointer to the factory [$className, $methodName] or null on cache miss
*/
private function getFactoryFromCacheByObjectClass(string $className): ?array
{
if (isset($this->mapClassToFactory[$className])) {
return $this->mapClassToFactory[$className];
}
// Let's try from the cache
$item = $this->cache->get('globInputTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $className));
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'factory' => $factory
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapClassToFactory[$className] = $factory;
return $factory;
}
}
// cache miss
return null;
}
/**
* @return null|array<string,string> An array of classes with the ExtendType annotation (key and value = FQCN)
*/
private function getExtendTypesFromCacheByObjectClass(string $className): ?array
{
if (isset($this->mapClassToExtendTypeArray[$className])) {
return $this->mapClassToExtendTypeArray[$className];
}
// Let's try from the cache
$item = $this->cache->get('globExtendTypeMapperByClass_' . str_replace('\\', '_', $this->namespace) . '_' . str_replace('\\', '_', $className));
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'extendTypeClasses' => $extendTypeClassNames
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapClassToExtendTypeArray[$className] = $extendTypeClassNames;
return $extendTypeClassNames;
}
}
// cache miss
return null;
}
/**
* @return null|array<string,string> An array of classes with the ExtendType annotation (key and value = FQCN)
*/
private function getExtendTypesFromCacheByGraphQLTypeName(string $graphqlTypeName): ?array
{
if (isset($this->mapNameToExtendType[$graphqlTypeName])) {
return $this->mapNameToExtendType[$graphqlTypeName];
}
// Let's try from the cache
$item = $this->cache->get('globExtendTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $graphqlTypeName);
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'extendTypeClasses' => $extendTypeClassNames
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapNameToExtendType[$graphqlTypeName] = $extendTypeClassNames;
return $extendTypeClassNames;
}
}
// cache miss
return null;
}
/**
* @return null|string[] A pointer to the factory [$className, $methodName] or null on cache miss
*/
private function getFactoryFromCacheByGraphQLInputTypeName(string $graphqlTypeName): ?array
{
if (isset($this->mapInputNameToFactory[$graphqlTypeName])) {
return $this->mapInputNameToFactory[$graphqlTypeName];
}
// Let's try from the cache
$item = $this->cache->get('globInputTypeMapperByName_' . str_replace('\\', '_', $this->namespace) . '_' . $graphqlTypeName);
if ($item !== null) {
[
'filemtime' => $filemtime,
'fileName' => $typeFileName,
'factory' => $factory
] = $item;
if ($filemtime === @filemtime($typeFileName)) {
$this->mapInputNameToFactory[$graphqlTypeName] = $factory;
return $factory;
}
}
// cache miss
return null;
}
}