diff --git a/.travis.yml b/.travis.yml index dbd477bf64..9ceadd1daf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,13 @@ jobs: - composer phpstan after_script: - ./vendor/bin/coveralls -v + - stage: test + php: 7.4snapshot + env: PREFER_LOWEST="" + before_script: + - *composerupdate + script: + - *phpunit - stage: test php: 7.2 env: PREFER_LOWEST="" @@ -91,6 +98,5 @@ jobs: - git config --global user.email "${GH_EMAIL}" - echo "machine github.com login ${GH_NAME} password ${GH_TOKEN}" > ~/.netrc - cd website && yarn install && GIT_USER="${GH_NAME}" yarn run publish-gh-pages -matrix: allow_failures: - stage: test_dependencies diff --git a/composer.json b/composer.json index f4ac263941..bd17eacb01 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,14 @@ "doctrine/cache": "^1.8", "thecodingmachine/class-explorer": "^1.0.2", "psr/simple-cache": "^1", - "phpdocumentor/reflection-docblock": "^4.3", - "phpdocumentor/type-resolver": "^0.4", + "phpdocumentor/reflection-docblock": "^4.3 | ^5.0", + "phpdocumentor/type-resolver": "^0.4 || ^1.0.0", "psr/http-message": "^1", "ecodev/graphql-upload": "^4.0", "symfony/lock": "^3 || ^4" }, "require-dev": { - "phpunit/phpunit": "^6.1", + "phpunit/phpunit": "^7.5.16", "satooshi/php-coveralls": "^1.0", "symfony/cache": "^4.1.4", "mouf/picotainer": "^1.1", @@ -49,7 +49,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "3.1.x-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6b8eec52ef..c7b363d680 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,13 +8,13 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" - bootstrap="vendor/autoload.php" + bootstrap="tests/Bootstrap.php" > ./tests/ ./tests/dependencies/ + ./tests/Bootstrap.php @@ -24,7 +24,7 @@ - + diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 80c839c8bc..50c81eb439 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -233,7 +233,7 @@ private function getFieldsByAnnotations($controller, string $annotationName, boo } } - foreach ($refClass->getMethods() as $refMethod) { + foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) 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. @@ -308,7 +308,9 @@ private function mapReturnType(ReflectionMethod $refMethod, DocBlock $docBlockOb $returnType = $refMethod->getReturnType(); if ($returnType !== null) { $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); - $phpdocType = $typeResolver->resolve((string) $returnType); + $phpdocType = $typeResolver->resolve( + $returnType->getName() + ); $phpdocType = $this->resolveSelf($phpdocType, $refMethod->getDeclaringClass()); } else { $phpdocType = new Mixed_(); @@ -490,10 +492,11 @@ private function mapParameters(array $refParameters, DocBlock $docBlock): array $parameterType = $parameter->getType(); $allowsNull = $parameterType === null ? true : $parameterType->allowsNull(); - $type = (string) $parameterType; - if ($type === '') { + if ($parameterType === null) { throw MissingTypeHintException::missingTypeHint($parameter); } + + $type = $parameterType->getName(); $phpdocType = $typeResolver->resolve($type); $phpdocType = $this->resolveSelf($phpdocType, $parameter->getDeclaringClass()); diff --git a/src/GlobControllerQueryProvider.php b/src/GlobControllerQueryProvider.php index b999251a66..a679bf5f90 100644 --- a/src/GlobControllerQueryProvider.php +++ b/src/GlobControllerQueryProvider.php @@ -39,6 +39,10 @@ final class GlobControllerQueryProvider implements QueryProviderInterface * @var ContainerInterface */ private $container; + /** + * @var ClassNameMapper + */ + private $classNameMapper; /** * @var AggregateControllerQueryProvider */ @@ -69,10 +73,11 @@ final class GlobControllerQueryProvider implements QueryProviderInterface * @param int|null $cacheTtl * @param bool $recursive Whether subnamespaces of $namespace must be analyzed. */ - public function __construct(string $namespace, FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper, ContainerInterface $container, LockFactory $lockFactory, CacheInterface $cache, ?int $cacheTtl = null, bool $recursive = true) + public function __construct(string $namespace, FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper, ContainerInterface $container, LockFactory $lockFactory, CacheInterface $cache, ?ClassNameMapper $classNameMapper = null, ?int $cacheTtl = null, bool $recursive = true) { $this->namespace = $namespace; $this->container = $container; + $this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true); $this->cache = $cache; $this->cacheTtl = $cacheTtl; $this->fieldsBuilderFactory = $fieldsBuilderFactory; @@ -126,7 +131,7 @@ private function getInstancesList(): array */ private function buildInstancesList(): array { - $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->cacheTtl, ClassNameMapper::createFromComposerFile(null, null, true), $this->recursive); + $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->cacheTtl, $this->classNameMapper, $this->recursive); $classes = $explorer->getClasses(); $instances = []; foreach ($classes as $className) { diff --git a/src/InputTypeUtils.php b/src/InputTypeUtils.php index 8e3eb02104..a24a3b9571 100644 --- a/src/InputTypeUtils.php +++ b/src/InputTypeUtils.php @@ -55,7 +55,7 @@ private function validateReturnType(ReflectionMethod $refMethod): Fqsen throw MissingTypeHintException::nullableReturnType($refMethod); } - $type = (string) $returnType; + $type = $returnType->getName(); $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 82f0709fb2..e76c2d999b 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -126,15 +126,20 @@ final class GlobTypeMapper implements TypeMapperInterface * @var LockFactory */ private $lockFactory; + /** + * @var ClassNameMapper + */ + private $classNameMapper; /** * @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) + public function __construct(string $namespace, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, LockFactory $lockFactory, CacheInterface $cache, ClassNameMapper $classNameMapper = null, ?int $globTtl = 2, ?int $mapTtl = null, bool $recursive = true) { $this->namespace = $namespace; $this->typeGenerator = $typeGenerator; $this->container = $container; + $this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true); $this->annotationReader = $annotationReader; $this->namingStrategy = $namingStrategy; $this->cache = $cache; @@ -288,7 +293,7 @@ private function getClassList(): array { if ($this->classes === null) { $this->classes = []; - $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, ClassNameMapper::createFromComposerFile(null, null, true), $this->recursive); + $explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTtl, $this->classNameMapper, $this->recursive); $classes = $explorer->getClasses(); foreach ($classes as $className) { if (!\class_exists($className)) { @@ -329,8 +334,8 @@ private function buildMap(): void $isAbstract = $refClass->isAbstract(); - foreach ($refClass->getMethods() as $method) { - if (!$method->isPublic() || ($isAbstract && !$method->isStatic())) { + foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($isAbstract && !$method->isStatic()) { continue; } $factory = $this->annotationReader->getFactoryAnnotation($method); diff --git a/src/MissingTypeHintException.php b/src/MissingTypeHintException.php index 162e27fe68..418557cf10 100644 --- a/src/MissingTypeHintException.php +++ b/src/MissingTypeHintException.php @@ -18,7 +18,7 @@ public static function missingReturnType(ReflectionMethod $method): self public static function invalidReturnType(ReflectionMethod $method): self { - return new self(sprintf('The return type of factory "%s::%s" must be an object, "%s" passed instead.', $method->getDeclaringClass()->getName(), $method->getName(), $method->getReturnType())); + return new self(sprintf('The return type of factory "%s::%s" must be an object, "%s" passed instead.', $method->getDeclaringClass()->getName(), $method->getName(), $method->getReturnType() ? $method->getReturnType()->getName() : 'mixed')); } public static function nullableReturnType(ReflectionMethod $method): self diff --git a/src/QueryField.php b/src/QueryField.php index 2fd93a8c01..4b5944abc1 100644 --- a/src/QueryField.php +++ b/src/QueryField.php @@ -45,7 +45,7 @@ public function __construct(string $name, OutputType $type, array $arguments, ?c $config = [ 'name' => $name, 'type' => $type, - 'args' => array_map(function(array $item) { return $item['type']; }, $arguments) + 'args' => $arguments, ]; if ($comment) { $config['description'] = $comment; diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 0e33105224..adbfb96529 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -10,6 +10,7 @@ use Doctrine\Common\Cache\ApcuCache; use function extension_loaded; use GraphQL\Type\SchemaConfig; +use Mouf\Composer\ClassNameMapper; use Psr\Container\ContainerInterface; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Lock\Factory as LockFactory; @@ -74,6 +75,10 @@ class SchemaFactory * @var ContainerInterface */ private $container; + /** + * @var ClassNameMapper + */ + private $classNameMapper; /** * @var SchemaConfig */ @@ -180,6 +185,12 @@ public function setSchemaConfig(SchemaConfig $schemaConfig): self return $this; } + public function setClassNameMapper(ClassNameMapper $classNameMapper): self + { + $this->classNameMapper = $classNameMapper; + return $this; + } + public function createSchema(): Schema { $annotationReader = new AnnotationReader($this->getDoctrineAnnotationReader(), AnnotationReader::LAX_MODE); @@ -210,7 +221,7 @@ public function createSchema(): Schema foreach ($this->typeNamespaces as $typeNamespace) { $typeMappers[] = new GlobTypeMapper($typeNamespace, $typeGenerator, $inputTypeGenerator, $inputTypeUtils, - $this->container, $annotationReader, $namingStrategy, $lockFactory, $this->cache); + $this->container, $annotationReader, $namingStrategy, $lockFactory, $this->cache, $this->classNameMapper); } foreach ($this->typeMappers as $typeMapper) { @@ -229,7 +240,7 @@ public function createSchema(): Schema $queryProviders = []; foreach ($this->controllerNamespaces as $controllerNamespace) { $queryProviders[] = new GlobControllerQueryProvider($controllerNamespace, $fieldsBuilderFactory, $recursiveTypeMapper, - $this->container, $lockFactory, $this->cache); + $this->container, $lockFactory, $this->cache, $this->classNameMapper); } foreach ($this->queryProviders as $queryProvider) { diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php new file mode 100644 index 0000000000..2ff4503963 --- /dev/null +++ b/tests/Bootstrap.php @@ -0,0 +1,9 @@ +toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); } + public function testDefaultValueInSchema() + { + /** @var Schema $schema */ + $schema = $this->mainContainer->get(Schema::class); + + $queryString = ' + query deprecatedField { + __type(name: "Query") { + fields { + name + args { + name + defaultValue + } + } + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $queryString + ); + + $defaultField = null; + foreach ($result->data['__type']['fields'] as $field) { + if ($field['name'] === 'defaultValue') { + $defaultField = $field; + break; + } + } + + $this->assertSame( + $defaultField['args'][0]['defaultValue'], + '"value"' + ); + } + } diff --git a/tests/Mappers/GlobTypeMapperTest.php b/tests/Mappers/GlobTypeMapperTest.php index 95b27eaed8..422474ed8a 100644 --- a/tests/Mappers/GlobTypeMapperTest.php +++ b/tests/Mappers/GlobTypeMapperTest.php @@ -79,9 +79,19 @@ public function testGlobTypeMapperDuplicateInputTypesException() $mapper = new GlobTypeMapper('TheCodingMachine\GraphQLite\Fixtures\DuplicateInputTypes', $typeGenerator, $this->getInputTypeGenerator(), $this->getInputTypeUtils(), $container, new \TheCodingMachine\GraphQLite\AnnotationReader(new AnnotationReader()), new NamingStrategy(), $this->getLockFactory(), new NullCache()); - $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\''); - $mapper->canMapClassToInputType(TestObject::class); + $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(), + [ + '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\'' + ]); + $caught = true; + } + $this->assertTrue($caught, 'DuplicateMappingException is thrown'); } public function testGlobTypeMapperClassNotFoundException() diff --git a/tests/SchemaFactoryTest.php b/tests/SchemaFactoryTest.php index e9e3907d07..ec7460279f 100644 --- a/tests/SchemaFactoryTest.php +++ b/tests/SchemaFactoryTest.php @@ -5,11 +5,14 @@ use GraphQL\Error\Debug; use GraphQL\GraphQL; use GraphQL\Type\SchemaConfig; +use Mouf\Composer\ClassNameMapper; use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Simple\ArrayCache; use Symfony\Component\Cache\Simple\PhpFilesCache; use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Hydrators\FactoryHydrator; +use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper; use TheCodingMachine\GraphQLite\Security\VoidAuthenticationService; use TheCodingMachine\GraphQLite\Security\VoidAuthorizationService; @@ -57,10 +60,47 @@ public function testSetters(): void $this->doTestSchema($schema); } + public function testClassNameMapperInjectionWithValidMapper(): void + { + $factory = new SchemaFactory( + new ArrayCache(), + new BasicAutoWiringContainer( + new EmptyContainer() + ) + ); + $factory->setAuthenticationService(new VoidAuthenticationService()) + ->setAuthorizationService(new VoidAuthorizationService()) + ->setClassNameMapper(ClassNameMapper::createFromComposerFile(null, null, true)) + ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') + ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + + $schema = $factory->createSchema(); + + $this->doTestSchema($schema); + } + + public function testClassNameMapperInjectionWithInvalidMapper(): void + { + $factory = new SchemaFactory( + new ArrayCache(), + new BasicAutoWiringContainer( + new EmptyContainer() + ) + ); + $factory->setAuthenticationService(new VoidAuthenticationService()) + ->setAuthorizationService(new VoidAuthorizationService()) + ->setClassNameMapper(new ClassNameMapper()) + ->addControllerNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers') + ->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration'); + + $this->expectException(\TypeError::class); + $this->doTestSchema($factory->createSchema()); + } + public function testException(): void { $container = new BasicAutoWiringContainer(new EmptyContainer()); - $cache = new PhpFilesCache(); + $cache = new ArrayCache(); $factory = new SchemaFactory($cache, $container); @@ -71,7 +111,7 @@ public function testException(): void public function testException2(): void { $container = new BasicAutoWiringContainer(new EmptyContainer()); - $cache = new PhpFilesCache(); + $cache = new ArrayCache(); $factory = new SchemaFactory($cache, $container); $factory->addTypeNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration');