diff --git a/.gitignore b/.gitignore index 638ad6a42..9730d573c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea phpunit.xml /build /vendor diff --git a/README.md b/README.md index 241484d34..dbe8cd3ad 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Documentation - [Errors handling](docs/error-handling/index.md) - [Events](docs/events/index.md) - [Profiler](docs/profiler/index.md) +- [Tune configuration](docs/tune_configuration.md) Talks and slides to help you start ---------------------------------- diff --git a/UPGRADE.md b/UPGRADE.md index 6e593b735..a79da4ee0 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -56,7 +56,7 @@ public function __construct( ) ``` `TypeBuilder` here is a new service `Overblog\GraphQLBundle\Generator\TypeBuilder`, which is also used internally. -The rest of the arguments were moved into the separate class `Overblog\GraphQLBundle\Generator\TypeGeneratorOptions` +The rest of the arguments were moved into the separate class `Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions` with the following constructor signature: ```php diff --git a/composer.json b/composer.json index 68fb15164..28a3ece10 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ } }, "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, "bin-dir": "bin", "sort-packages": true }, diff --git a/docs/tune_configuration.md b/docs/tune_configuration.md new file mode 100644 index 000000000..954ffad9b --- /dev/null +++ b/docs/tune_configuration.md @@ -0,0 +1,22 @@ +Tune configuration +================== + +Custom GraphQl configuration parsers +------------------------------------ + +You can configure custom GraphQl configuration parsers. +Your parsers MUST implement at least `\Overblog\GraphQLBundle\Config\Parser\ParserInterface` +and optionally `\Overblog\GraphQLBundle\Config\Parser\PreParserInterface` when required. + +Default values will be applied when omitted. + +```yaml +overblog_graphql: + # ... + parsers: + yaml: 'Overblog\GraphQLBundle\Config\Parser\YamlParser' + graphql: 'Overblog\GraphQLBundle\Config\Parser\GraphQLParser' + annotation: 'Overblog\GraphQLBundle\Config\Parser\AnnotationParser' + attribute: 'Overblog\GraphQLBundle\Config\Parser\AttributeParser' + # ... +``` diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8d2b019cb..d366642bf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Abstract class Overblog\\\\GraphQLBundle\\\\Annotation\\\\Builder cannot be an Attribute class\\.$#" - count: 1 - path: src/Annotation/Builder.php - - message: "#^Access to an undefined property GraphQL\\\\Language\\\\AST\\\\Node\\:\\:\\$description\\.$#" count: 1 @@ -20,6 +15,11 @@ parameters: count: 1 path: src/Config/Parser/GraphQL/ASTConverter/EnumNode.php + - + message: "#^Method Overblog\\\\GraphQLBundle\\\\Config\\\\Parser\\\\GraphQL\\\\ASTConverter\\\\FieldsNode\\:\\:toConfig\\(\\) should return array\\ but returns array\\\\>\\.$#" + count: 1 + path: src/Config/Parser/GraphQL/ASTConverter/FieldsNode.php + - message: "#^Parameter \\#1 \\$node of static method Overblog\\\\GraphQLBundle\\\\Config\\\\Parser\\\\GraphQL\\\\ASTConverter\\\\FieldsNode\\:\\:toConfig\\(\\) expects GraphQL\\\\Language\\\\AST\\\\Node, object given\\.$#" count: 1 @@ -50,11 +50,6 @@ parameters: count: 1 path: src/Config/Parser/GraphQLParser.php - - - message: "#^Dead catch \\- Overblog\\\\GraphQLBundle\\\\Config\\\\Parser\\\\MetadataParser\\\\TypeGuesser\\\\TypeGuessingException is never thrown in the try block\\.$#" - count: 3 - path: src/Config/Parser/MetadataParser/MetadataParser.php - - message: "#^Parameter \\#1 \\$filename of function file_get_contents expects string, string\\|false given\\.$#" count: 1 @@ -80,110 +75,50 @@ parameters: count: 1 path: src/Config/Parser/YamlParser.php - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Config\\\\Processor\\\\BuilderProcessor\\:\\:getBuilder\\(\\) should return Overblog\\\\GraphQLBundle\\\\Definition\\\\Builder\\\\MappingInterface but returns object\\.$#" - count: 1 - path: src/Config/Processor/BuilderProcessor.php - - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Definition\\\\Builder\\\\TypeFactory\\:\\:create\\(\\) should return GraphQL\\\\Type\\\\Definition\\\\Type but returns object\\.$#" - count: 1 - path: src/Definition/Builder/TypeFactory.php - - - - message: "#^PHPDoc tag @var for constant Overblog\\\\GraphQLBundle\\\\DependencyInjection\\\\Compiler\\\\ConfigParserPass\\:\\:PARSERS with type array\\\\> is not subtype of value array\\{yaml\\: 'Overblog…', graphql\\: 'Overblog…', annotation\\: 'Overblog…', attribute\\: 'Overblog…'\\}\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ConfigParserPass.php - - message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#" count: 1 path: src/DependencyInjection/Compiler/ConfigParserPass.php - - message: "#^Property Overblog\\\\GraphQLBundle\\\\DependencyInjection\\\\Compiler\\\\ConfigParserPass\\:\\:\\$preTreatedFiles is never read, only written\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ConfigParserPass.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\DependencyInjection\\\\Compiler\\\\ConfigParserPass\\:\\:\\$treatedFiles is never read, only written\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ConfigParserPass.php - - - - message: "#^Trying to invoke array\\{'Overblog…'\\|'Overblog…'\\|'Overblog…'\\|'Overblog…', 'parse'\\|'preParse'\\} but it might not be a callable\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ConfigParserPass.php - - - - message: "#^Parameter \\#1 \\$id of class Symfony\\\\Component\\\\DependencyInjection\\\\Reference constructor expects string, string\\|Symfony\\\\Component\\\\DependencyInjection\\\\Reference given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php - - - - message: "#^Parameter \\#1 \\$keys of function array_fill_keys expects array, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php - - - - message: "#^Parameter \\#1 \\$namespace of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGeneratorOptions constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php - - - - message: "#^Parameter \\#1 \\$typeConfigs of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGenerator constructor expects array, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php - - - - message: "#^Parameter \\#2 \\$cacheDir of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGeneratorOptions constructor expects string\\|null, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php - - - - message: "#^Parameter \\#2 \\$namespace of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeBuilder constructor expects string, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php - - - - message: "#^Parameter \\#3 \\$useClassMap of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGeneratorOptions constructor expects bool, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" + message: "#^Result of \\|\\| is always false\\.$#" count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php + path: src/Error/UserErrors.php - - message: "#^Parameter \\#4 \\$cacheBaseDir of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGeneratorOptions constructor expects string\\|null, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" + message: "#^Call to function array_key_exists\\(\\) with 'defaultValue' and Overblog\\\\GraphQLBundle\\\\Generator\\\\Model\\\\ArgumentConfig will always evaluate to false\\.$#" count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Parameter \\#5 \\$cacheDirMask of class Overblog\\\\GraphQLBundle\\\\Generator\\\\TypeGeneratorOptions constructor expects int\\|null, array\\|bool\\|float\\|int\\|string\\|null given\\.$#" + message: "#^Call to function array_key_exists\\(\\) with 'defaultValue' and Overblog\\\\GraphQLBundle\\\\Generator\\\\Model\\\\FieldConfig will always evaluate to false\\.$#" count: 1 - path: src/DependencyInjection/Compiler/TypeGeneratorPass.php + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Error\\\\ExceptionConverter\\:\\:convertException\\(\\) should return Throwable but returns object\\.$#" - count: 1 - path: src/Error/ExceptionConverter.php + message: "#^Parameter \\#1 \\$config of method Overblog\\\\GraphQLBundle\\\\Generator\\\\ValidationRulesBuilder\\:\\:build\\(\\) expects array\\('constraints' \\=\\> array, 'link' \\=\\> string, 'cascade' \\=\\> array\\), array given\\.$#" + count: 2 + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Array \\(array\\\\) does not accept GraphQL\\\\Error\\\\UserError\\.$#" + message: "#^Parameter \\#1 \\$config of method Overblog\\\\GraphQLBundle\\\\Generator\\\\ValidationRulesBuilder\\:\\:build\\(\\) expects array\\('constraints' \\=\\> array, 'link' \\=\\> string, 'cascade' \\=\\> array\\), array&nonEmpty given\\.$#" count: 1 - path: src/Error/UserErrors.php + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Result of \\|\\| is always false\\.$#" + message: "#^Parameter \\#2 \\$search of function array_key_exists expects array, Overblog\\\\GraphQLBundle\\\\Generator\\\\Model\\\\ArgumentConfig given\\.$#" count: 1 - path: src/Error/UserErrors.php + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Cannot use array destructuring on callable\\.$#" + message: "#^Parameter \\#2 \\$search of function array_key_exists expects array, Overblog\\\\GraphQLBundle\\\\Generator\\\\Model\\\\FieldConfig given\\.$#" count: 1 - path: src/Generator/TypeBuilder.php + path: src/Generator/ConfigBuilder/FieldsBuilder.php - - message: "#^Offset 'validationGroups' on array\\{type\\: string, resolve\\?\\: string, description\\?\\: string, args\\?\\: array, complexity\\?\\: string, deprecatedReason\\?\\: string, validation\\?\\: array\\} on left side of \\?\\? does not exist\\.$#" + message: "#^Parameter \\#1 \\$config of method Overblog\\\\GraphQLBundle\\\\Generator\\\\ValidationRulesBuilder\\:\\:build\\(\\) expects array\\('constraints' \\=\\> array, 'link' \\=\\> string, 'cascade' \\=\\> array\\), array given\\.$#" count: 1 - path: src/Generator/TypeBuilder.php + path: src/Generator/ConfigBuilder/ValidationBuilder.php - message: "#^Strict comparison using \\=\\=\\= between null and Composer\\\\Autoload\\\\ClassLoader will always evaluate to false\\.$#" @@ -210,71 +145,21 @@ parameters: count: 1 path: src/Relay/Mutation/PayloadDefinition.php - - - message: "#^Unsafe access to private constant Overblog\\\\GraphQLBundle\\\\Resolver\\\\FieldResolver\\:\\:PREFIXES through static\\:\\:\\.$#" - count: 1 - path: src/Resolver/FieldResolver.php - - message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#" count: 1 path: src/Resolver/TypeResolver.php - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Validator\\\\Constraints\\\\ExpressionValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Validator/Constraints/ExpressionValidator.php - - - - message: "#^Array \\(array\\\\) does not accept Symfony\\\\Component\\\\Validator\\\\Mapping\\\\MetadataInterface\\.$#" - count: 1 - path: src/Validator/InputValidator.php - - - - message: "#^Offset 'validation' on array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\OutputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\OutputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), resolve\\?\\: \\(callable\\(mixed, array\\, mixed, GraphQL\\\\Type\\\\Definition\\\\ResolveInfo\\)\\: mixed\\)\\|null, args\\?\\: array\\\\|null, description\\?\\: string\\|null, deprecationReason\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\FieldDefinitionNode\\|null, complexity\\?\\: \\(callable\\(int, array\\\\)\\: int\\)\\|null\\} on left side of \\?\\? does not exist\\.$#" - count: 1 - path: src/Validator/InputValidator.php - - - - message: "#^Unsafe call to private method Overblog\\\\GraphQLBundle\\\\Validator\\\\InputValidator\\:\\:isListOfType\\(\\) through static\\:\\:\\.$#" - count: 1 - path: src/Validator/InputValidator.php - - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\MetadataFactory\\:\\:getMetadataFor\\(\\) has parameter \\$object with no type specified\\.$#" - count: 1 - path: src/Validator/Mapping/MetadataFactory.php - - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\MetadataFactory\\:\\:hasMetadataFor\\(\\) has parameter \\$object with no type specified\\.$#" - count: 1 - path: src/Validator/Mapping/MetadataFactory.php - - message: "#^Array \\(array\\\\) does not accept Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\PropertyMetadata\\.$#" count: 1 path: src/Validator/Mapping/ObjectMetadata.php - - - message: "#^PHPDoc tag @return with type Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\ObjectMetadata is not subtype of native type static\\(Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\ObjectMetadata\\)\\.$#" - count: 1 - path: src/Validator/Mapping/ObjectMetadata.php - - message: "#^Call to an undefined method ReflectionMethod\\|ReflectionProperty\\:\\:getValue\\(\\)\\.$#" count: 1 path: src/Validator/Mapping/PropertyMetadata.php - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Validator\\\\Mapping\\\\PropertyMetadata\\:\\:getPropertyValue\\(\\) has parameter \\$object with no type specified\\.$#" - count: 1 - path: src/Validator/Mapping/PropertyMetadata.php - - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Invalid\\\\InvalidPrivateMethod\\:\\:gql\\(\\) is unused\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Invalid/InvalidPrivateMethod.php - - message: "#^Access to an undefined property GraphQL\\\\Language\\\\AST\\\\Node\\:\\:\\$value\\.$#" count: 1 @@ -291,79 +176,9 @@ parameters: path: tests/Config/Parser/fixtures/annotations/Scalar/GalaxyCoordinates.php - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Animal\\:\\:\\$lives is unused\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Animal.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Animal\\:\\:\\$name is unused\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Animal.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$battles has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$bool has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$boolean has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$color has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$creator has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$crystal has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$currentHolder has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$decimal has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$float has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$holders has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$size has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$string has no typehint specified\\.$#" + message: "#^Parameter \\#1 \\$errors of class Overblog\\\\GraphQLBundle\\\\Error\\\\UserErrors constructor expects array\\, array\\ given\\.$#" count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Config\\\\Parser\\\\fixtures\\\\annotations\\\\Type\\\\Lightsaber\\:\\:\\$text has no typehint specified\\.$#" - count: 1 - path: tests/Config/Parser/fixtures/annotations/Type/Lightsaber.php + path: tests/Error/ErrorHandlerTest.php - message: "#^Parameter \\#1 \\$exceptionMap of class Overblog\\\\GraphQLBundle\\\\Error\\\\ExceptionConverter constructor expects array\\, array\\\\> given\\.$#" @@ -380,11 +195,6 @@ parameters: count: 4 path: tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php - - - message: "#^Instantiated class Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\User not found\\.$#" - count: 1 - path: tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php - - message: "#^Call to method disableOriginalConstructor\\(\\) on an unknown class PHPUnit_Framework_MockObject_MockBuilder\\.$#" count: 1 @@ -395,11 +205,6 @@ parameters: count: 1 path: tests/ExpressionLanguage/TestCase.php - - - message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{mixed, 'with'\\} given\\.$#" - count: 1 - path: tests/ExpressionLanguage/TestCase.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\KernelInterface\\:\\:isBooted\\(\\)\\.$#" count: 1 @@ -440,16 +245,6 @@ parameters: count: 5 path: tests/Functional/Controller/GraphControllerTest.php - - - message: "#^Method Overblog\\\\GraphQLBundle\\\\Tests\\\\Functional\\\\TestCase\\:\\:createKernel\\(\\) should return Symfony\\\\Component\\\\HttpKernel\\\\KernelInterface but returns object\\.$#" - count: 1 - path: tests/Functional/TestCase.php - - - - message: "#^Missing call to parent\\:\\:tearDown\\(\\) method\\.$#" - count: 1 - path: tests/Functional/TestCase.php - - message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#" count: 1 @@ -465,21 +260,6 @@ parameters: count: 1 path: tests/Functional/Upload/UploadTest.php - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Functional\\\\Validator\\\\DummyEntity\\:\\:\\$string1 is never written, only read\\.$#" - count: 1 - path: tests/Functional/Validator/DummyEntity.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Functional\\\\Validator\\\\DummyEntity\\:\\:\\$string2 is never written, only read\\.$#" - count: 1 - path: tests/Functional/Validator/DummyEntity.php - - - - message: "#^Property Overblog\\\\GraphQLBundle\\\\Tests\\\\Functional\\\\Validator\\\\DummyEntity\\:\\:\\$string3 is never written, only read\\.$#" - count: 1 - path: tests/Functional/Validator/DummyEntity.php - - message: "#^Cannot access offset mixed on iterable\\\\.$#" count: 2 diff --git a/src/Config/CustomScalarTypeDefinition.php b/src/Config/CustomScalarTypeDefinition.php index d622e7229..4f1d148c3 100644 --- a/src/Config/CustomScalarTypeDefinition.php +++ b/src/Config/CustomScalarTypeDefinition.php @@ -8,10 +8,17 @@ class CustomScalarTypeDefinition extends TypeDefinition { + public const CONFIG_NAME = '_custom_scalar_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ - $node = self::createNode('_custom_scalar_config'); + $node = self::createNode(static::CONFIG_NAME); /** @phpstan-ignore-next-line */ $node diff --git a/src/Config/EnumTypeDefinition.php b/src/Config/EnumTypeDefinition.php index 5d39af79b..def5aa9eb 100644 --- a/src/Config/EnumTypeDefinition.php +++ b/src/Config/EnumTypeDefinition.php @@ -11,10 +11,17 @@ class EnumTypeDefinition extends TypeDefinition { + public const CONFIG_NAME = '_enum_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ - $node = self::createNode('_enum_config'); + $node = self::createNode(static::CONFIG_NAME); /** @phpstan-ignore-next-line */ $node diff --git a/src/Config/InputObjectTypeDefinition.php b/src/Config/InputObjectTypeDefinition.php index 8291a314e..0ce049a04 100644 --- a/src/Config/InputObjectTypeDefinition.php +++ b/src/Config/InputObjectTypeDefinition.php @@ -9,10 +9,17 @@ class InputObjectTypeDefinition extends TypeDefinition { + public const CONFIG_NAME = '_input_object_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ - $node = self::createNode('_input_object_config'); + $node = self::createNode(static::CONFIG_NAME); /** @phpstan-ignore-next-line */ $node diff --git a/src/Config/InterfaceTypeDefinition.php b/src/Config/InterfaceTypeDefinition.php index e24e59c49..31e12748e 100644 --- a/src/Config/InterfaceTypeDefinition.php +++ b/src/Config/InterfaceTypeDefinition.php @@ -8,10 +8,17 @@ class InterfaceTypeDefinition extends TypeWithOutputFieldsDefinition { + public const CONFIG_NAME = '_interface_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ - $node = self::createNode('_interface_config'); + $node = self::createNode(static::CONFIG_NAME); /** @phpstan-ignore-next-line */ $node diff --git a/src/Config/ObjectTypeDefinition.php b/src/Config/ObjectTypeDefinition.php index 8444b21ea..836dfc3af 100644 --- a/src/Config/ObjectTypeDefinition.php +++ b/src/Config/ObjectTypeDefinition.php @@ -10,9 +10,16 @@ class ObjectTypeDefinition extends TypeWithOutputFieldsDefinition { + public const CONFIG_NAME = '_object_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { - $builder = new TreeBuilder('_object_config', 'array'); + $builder = new TreeBuilder(static::CONFIG_NAME, 'array'); /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); @@ -20,7 +27,7 @@ public function getDefinition(): ArrayNodeDefinition /** @phpstan-ignore-next-line */ $node ->children() - ->append($this->validationSection(self::VALIDATION_LEVEL_CLASS)) + ->append($this->validationSection(static::VALIDATION_LEVEL_CLASS)) ->append($this->nameSection()) ->append($this->outputFieldsSection()) ->append($this->fieldsBuilderSection()) diff --git a/src/Config/Parser/GraphQL/ASTConverter/CustomScalarNode.php b/src/Config/Parser/GraphQL/ASTConverter/CustomScalarNode.php index f9f9fc5ba..6638928a5 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/CustomScalarNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/CustomScalarNode.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; use GraphQL\Language\AST\Node; +use Overblog\GraphQLBundle\Enum\TypeEnum; use RuntimeException; class CustomScalarNode implements NodeInterface @@ -19,7 +20,7 @@ public static function toConfig(Node $node): array ]; return [ - 'type' => 'custom-scalar', + 'type' => TypeEnum::CUSTOM_SCALAR, 'config' => $config, ]; } diff --git a/src/Config/Parser/GraphQL/ASTConverter/DescriptionNode.php b/src/Config/Parser/GraphQL/ASTConverter/DescriptionNode.php index f2affb24d..449e50760 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/DescriptionNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/DescriptionNode.php @@ -6,12 +6,17 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\AST\TypeExtensionNode; use function trim; class DescriptionNode implements NodeInterface { public static function toConfig(Node $node): array { + if ($node instanceof TypeExtensionNode) { + return []; + } + return ['description' => self::cleanAstDescription($node->description)]; } @@ -21,8 +26,6 @@ private static function cleanAstDescription(?StringValueNode $description): ?str return null; } - $description = trim($description->value); - - return empty($description) ? null : $description; + return trim($description->value) ?: null; } } diff --git a/src/Config/Parser/GraphQL/ASTConverter/EnumNode.php b/src/Config/Parser/GraphQL/ASTConverter/EnumNode.php index d947f8fcf..353491959 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/EnumNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/EnumNode.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; use GraphQL\Language\AST\Node; +use Overblog\GraphQLBundle\Enum\TypeEnum; class EnumNode implements NodeInterface { @@ -28,7 +29,7 @@ public static function toConfig(Node $node): array $config['values'] = $values; return [ - 'type' => 'enum', + 'type' => TypeEnum::ENUM, 'config' => $config, ]; } diff --git a/src/Config/Parser/GraphQL/ASTConverter/FieldsNode.php b/src/Config/Parser/GraphQL/ASTConverter/FieldsNode.php index 8136a0c71..9b3805670 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/FieldsNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/FieldsNode.php @@ -4,6 +4,8 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; +use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\Node; use GraphQL\Utils\AST; @@ -17,7 +19,7 @@ public static function toConfig(Node $node, string $property = 'fields'): array $fieldConfig = TypeNode::toConfig($definition) + DescriptionNode::toConfig($definition); if (!empty($definition->arguments)) { - $fieldConfig['args'] = self::toConfig($definition, 'arguments'); + $fieldConfig['args'] = static::toConfig($definition, 'arguments'); } if (!empty($definition->defaultValue)) { @@ -29,10 +31,21 @@ public static function toConfig(Node $node, string $property = 'fields'): array $fieldConfig['deprecationReason'] = $directiveConfig['deprecationReason']; } - $config[$definition->name->value] = $fieldConfig; + $config[$definition->name->value] = static::extendFieldConfig($fieldConfig, $definition); } } return $config; } + + /** + * @param array $fieldConfig + * @param FieldDefinitionNode|InputValueDefinitionNode $fieldDefinition + * + * @return array + */ + protected static function extendFieldConfig(array $fieldConfig, Node $fieldDefinition): array + { + return $fieldConfig; + } } diff --git a/src/Config/Parser/GraphQL/ASTConverter/InputObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/InputObjectNode.php index 31c4cb462..8d3396c1c 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/InputObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/InputObjectNode.php @@ -4,7 +4,9 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; +use Overblog\GraphQLBundle\Enum\TypeEnum; + class InputObjectNode extends ObjectNode { - protected const TYPENAME = 'input-object'; + protected const TYPENAME = TypeEnum::INPUT_OBJECT; } diff --git a/src/Config/Parser/GraphQL/ASTConverter/InterfaceNode.php b/src/Config/Parser/GraphQL/ASTConverter/InterfaceNode.php index dfd63f8d8..651a314fc 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/InterfaceNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/InterfaceNode.php @@ -4,7 +4,9 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; +use Overblog\GraphQLBundle\Enum\TypeEnum; + class InterfaceNode extends ObjectNode { - protected const TYPENAME = 'interface'; + protected const TYPENAME = TypeEnum::INTERFACE; } diff --git a/src/Config/Parser/GraphQL/ASTConverter/NodeInterface.php b/src/Config/Parser/GraphQL/ASTConverter/NodeInterface.php index d9d7561d4..f0abc4884 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/NodeInterface.php +++ b/src/Config/Parser/GraphQL/ASTConverter/NodeInterface.php @@ -8,5 +8,8 @@ interface NodeInterface { + /** + * @return array + */ public static function toConfig(Node $node): array; } diff --git a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index cb3bfcc91..1e010ef5c 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -5,18 +5,56 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use Overblog\GraphQLBundle\Enum\TypeEnum; class ObjectNode implements NodeInterface { - protected const TYPENAME = 'object'; + protected const TYPENAME = TypeEnum::OBJECT; + /** + * @param ObjectTypeDefinitionNode $node + * + * @return array + */ public static function toConfig(Node $node): array { - $config = DescriptionNode::toConfig($node) + [ + return [ + 'type' => static::TYPENAME, + 'config' => static::parseConfig($node), + ]; + } + + /** + * @param ObjectTypeDefinitionNode $node + * + * @return array + */ + protected static function parseConfig(Node $node): array + { + $config = DescriptionNode::toConfig($node) + static::parseFields($node); + $config += static::parseInterfaces($node); + + return $config; + } + + /** + * @return array{fields: array } + */ + protected static function parseFields(Node $node): array + { + return [ 'fields' => FieldsNode::toConfig($node), ]; + } - if (!empty($node->interfaces)) { + /** + * @return array> + */ + protected static function parseInterfaces(Node $node): array + { + $config = []; + if (isset($node->interfaces) && !empty($node->interfaces)) { $interfaces = []; foreach ($node->interfaces as $interface) { $interfaces[] = TypeNode::astTypeNodeToString($interface); @@ -24,9 +62,6 @@ public static function toConfig(Node $node): array $config['interfaces'] = $interfaces; } - return [ - 'type' => static::TYPENAME, - 'config' => $config, - ]; + return $config; } } diff --git a/src/Config/Parser/GraphQL/ASTConverter/UnionNode.php b/src/Config/Parser/GraphQL/ASTConverter/UnionNode.php index 050eb2ea7..deba382f5 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/UnionNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/UnionNode.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; use GraphQL\Language\AST\Node; +use Overblog\GraphQLBundle\Enum\TypeEnum; class UnionNode implements NodeInterface { @@ -21,7 +22,7 @@ public static function toConfig(Node $node): array } return [ - 'type' => 'union', + 'type' => TypeEnum::UNION, 'config' => $config, ]; } diff --git a/src/Config/Parser/GraphQLParser.php b/src/Config/Parser/GraphQLParser.php index bf9414151..b3a389e13 100644 --- a/src/Config/Parser/GraphQLParser.php +++ b/src/Config/Parser/GraphQLParser.php @@ -6,81 +6,131 @@ use Exception; use GraphQL\Language\AST\DefinitionNode; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\EnumTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeDefinitionNode; +use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\Parser; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\CustomScalarNode; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\EnumNode; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\InputObjectNode; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\InterfaceNode; use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\NodeInterface; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\ObjectNode; +use Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter\UnionNode; use SplFileInfo; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use function array_keys; + use function array_pop; -use function call_user_func; use function explode; use function file_get_contents; -use function get_class; -use function in_array; use function preg_replace; use function sprintf; use function trim; -use function ucfirst; class GraphQLParser implements ParserInterface { - private const DEFINITION_TYPE_MAPPING = [ - NodeKind::OBJECT_TYPE_DEFINITION => 'object', - NodeKind::INTERFACE_TYPE_DEFINITION => 'interface', - NodeKind::ENUM_TYPE_DEFINITION => 'enum', - NodeKind::UNION_TYPE_DEFINITION => 'union', - NodeKind::INPUT_OBJECT_TYPE_DEFINITION => 'inputObject', - NodeKind::SCALAR_TYPE_DEFINITION => 'customScalar', + protected const DEFINITION_TYPE_MAPPING = [ + NodeKind::OBJECT_TYPE_DEFINITION => ObjectNode::class, + NodeKind::INTERFACE_TYPE_DEFINITION => InterfaceNode::class, + NodeKind::ENUM_TYPE_DEFINITION => EnumNode::class, + NodeKind::UNION_TYPE_DEFINITION => UnionNode::class, + NodeKind::INPUT_OBJECT_TYPE_DEFINITION => InputObjectNode::class, + NodeKind::SCALAR_TYPE_DEFINITION => CustomScalarNode::class, ]; public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array { $container->addResource(new FileResource($file->getRealPath())); - $content = trim((string) file_get_contents($file->getPathname())); - $typesConfig = []; - // allow empty files - if (empty($content)) { - return []; - } - try { - $ast = Parser::parse($content); - } catch (Exception $e) { - throw new InvalidArgumentException(sprintf('An error occurred while parsing the file "%s".', $file), $e->getCode(), $e); - } + return static::buildTypes(static::parseFile($file)); + } + + /** + * @return array + */ + protected static function buildTypes(DocumentNode $ast): array + { + $nameGeneratorBag = []; + $typesConfig = []; foreach ($ast->definitions as $typeDef) { /** - * @var ObjectTypeDefinitionNode|InputObjectTypeDefinitionNode|EnumTypeDefinitionNode $typeDef + * @var ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|UnionTypeDefinitionNode|InputObjectTypeDefinitionNode|ScalarTypeDefinitionNode $typeDef */ - if (isset($typeDef->kind) && in_array($typeDef->kind, array_keys(self::DEFINITION_TYPE_MAPPING))) { - /** - * @var class-string $class - */ - $class = sprintf('\\%s\\GraphQL\\ASTConverter\\%sNode', __NAMESPACE__, ucfirst(self::DEFINITION_TYPE_MAPPING[$typeDef->kind])); - $typesConfig[$typeDef->name->value] = call_user_func([$class, 'toConfig'], $typeDef); - } else { - self::throwUnsupportedDefinitionNode($typeDef); - } + $config = static::prepareConfig($typeDef); + $typesConfig[static::getTypeDefinitionName($typeDef, $nameGeneratorBag)] = $config; } return $typesConfig; } - private static function throwUnsupportedDefinitionNode(DefinitionNode $typeDef): void + protected static function createUnsupportedDefinitionNodeException(DefinitionNode $typeDef): InvalidArgumentException { - $path = explode('\\', get_class($typeDef)); - throw new InvalidArgumentException( + $path = explode('\\', \get_class($typeDef)); + + return new InvalidArgumentException( sprintf( '%s definition is not supported right now.', preg_replace('@DefinitionNode$@', '', array_pop($path)) ) ); } + + /** + * @return class-string + */ + protected static function getNodeClass(DefinitionNode $typeDef): string + { + if (isset($typeDef->kind) && \array_key_exists($typeDef->kind, static::DEFINITION_TYPE_MAPPING)) { + return static::DEFINITION_TYPE_MAPPING[$typeDef->kind]; + } + + throw static::createUnsupportedDefinitionNodeException($typeDef); + } + + /** + * @param array $nameGeneratorBag + */ + protected static function getTypeDefinitionName(DefinitionNode $typeDef, array &$nameGeneratorBag): string + { + if (isset($typeDef->name)) { + return $typeDef->name->value; + } + + throw static::createUnsupportedDefinitionNodeException($typeDef); + } + + /** + * @return array + */ + protected static function prepareConfig(DefinitionNode $typeDef): array + { + $nodeClass = static::getNodeClass($typeDef); + + return \call_user_func([$nodeClass, 'toConfig'], $typeDef); + } + + protected static function parseFile(SplFileInfo $file): DocumentNode + { + $content = trim((string) file_get_contents($file->getPathname())); + + // allow empty files + if (empty($content)) { + return new DocumentNode(['definitions' => new NodeList([])]); + } + + try { + return Parser::parse($content); + } catch (Exception $e) { + throw new InvalidArgumentException(sprintf('An error occurred while parsing the file "%s".', $file), $e->getCode(), $e); + } + } } diff --git a/src/Config/Parser/MetadataParser/MetadataParser.php b/src/Config/Parser/MetadataParser/MetadataParser.php index a3d9007d8..0d246b933 100644 --- a/src/Config/Parser/MetadataParser/MetadataParser.php +++ b/src/Config/Parser/MetadataParser/MetadataParser.php @@ -12,6 +12,7 @@ use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeHintTypeGuesser; use Overblog\GraphQLBundle\Config\Parser\PreParserInterface; +use Overblog\GraphQLBundle\Enum\TypeEnum; use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface; use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface; use ReflectionClass; @@ -169,7 +170,7 @@ private static function classMetadatasToGQLConfiguration( if (!$edgeType) { $edgeType = $gqlName.'Edge'; $gqlTypes[$edgeType] = [ - 'type' => 'object', + 'type' => TypeEnum::OBJECT, 'config' => [ 'builders' => [ ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classMetadata->node]], @@ -397,7 +398,7 @@ private static function inputMetadataToGQLConfiguration(ReflectionClass $reflect 'fields' => self::getGraphQLInputFieldsFromMetadatas($reflectionClass, self::getClassProperties($reflectionClass)), ], self::getDescriptionConfiguration(static::getMetadatas($reflectionClass))); - return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration]; + return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : TypeEnum::INPUT_OBJECT, 'config' => $inputConfiguration]; } /** @@ -421,7 +422,7 @@ private static function scalarMetadataToGQLConfiguration(ReflectionClass $reflec $scalarConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $scalarConfiguration; - return ['type' => 'custom-scalar', 'config' => $scalarConfiguration]; + return ['type' => TypeEnum::CUSTOM_SCALAR, 'config' => $scalarConfiguration]; } /** @@ -459,7 +460,7 @@ private static function enumMetadataToGQLConfiguration(ReflectionClass $reflecti $enumConfiguration = ['values' => $values]; $enumConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $enumConfiguration; - return ['type' => 'enum', 'config' => $enumConfiguration]; + return ['type' => TypeEnum::ENUM, 'config' => $enumConfiguration]; } /** @@ -504,7 +505,7 @@ private static function unionMetadataToGQLConfiguration(ReflectionClass $reflect } } - return ['type' => 'union', 'config' => $unionConfiguration]; + return ['type' => TypeEnum::UNION, 'config' => $unionConfiguration]; } /** diff --git a/src/Config/Parser/ParserInterface.php b/src/Config/Parser/ParserInterface.php index 5f08e9d7d..9908c6e7c 100644 --- a/src/Config/Parser/ParserInterface.php +++ b/src/Config/Parser/ParserInterface.php @@ -9,5 +9,10 @@ interface ParserInterface { + /** + * @param array $configs + * + * @return array + */ public static function parse(SplFileInfo $file, ContainerBuilder $container, array $configs = []): array; } diff --git a/src/Config/PermittedInheritTypeProvider.php b/src/Config/PermittedInheritTypeProvider.php new file mode 100644 index 000000000..7c5b4d10e --- /dev/null +++ b/src/Config/PermittedInheritTypeProvider.php @@ -0,0 +1,34 @@ +getExtraTypes($type)]; + } + + /** + * @return string[] + */ + protected function getExtraTypes(string $type): array + { + $allowedTypes = []; + if (TypeEnum::OBJECT === $type) { + $allowedTypes[] = TypeEnum::INTERFACE; + } + + return $allowedTypes; + } +} diff --git a/src/Config/Processor/InheritanceProcessor.php b/src/Config/Processor/InheritanceProcessor.php index 634162e87..05505c3c0 100644 --- a/src/Config/Processor/InheritanceProcessor.php +++ b/src/Config/Processor/InheritanceProcessor.php @@ -6,6 +6,8 @@ use Exception; use InvalidArgumentException; +use Overblog\GraphQLBundle\Config\PermittedInheritTypeProvider; +use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_column; use function array_filter; use function array_flip; @@ -28,6 +30,24 @@ final class InheritanceProcessor implements ProcessorInterface public const HEIRS_KEY = 'heirs'; public const INHERITS_KEY = 'inherits'; + /** + * TODO: refactor. This is dirty solution but quick and with minimal impact on existing structure. + * + * @var class-string + */ + private static $permittedInheritTypeProviderClass = PermittedInheritTypeProvider::class; + + /** + * @param class-string $fqcn + */ + public static function setPermittedInheritTypeProviderClass(string $fqcn): void + { + if (!is_subclass_of($fqcn, PermittedInheritTypeProvider::class, true)) { + throw new \InvalidArgumentException(sprintf('Options must be a FQCN implementing %s', PermittedInheritTypeProvider::class)); + } + self::$permittedInheritTypeProviderClass = $fqcn; + } + public static function process(array $configs): array { $configs = self::processConfigsHeirs($configs); @@ -79,10 +99,7 @@ private static function processConfigsInherits(array $configs): array continue; } - $allowedTypes = [$config['type']]; - if ('object' === $config['type']) { - $allowedTypes[] = 'interface'; - } + $allowedTypes = self::getAllowedTypes($config['type']); $flattenInherits = self::flattenInherits($name, $configs, $allowedTypes); if (empty($flattenInherits)) { continue; @@ -106,7 +123,7 @@ private static function inheritsTypeConfig(string $child, array $parents, array $mergedParentsConfig = self::mergeConfigs(...array_column($parentTypes, 'config')); $childType = $configs[$child]; // unset resolveType field resulting from the merge of a "interface" type - if ('object' === $childType['type']) { + if (TypeEnum::OBJECT === $childType['type']) { unset($mergedParentsConfig['resolveType']); } @@ -197,4 +214,15 @@ private static function mergeConfigs(array ...$configs): array return $result; } + + /** + * @return string[] + */ + private static function getAllowedTypes(string $type): array + { + /** @var class-string $class */ + $class = self::$permittedInheritTypeProviderClass; + + return (new $class)->getAllowedTypes($type); + } } diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index f02588750..766415676 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -32,6 +32,8 @@ public static function create(): self return new static(); } + abstract public static function getName(): string; + protected function resolveTypeSection(): VariableNodeDefinition { return self::createNode('resolveType', 'variable'); diff --git a/src/Config/TypeWithOutputFieldsDefinition.php b/src/Config/TypeWithOutputFieldsDefinition.php index 438f8da3f..5fa7abbbc 100644 --- a/src/Config/TypeWithOutputFieldsDefinition.php +++ b/src/Config/TypeWithOutputFieldsDefinition.php @@ -17,6 +17,7 @@ protected function outputFieldsSection(): NodeDefinition $node->isRequired()->requiresAtLeastOneElement(); + /** @var ArrayNodeDefinition $prototype */ $prototype = $node->useAttributeAsKey('name', false)->prototype('array'); /** @phpstan-ignore-next-line */ @@ -84,6 +85,8 @@ protected function outputFieldsSection(): NodeDefinition ->end() ->end(); + $this->extendFieldPrototype($prototype); + return $node; } @@ -101,4 +104,9 @@ protected function fieldsBuilderSection(): ArrayNodeDefinition return $node; } + + protected function extendFieldPrototype(ArrayNodeDefinition $prototype): void + { + + } } diff --git a/src/Config/UnionTypeDefinition.php b/src/Config/UnionTypeDefinition.php index 7c9e2cf9b..4297bed8e 100644 --- a/src/Config/UnionTypeDefinition.php +++ b/src/Config/UnionTypeDefinition.php @@ -8,10 +8,17 @@ class UnionTypeDefinition extends TypeDefinition { + public const CONFIG_NAME = '_union_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } + public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ - $node = self::createNode('_union_config'); + $node = self::createNode(static::CONFIG_NAME); /** @phpstan-ignore-next-line */ $node diff --git a/src/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index ec278e13c..14f11e5b4 100644 --- a/src/Definition/Builder/SchemaBuilder.php +++ b/src/Definition/Builder/SchemaBuilder.php @@ -13,8 +13,8 @@ class SchemaBuilder { - private TypeResolver $typeResolver; - private bool $enableValidation; + protected TypeResolver $typeResolver; + protected bool $enableValidation; public function __construct(TypeResolver $typeResolver, bool $enableValidation = false) { @@ -22,8 +22,16 @@ public function __construct(TypeResolver $typeResolver, bool $enableValidation = $this->enableValidation = $enableValidation; } - public function getBuilder(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): Closure - { + /** + * @param string[] $types + */ + public function getBuilder( + string $name, + ?string $queryAlias, + ?string $mutationAlias = null, + ?string $subscriptionAlias = null, + array $types = [] + ): Closure { return function () use ($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types): ExtensibleSchema { static $schema = null; if (null === $schema) { @@ -37,14 +45,22 @@ public function getBuilder(string $name, ?string $queryAlias, ?string $mutationA /** * @param string[] $types */ - public function create(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): ExtensibleSchema - { + public function create( + string $name, + ?string $queryAlias, + ?string $mutationAlias = null, + ?string $subscriptionAlias = null, + array $types = [] + ): ExtensibleSchema { $this->typeResolver->setCurrentSchemaName($name); $query = $this->typeResolver->resolve($queryAlias); $mutation = $this->typeResolver->resolve($mutationAlias); $subscription = $this->typeResolver->resolve($subscriptionAlias); - $schema = new ExtensibleSchema($this->buildSchemaArguments($name, $query, $mutation, $subscription, $types)); + /** @var class-string $class */ + $class = $this->getSchemaClass(); + + $schema = new $class($this->buildSchemaArguments($name, $query, $mutation, $subscription, $types)); $extensions = []; if ($this->enableValidation) { @@ -55,22 +71,53 @@ public function create(string $name, ?string $queryAlias, ?string $mutationAlias return $schema; } - private function buildSchemaArguments(string $schemaName, Type $query, ?Type $mutation, ?Type $subscription, array $types = []): array - { + /** + * @param string[] $types + * + * @return array + */ + protected function buildSchemaArguments( + string $schemaName, + ?Type $query, + ?Type $mutation, + ?Type $subscription, + array $types = [] + ): array { return [ 'query' => $query, 'mutation' => $mutation, 'subscription' => $subscription, - 'typeLoader' => function ($name) use ($schemaName) { - $this->typeResolver->setCurrentSchemaName($schemaName); + 'typeLoader' => $this->createTypeLoaderClosure($schemaName), + 'types' => $this->createTypesClosure($schemaName, $types), + ]; + } - return $this->typeResolver->resolve($name); - }, - 'types' => function () use ($types, $schemaName) { - $this->typeResolver->setCurrentSchemaName($schemaName); + protected function createTypeLoaderClosure(string $schemaName): callable + { + return function ($name) use ($schemaName): ?Type { + $this->typeResolver->setCurrentSchemaName($schemaName); - return array_map([$this->typeResolver, 'resolve'], $types); - }, - ]; + return $this->typeResolver->resolve($name); + }; + } + + /** + * @param string[] $types + */ + protected function createTypesClosure(string $schemaName, array $types): callable + { + return function () use ($types, $schemaName): array { + $this->typeResolver->setCurrentSchemaName($schemaName); + + return array_map(fn (string $x): ?Type => $this->typeResolver->resolve($x), $types); + }; + } + + /** + * @return class-string + */ + protected function getSchemaClass(): string + { + return ExtensibleSchema::class; } } diff --git a/src/Definition/Builder/TypeFactory.php b/src/Definition/Builder/TypeFactory.php index 19978aa59..9466af3b2 100644 --- a/src/Definition/Builder/TypeFactory.php +++ b/src/Definition/Builder/TypeFactory.php @@ -19,6 +19,9 @@ public function __construct(ConfigProcessor $configProcessor, GraphQLServices $g $this->graphQLServices = $graphQLServices; } + /** + * @param class-string $class + */ public function create(string $class): Type { return new $class($this->configProcessor, $this->graphQLServices); diff --git a/src/Definition/Resolver/AliasedInterface.php b/src/Definition/Resolver/AliasedInterface.php index feed83ba0..e6cde401d 100644 --- a/src/Definition/Resolver/AliasedInterface.php +++ b/src/Definition/Resolver/AliasedInterface.php @@ -11,6 +11,8 @@ interface AliasedInterface * * For instance: * array('myMethod' => 'myAlias') + * + * @return array */ public static function getAliases(): array; } diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 19178e0de..fb15d2aec 100644 --- a/src/DependencyInjection/Compiler/ConfigParserPass.php +++ b/src/DependencyInjection/Compiler/ConfigParserPass.php @@ -8,6 +8,7 @@ use Overblog\GraphQLBundle\Config\Parser\AnnotationParser; use Overblog\GraphQLBundle\Config\Parser\AttributeParser; use Overblog\GraphQLBundle\Config\Parser\GraphQLParser; +use Overblog\GraphQLBundle\Config\Parser\ParserInterface; use Overblog\GraphQLBundle\Config\Parser\PreParserInterface; use Overblog\GraphQLBundle\Config\Parser\YamlParser; use Overblog\GraphQLBundle\DependencyInjection\TypesConfiguration; @@ -35,24 +36,37 @@ class ConfigParserPass implements CompilerPassInterface { + public const TYPE_YAML = 'yaml'; + public const TYPE_GRAPHQL = 'graphql'; + public const TYPE_ANNOTATION = 'annotation'; + public const TYPE_ATTRIBUTE = 'attribute'; + + public const SUPPORTED_TYPES = [ + self::TYPE_YAML, + self::TYPE_GRAPHQL, + self::TYPE_ANNOTATION, + self::TYPE_ATTRIBUTE, + ]; + public const SUPPORTED_TYPES_EXTENSIONS = [ - 'yaml' => '{yaml,yml}', - 'graphql' => '{graphql,graphqls}', - 'annotation' => 'php', - 'attribute' => 'php', + self::TYPE_YAML => '{yaml,yml}', + self::TYPE_GRAPHQL => '{graphql,graphqls}', + self::TYPE_ANNOTATION => 'php', + self::TYPE_ATTRIBUTE => 'php', ]; /** - * @var array> + * @deprecated They are going to be configurable. + * @var array> */ public const PARSERS = [ - 'yaml' => YamlParser::class, - 'graphql' => GraphQLParser::class, - 'annotation' => AnnotationParser::class, - 'attribute' => AttributeParser::class, + self::TYPE_YAML => YamlParser::class, + self::TYPE_GRAPHQL => GraphQLParser::class, + self::TYPE_ANNOTATION => AnnotationParser::class, + self::TYPE_ATTRIBUTE => AttributeParser::class, ]; - private static array $defaultDefaultConfig = [ + private const DEFAULT_CONFIG = [ 'definitions' => [ 'mappings' => [ 'auto_discover' => [ @@ -63,8 +77,15 @@ class ConfigParserPass implements CompilerPassInterface 'types' => [], ], ], + 'parsers' => self::PARSERS, ]; + /** + * @deprecated Use {@see ConfigParserPass::PARSERS }. Added for the backward compatibility. + * @var array> + */ + private static array $defaultDefaultConfig = self::DEFAULT_CONFIG; + private array $treatedFiles = []; private array $preTreatedFiles = []; @@ -86,6 +107,10 @@ private function getConfigs(ContainerBuilder $container): array $config = $container->getParameterBag()->resolveValue($container->getParameter('overblog_graphql.config')); $container->getParameterBag()->remove('overblog_graphql.config'); $container->setParameter($this->getAlias().'.classes_map', []); + + // use default value if needed + $config = array_replace_recursive(self::DEFAULT_CONFIG, $config); + $typesMappings = $this->mappingConfig($config, $container); // reset treated files $this->treatedFiles = []; @@ -96,7 +121,7 @@ private function getConfigs(ContainerBuilder $container): array // Pre-parse all files AnnotationParser::reset($config); AttributeParser::reset($config); - $typesNeedPreParsing = $this->typesNeedPreParsing(); + $typesNeedPreParsing = $this->typesNeedPreParsing($config['parsers']); foreach ($typesMappings as $params) { if ($typesNeedPreParsing[$params['type']]) { $this->parseTypeConfigFiles($params['type'], $params['files'], $container, $config, true); @@ -115,10 +140,15 @@ private function getConfigs(ContainerBuilder $container): array return $flattenTypeConfig; } - private function typesNeedPreParsing(): array + /** + * @param array $parsers + * + * @return array + */ + private function typesNeedPreParsing(array $parsers): array { $needPreParsing = []; - foreach (self::PARSERS as $type => $className) { + foreach ($parsers as $type => $className) { $needPreParsing[$type] = is_a($className, PreParserInterface::class, true); } @@ -145,7 +175,7 @@ private function parseTypeConfigFiles(string $type, iterable $files, ContainerBu continue; } - $parser = [self::PARSERS[$type], $method]; + $parser = [$configs['parsers'][$type], $method]; if (is_callable($parser)) { $config[] = ($parser)($file, $container, $configs); } @@ -169,9 +199,6 @@ private function checkTypesDuplication(array $typeConfigs): void private function mappingConfig(array $config, ContainerBuilder $container): array { - // use default value if needed - $config = array_replace_recursive(self::$defaultDefaultConfig, $config); - $mappingConfig = $config['definitions']['mappings']; $typesMappings = $mappingConfig['types']; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 18a2b335c..7e04a9cef 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -7,6 +7,7 @@ use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; +use Overblog\GraphQLBundle\Config\Parser\ParserInterface; use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\DependencyInjection\Compiler\ConfigParserPass; use Overblog\GraphQLBundle\Error\ErrorHandler; @@ -57,6 +58,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->append($this->securitySection()) ->append($this->doctrineSection()) ->append($this->profilerSection()) + ->append($this->parsersSection()) ->end(); return $treeBuilder; @@ -318,6 +320,32 @@ private function doctrineSection(): ArrayNodeDefinition return $node; } + private function parsersSection(): ArrayNodeDefinition + { + /** @var ArrayNodeDefinition $node */ + $node = (new TreeBuilder('parsers'))->getRootNode(); + $node->useAttributeAsKey('name'); + + $parserPrototype = $node->scalarPrototype(); + $parserPrototype->cannotBeEmpty(); + $parserPrototype->validate() + ->ifTrue(static function (string $x): bool { + return !is_subclass_of($x, ParserInterface::class, true); + }) + ->thenInvalid(sprintf('Parser MUST implement "%s', ParserInterface::class)); + + $node->validate() + ->ifTrue(static function (array $x): bool { + return (bool) array_diff(array_keys($x), ConfigParserPass::SUPPORTED_TYPES); + }) + ->then(static function (array $x) { + $types = implode(', ', array_diff(array_keys($x), ConfigParserPass::SUPPORTED_TYPES)); + throw new \InvalidArgumentException(sprintf('Configured parsers for not supported types: %s', $types)); + }); + + return $node; + } + private function profilerSection(): ArrayNodeDefinition { $builder = new TreeBuilder('profiler'); diff --git a/src/DependencyInjection/TypesConfiguration.php b/src/DependencyInjection/TypesConfiguration.php index 329d8c7e1..09ef2d8d7 100644 --- a/src/DependencyInjection/TypesConfiguration.php +++ b/src/DependencyInjection/TypesConfiguration.php @@ -6,6 +6,8 @@ use Overblog\GraphQLBundle\Config; use Overblog\GraphQLBundle\Config\Processor\InheritanceProcessor; +use Overblog\GraphQLBundle\Config\TypeDefinition; +use Overblog\GraphQLBundle\Enum\TypeEnum; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -21,15 +23,48 @@ class TypesConfiguration implements ConfigurationInterface { + /** + * TODO: refactor. This is dirty solution but quick and with minimal impact on existing structure. + * + * @var array> + */ + private static array $configBuilderClasses = [ + Config\ObjectTypeDefinition::CONFIG_NAME => Config\ObjectTypeDefinition::class, + Config\EnumTypeDefinition::CONFIG_NAME => Config\EnumTypeDefinition::class, + Config\InterfaceTypeDefinition::CONFIG_NAME => Config\InterfaceTypeDefinition::class, + Config\UnionTypeDefinition::CONFIG_NAME => Config\UnionTypeDefinition::class, + Config\InputObjectTypeDefinition::CONFIG_NAME => Config\InputObjectTypeDefinition::class, + Config\CustomScalarTypeDefinition::CONFIG_NAME => Config\CustomScalarTypeDefinition::class, + ]; + + /** + * @var string[] + */ private static array $types = [ - 'object', - 'enum', - 'interface', - 'union', - 'input-object', - 'custom-scalar', + TypeEnum::OBJECT, + TypeEnum::ENUM, + TypeEnum::INTERFACE, + TypeEnum::UNION, + TypeEnum::INPUT_OBJECT, + TypeEnum::CUSTOM_SCALAR, ]; + /** + * @param class-string $fqcn + */ + public static function setConfigBuilderClass(string $fqcn): void + { + if (!is_subclass_of($fqcn, TypeDefinition::class, true)) { + throw new \InvalidArgumentException(sprintf('Options must be a FQCN implementing %s', TypeDefinition::class)); + } + self::$configBuilderClasses[$fqcn::getName()] = $fqcn; + } + + public static function addType(string $type): void + { + self::$types[] = $type; + } + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('overblog_graphql_types'); @@ -42,9 +77,11 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addBeforeNormalization($rootNode); // @phpstan-ignore-next-line - $rootNode + $prototype = $rootNode ->useAttributeAsKey('name') - ->prototype('array') + ->prototype('array'); + + $prototype // config is the unique config entry allowed ->beforeNormalization() ->ifTrue(function ($v) use ($configTypeKeys) { @@ -80,8 +117,9 @@ public function getConfigTreeBuilder(): TreeBuilder return $v; }) ->end() - ->cannotBeOverwritten() - ->children() + ->cannotBeOverwritten(); + $prototypeChildren = $prototype->children(); + $prototypeChildren ->scalarNode('class_name') ->isRequired() ->validate() @@ -94,12 +132,14 @@ public function getConfigTreeBuilder(): TreeBuilder ->prototype('scalar')->info('Types to inherit of.')->end() ->end() ->booleanNode('decorator')->info('Decorator will not be generated.')->defaultFalse()->end() - ->append(Config\ObjectTypeDefinition::create()->getDefinition()) - ->append(Config\EnumTypeDefinition::create()->getDefinition()) - ->append(Config\InterfaceTypeDefinition::create()->getDefinition()) - ->append(Config\UnionTypeDefinition::create()->getDefinition()) - ->append(Config\InputObjectTypeDefinition::create()->getDefinition()) - ->append(Config\CustomScalarTypeDefinition::create()->getDefinition()) + ; + + foreach (self::$configBuilderClasses as $configBuilderClass) { + /** @var class-string $configBuilderClass */ + $prototypeChildren->append($configBuilderClass::create()->getDefinition()); + } + + $prototypeChildren ->variableNode('config')->end() ->end() // _{TYPE}_config is renamed config diff --git a/src/Enum/TypeEnum.php b/src/Enum/TypeEnum.php new file mode 100644 index 000000000..6778f2d97 --- /dev/null +++ b/src/Enum/TypeEnum.php @@ -0,0 +1,20 @@ + $errors + */ public function __construct( array $errors, string $message = '', @@ -27,7 +31,7 @@ public function __construct( } /** - * @param UserError[]|string[] $errors + * @param WebonyxUserError[]|string[] $errors */ public function setErrors(array $errors): void { @@ -37,14 +41,14 @@ public function setErrors(array $errors): void } /** - * @param string|\GraphQL\Error\UserError $error + * @param string|WebonyxUserError $error */ public function addError($error): self { if (is_string($error)) { $error = new UserError($error); - } elseif (!is_object($error) || !$error instanceof \GraphQL\Error\UserError) { - throw new InvalidArgumentException(sprintf('Error must be string or instance of %s.', \GraphQL\Error\UserError::class)); + } elseif (!is_object($error) || !$error instanceof WebonyxUserError) { + throw new InvalidArgumentException(sprintf('Error must be string or instance of %s.', WebonyxUserError::class)); } $this->errors[] = $error; @@ -53,7 +57,7 @@ public function addError($error): self } /** - * @return UserError[] + * @return WebonyxUserError[] */ public function getErrors(): array { diff --git a/src/ExpressionLanguage/ExpressionFunction.php b/src/ExpressionLanguage/ExpressionFunction.php index 06b9610ff..2c79af7d7 100644 --- a/src/ExpressionLanguage/ExpressionFunction.php +++ b/src/ExpressionLanguage/ExpressionFunction.php @@ -10,7 +10,7 @@ class ExpressionFunction extends BaseExpressionFunction { - protected string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; + protected string $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; public function __construct(string $name, callable $compiler, ?callable $evaluator = null) { diff --git a/src/Generator/AwareTypeBaseClassProvider.php b/src/Generator/AwareTypeBaseClassProvider.php new file mode 100644 index 000000000..a71f2417d --- /dev/null +++ b/src/Generator/AwareTypeBaseClassProvider.php @@ -0,0 +1,41 @@ + $this->addProvider($x)); + } + + public function addProvider(TypeBaseClassProviderInterface $provider): void + { + $this->providers[$provider::getType()] = $provider; + } + + /** + * @return class-string + */ + public function getFQCN(string $type): string + { + if (!\array_key_exists($type, $this->providers)) { + throw new \InvalidArgumentException(sprintf('Not configured type required: "%s"', $type)); + } + + return $this->providers[$type]->getBaseClass(); + } +} diff --git a/src/Generator/Collection.php b/src/Generator/Collection.php index 9b861be3b..deb4236d0 100644 --- a/src/Generator/Collection.php +++ b/src/Generator/Collection.php @@ -4,16 +4,14 @@ namespace Overblog\GraphQLBundle\Generator; -use Murtukov\PHPCodeGenerator\Collection as BaseCollection; -use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; +use Overblog\GraphQLBundle\Generator\Model\Collection as BaseCollection; + +@trigger_error(sprintf('Since overblog/graphql-bundle 0.14.4: Class \Overblog\GraphQLBundle\Generator\TypeGeneratorOptions is deprecated. Use %s instead of it.', BaseCollection::class), \E_USER_DEPRECATED); /** - * Extends the default Collection to properly convert expressions. + * @deprecated Use {@see \Overblog\GraphQLBundle\Generator\Model\Collection } */ class Collection extends BaseCollection { - /** - * Mark converters to be used by convertion of array values. - */ - protected array $converters = [ExpressionConverter::class]; + } diff --git a/src/Generator/ConfigBuilder.php b/src/Generator/ConfigBuilder.php new file mode 100644 index 000000000..dd13fee86 --- /dev/null +++ b/src/Generator/ConfigBuilder.php @@ -0,0 +1,118 @@ + + */ + protected iterable $builders; + + /** + * @param iterable $builders + */ + public function __construct(iterable $builders) + { + $this->builders = $builders; + } + + /** + * Builds a config array compatible with webonyx/graphql-php type system. The content + * of the array depends on the GraphQL type that is currently being generated. + * + * Render example (object): + * + * [ + * 'name' => self::NAME, + * 'description' => 'Root query type', + * 'fields' => fn() => [ + * 'posts' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\FieldsBuilder::buildField()}, + * 'users' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\FieldsBuilder::buildField()}, + * ... + * ], + * 'interfaces' => fn() => [ + * $services->getType('PostInterface'), + * ... + * ], + * 'resolveField' => {@see \Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder::build()}, + * ] + * + * Render example (input-object): + * + * [ + * 'name' => self::NAME, + * 'description' => 'Some description.', + * 'validation' => {@see \Overblog\GraphQLBundle\Generator\ValidationRulesBuilder::build()} + * 'fields' => fn() => [ + * {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\FieldsBuilder::buildField()}, + * ... + * ], + * ] + * + * Render example (interface) + * + * [ + * 'name' => self::NAME, + * 'description' => 'Some description.', + * 'fields' => fn() => [ + * {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\FieldsBuilder::buildField()}, + * ... + * ], + * 'resolveType' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\ResolveTypeBuilder::buildResolveType()}, + * ] + * + * Render example (union): + * + * [ + * 'name' => self::NAME, + * 'description' => 'Some description.', + * 'types' => fn() => [ + * $services->getType('Photo'), + * ... + * ], + * 'resolveType' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\ResolveTypeBuilder::buildResolveType()}, + * ] + * + * Render example (custom-scalar): + * + * [ + * 'name' => self::NAME, + * 'description' => 'Some description' + * 'serialize' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\CustomScalarTypeFieldsBuilder::buildScalarCallback()}, + * 'parseValue' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\CustomScalarTypeFieldsBuilder::buildScalarCallback()}, + * 'parseLiteral' => {@see \Overblog\GraphQLBundle\Generator\ConfigBuilder\CustomScalarTypeFieldsBuilder::buildScalarCallback()}, + * ] + * + * Render example (enum): + * + * [ + * 'name' => self::NAME, + * 'values' => [ + * 'PUBLISHED' => ['value' => 1], + * 'DRAFT' => ['value' => 2], + * 'STANDBY' => [ + * 'value' => 3, + * 'description' => 'Waiting for validation', + * ], + * ... + * ], + * ] + */ + public function build(TypeConfig $typeConfig, PhpFile $phpFile): Collection + { + $configLoader = Collection::assoc(); + foreach ($this->builders as $builder) { + $builder->build($typeConfig, $configLoader, $phpFile); + } + + return $configLoader; + } +} diff --git a/src/Generator/ConfigBuilder/ConfigBuilderInterface.php b/src/Generator/ConfigBuilder/ConfigBuilderInterface.php new file mode 100644 index 000000000..d85f44fe5 --- /dev/null +++ b/src/Generator/ConfigBuilder/ConfigBuilderInterface.php @@ -0,0 +1,14 @@ +isCustomScalar()) { + if (isset($typeConfig->scalarType)) { + $builder->addItem('scalarType', $typeConfig->scalarType); + } + + if (isset($typeConfig->serialize)) { + $builder->addItem('serialize', $this->buildScalarCallback($typeConfig->serialize, 'serialize', $typeConfig, $phpFile)); + } + + if (isset($typeConfig->parseValue)) { + $builder->addItem('parseValue', $this->buildScalarCallback($typeConfig->parseValue, 'parseValue', $typeConfig, $phpFile)); + } + + if (isset($typeConfig->parseLiteral)) { + $builder->addItem('parseLiteral', $this->buildScalarCallback($typeConfig->parseLiteral, 'parseLiteral', $typeConfig, $phpFile)); + } + } + } + + /** + * Builds an arrow function that calls a static method. + * + * Render example: + * + * fn() => MyClassName::myMethodName(...\func_get_args()) + * + * @param callable|mixed $callback - a callable string or a callable array + * + * @throws GeneratorException + */ + protected function buildScalarCallback($callback, string $fieldName, TypeConfig $typeConfig, PhpFile $phpFile): ArrowFunction + { + if (!is_callable($callback)) { + throw new GeneratorException("Value of '$fieldName' is not callable."); + } + + $closure = new ArrowFunction(); + + if (\is_array($callback)) { + [$class, $method] = $callback; + } elseif(\is_string($callback)) { + [$class, $method] = explode('::', $callback); + } else { + throw new GeneratorException(sprintf('Invalid type of "%s" value passed.', $fieldName)); + } + + $className = Utils::resolveQualifier($class); + + if ($className === $typeConfig->class_name) { + // Create an alias if name of serializer is same as type name + $className = 'Base' . $className; + $phpFile->addUse($class, $className); + } else { + $phpFile->addUse($class); + } + + $closure->setExpression(Literal::new("$className::$method(...\\func_get_args())")); + + return $closure; + } +} diff --git a/src/Generator/ConfigBuilder/DescriptionBuilder.php b/src/Generator/ConfigBuilder/DescriptionBuilder.php new file mode 100644 index 000000000..1b3fe318b --- /dev/null +++ b/src/Generator/ConfigBuilder/DescriptionBuilder.php @@ -0,0 +1,19 @@ +description)) { + $builder->addItem('description', $typeConfig->description); + } + } +} diff --git a/src/Generator/ConfigBuilder/FieldsBuilder.php b/src/Generator/ConfigBuilder/FieldsBuilder.php new file mode 100644 index 000000000..46300f450 --- /dev/null +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -0,0 +1,359 @@ +expressionConverter = $expressionConverter; + $this->resolveInstructionBuilder = $resolveInstructionBuilder; + $this->validationRulesBuilder = $validationRulesBuilder; + } + + public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void + { + // only by object, input-object and interface types + if (!empty($typeConfig->fields)) { + $builder->addItem('fields', ArrowFunction::new( + Collection::map( + $typeConfig->fields, + fn (array $fieldConfig, string $fieldName) => $this->buildField(new FieldConfig($fieldConfig, $fieldName), $typeConfig, $phpFile) + ) + )); + } + } + + /** + * Render example: + * + * [ + * 'type' => {@see buildType}, + * 'description' => 'Some description.', + * 'deprecationReason' => 'This field will be removed soon.', + * 'args' => fn() => [ + * {@see buildArg}, + * {@see buildArg}, + * ... + * ], + * 'resolve' => {@see \Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder::build()}, + * 'complexity' => {@see buildComplexity}, + * ] + * . + * + * @throws GeneratorException + * + * @internal + */ + protected function buildField(FieldConfig $fieldConfig, TypeConfig $typeConfig, PhpFile $phpFile): Collection + { + // TODO(any): modify `InputValidator` and `TypeDecoratorListener` to support it before re-enabling this + // see https://github.com/overblog/GraphQLBundle/issues/973 + // If there is only 'type', use shorthand + /*if (1 === count($fieldConfig) && isset($fieldConfig->type)) { + return $this->buildType($fieldConfig->type); + }*/ + + $field = Collection::assoc() + ->addItem('type', $this->buildType($fieldConfig->type, $phpFile)); + + // only for object types + if (isset($fieldConfig->resolve)) { + if (isset($fieldConfig->validation)) { + $field->addItem('validation', $this->validationRulesBuilder->build($fieldConfig->validation, $phpFile)); + } + $field->addItem('resolve', $this->resolveInstructionBuilder->build($typeConfig, $fieldConfig->resolve, $fieldConfig->getName(), $fieldConfig->validationGroups ?? null)); + } + + if (isset($fieldConfig->deprecationReason)) { + $field->addItem('deprecationReason', $fieldConfig->deprecationReason); + } + + if (isset($fieldConfig->description)) { + $field->addItem('description', $fieldConfig->description); + } + + if (!empty($fieldConfig->args)) { + $field->addItem('args', Collection::map( + $fieldConfig->args, + fn (array $argConfig, string $argName) => $this->buildArg(new ArgumentConfig($argConfig, $argName), $phpFile), + false + )); + } + + if (isset($fieldConfig->complexity)) { + $field->addItem('complexity', $this->buildComplexity($fieldConfig->complexity)); + } + + if (isset($fieldConfig->public)) { + $field->addItem('public', $this->buildPublic($fieldConfig->public)); + } + + if (isset($fieldConfig->access)) { + $field->addItem('access', $this->buildAccess($fieldConfig->access)); + } + + if (!empty($fieldConfig->access) && is_string($fieldConfig->access) && EL::expressionContainsVar('object', $fieldConfig->access)) { + $field->addItem('useStrictAccess', false); + } + + if ($typeConfig->isInputObject()) { + if ($fieldConfig->offsetExists('defaultValue')) { + $field->addItem('defaultValue', $fieldConfig->defaultValue); + } + + if (isset($fieldConfig->validation)) { + $field->addItem('validation', $this->validationRulesBuilder->build($fieldConfig->validation, $phpFile)); + } + } + + return $field; + } + + /** + * Builds an arrow function from a string with an expression prefix, + * otherwise just returns the provided value back untouched. + * + * Render example (if expression): + * + * fn($value, $args, $context, $info, $object) => $services->get('private_service')->hasAccess() + * + * + * @param string|mixed $access + * + * @return ArrowFunction|mixed + */ + protected function buildAccess($access) + { + if (EL::isStringWithTrigger($access)) { + $expression = $this->expressionConverter->convert($access); + + return ArrowFunction::new() + ->addArguments('value', 'args', 'context', 'info', 'object') + ->setExpression(Literal::new($expression)); + } + + return $access; + } + + /** + * Render example: + * + * [ + * 'name' => 'username', + * 'type' => {@see buildType}, + * 'description' => 'Some fancy description.', + * 'defaultValue' => 'admin', + * ] + * . + * + * @throws GeneratorException + * + * @internal + */ + protected function buildArg(ArgumentConfig $argConfig, PhpFile $phpFile): Collection + { + // Convert to object for better readability + + $arg = Collection::assoc() + ->addItem('name', $argConfig->getName()) + ->addItem('type', $this->buildType($argConfig->type, $phpFile)); + + if (isset($argConfig->description)) { + $arg->addIfNotEmpty('description', $argConfig->description); + } + + if ($argConfig->offsetExists('defaultValue')) { + $arg->addItem('defaultValue', $argConfig->defaultValue); + } + + if (isset($argConfig->validation) && !empty($argConfig->validation)) { + if (isset($argConfig->validation['cascade']) && \in_array($argConfig->type, self::BUILT_IN_TYPES, true)) { + throw new GeneratorException('Cascade validation cannot be applied to built-in types.'); + } + + $arg->addIfNotEmpty('validation', $this->validationRulesBuilder->build($argConfig->validation, $phpFile)); + } + + return $arg; + } + + /** + * Builds a closure or an arrow function, depending on whether the `args` param is provided. + * + * Render example (closure): + * + * function ($value, $arguments) use ($services) { + * $args = $services->get('argumentFactory')->create($arguments); + * return ($args['age'] + 5); + * } + * + * + * Render example (arrow function): + * + * fn($childrenComplexity) => ($childrenComplexity + 20); + * + * + * @param string|mixed $complexity + */ + protected function buildComplexity($complexity): GeneratorInterface + { + if (EL::isStringWithTrigger($complexity)) { + $expression = $this->expressionConverter->convert($complexity); + + if (EL::expressionContainsVar('args', $complexity)) { + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + + return Closure::new() + ->addArgument('childrenComplexity') + ->addArgument('arguments', '', []) + ->bindVar(TypeGenerator::GRAPHQL_SERVICES) + ->append('$args = ', "{$gqlServices}->get('argumentFactory')->create(\$arguments)") + ->append('return ', $expression); + } + + $arrow = ArrowFunction::new(is_string($expression) ? new Literal($expression) : $expression); + + if (EL::expressionContainsVar('childrenComplexity', $complexity)) { + $arrow->addArgument('childrenComplexity'); + } + + return $arrow; + } + + return new ArrowFunction(0); + } + + /** + * Builds an arrow function from a string with an expression prefix, + * otherwise just returns the provided value back untouched. + * + * Render example (if expression): + * + * fn($fieldName, $typeName = self::NAME) => ($fieldName == "name") + * + * @param string|mixed $public + * + * @return ArrowFunction|mixed + */ + protected function buildPublic($public) + { + if (EL::isStringWithTrigger($public)) { + $expression = $this->expressionConverter->convert($public); + $arrow = ArrowFunction::new(Literal::new($expression)); + + if (EL::expressionContainsVar('fieldName', $public)) { + $arrow->addArgument('fieldName'); + } + + if (EL::expressionContainsVar('typeName', $public)) { + $arrow->addArgument('fieldName'); + $arrow->addArgument('typeName', '', new Literal('self::NAME')); + } + + return $arrow; + } + + return $public; + } + + /** + * Converts a native GraphQL type string into the `webonyx/graphql-php` + * type literal. References to user-defined types are converted into + * TypeResovler method call and wrapped into a closure. + * + * Render examples: + * + * - "String" -> Type::string() + * - "String!" -> Type::nonNull(Type::string()) + * - "[String!] -> Type::listOf(Type::nonNull(Type::string())) + * - "[Post]" -> Type::listOf($services->getType('Post')) + * + * @return GeneratorInterface|string + */ + protected function buildType(string $typeDefinition, PhpFile $phpFile) + { + $typeNode = Parser::parseType($typeDefinition); + + $isReference = false; + $type = $this->wrapTypeRecursive($typeNode, $isReference, $phpFile); + + if ($isReference) { + // References to other types should be wrapped in a closure + // for performance reasons + return ArrowFunction::new($type); + } + + return $type; + } + + /** + * Used by {@see buildType}. + * + * @param TypeNode|mixed $typeNode + * + * @return Literal|string + */ + protected function wrapTypeRecursive($typeNode, bool &$isReference, PhpFile $phpFile) + { + switch ($typeNode->kind) { + case NodeKind::NON_NULL_TYPE: + $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference, $phpFile); + $type = Literal::new("Type::nonNull($innerType)"); + $phpFile->addUse(Type::class); + break; + case NodeKind::LIST_TYPE: + $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference, $phpFile); + $type = Literal::new("Type::listOf($innerType)"); + $phpFile->addUse(Type::class); + break; + default: // NodeKind::NAMED_TYPE + if (in_array($typeNode->name->value, static::BUILT_IN_TYPES)) { + $name = strtolower($typeNode->name->value); + $type = Literal::new("Type::$name()"); + $phpFile->addUse(Type::class); + } else { + $name = $typeNode->name->value; + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $type = "{$gqlServices}->getType('$name')"; + $isReference = true; + } + break; + } + + return $type; + } +} diff --git a/src/Generator/ConfigBuilder/InterfacesBuilder.php b/src/Generator/ConfigBuilder/InterfacesBuilder.php new file mode 100644 index 000000000..ac6a5607d --- /dev/null +++ b/src/Generator/ConfigBuilder/InterfacesBuilder.php @@ -0,0 +1,23 @@ +interfaces) && !empty($typeConfig->interfaces)) { + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $items = array_map(static fn ($type) => "{$gqlServices}->getType('$type')", $typeConfig->interfaces); + $builder->addItem('interfaces', ArrowFunction::new(Collection::numeric($items, true))); + } + } +} diff --git a/src/Generator/ConfigBuilder/IsTypeOfBuilder.php b/src/Generator/ConfigBuilder/IsTypeOfBuilder.php new file mode 100644 index 000000000..d923df7a8 --- /dev/null +++ b/src/Generator/ConfigBuilder/IsTypeOfBuilder.php @@ -0,0 +1,55 @@ +expressionConverter = $expressionConverter; + } + + public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void + { + if (isset($typeConfig->isTypeOf)) { + $builder->addItem('isTypeOf', $this->buildIsTypeOf($typeConfig->isTypeOf)); + } + } + + /** + * Builds an arrow function from a string with an expression prefix, + * otherwise just returns the provided value back untouched. + * + * Render example: + * + * fn($className) => (($className = "App\\ClassName") && $value instanceof $className) + * + * @param mixed $isTypeOf + */ + private function buildIsTypeOf($isTypeOf): ArrowFunction + { + if (EL::isStringWithTrigger($isTypeOf)) { + $expression = $this->expressionConverter->convert($isTypeOf); + + return ArrowFunction::new(Literal::new($expression), 'bool') + ->setStatic() + ->addArguments('value', 'context') + ->addArgument('info', ResolveInfo::class); + } + + return ArrowFunction::new($isTypeOf); + } +} diff --git a/src/Generator/ConfigBuilder/NameBuilder.php b/src/Generator/ConfigBuilder/NameBuilder.php new file mode 100644 index 000000000..d738a2a97 --- /dev/null +++ b/src/Generator/ConfigBuilder/NameBuilder.php @@ -0,0 +1,18 @@ +addItem('name', new Literal('self::NAME')); + } +} diff --git a/src/Generator/ConfigBuilder/ResolveFieldBuilder.php b/src/Generator/ConfigBuilder/ResolveFieldBuilder.php new file mode 100644 index 000000000..edcd6aae5 --- /dev/null +++ b/src/Generator/ConfigBuilder/ResolveFieldBuilder.php @@ -0,0 +1,27 @@ +resolveInstructionBuilder = $resolveInstructionBuilder; + } + + public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void + { + if (isset($typeConfig->resolveField)) { + $builder->addItem('resolveField', $this->resolveInstructionBuilder->build($typeConfig, $typeConfig->resolveField)); + } + } +} diff --git a/src/Generator/ConfigBuilder/ResolveTypeBuilder.php b/src/Generator/ConfigBuilder/ResolveTypeBuilder.php new file mode 100644 index 000000000..2f6c33ed3 --- /dev/null +++ b/src/Generator/ConfigBuilder/ResolveTypeBuilder.php @@ -0,0 +1,55 @@ +expressionConverter = $expressionConverter; + } + + public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void + { + if (isset($typeConfig->resolveType)) { + $builder->addItem('resolveType', $this->buildResolveType($typeConfig->resolveType)); + } + } + + /** + * Builds an arrow function from a string with an expression prefix, + * otherwise just returns the provided value back untouched. + * + * Render example: + * + * fn($value, $context, $info) => $services->getType($value) + * + * @param mixed $resolveType + * + * @return mixed|ArrowFunction + */ + protected function buildResolveType($resolveType) + { + if (EL::isStringWithTrigger($resolveType)) { + $expression = $this->expressionConverter->convert($resolveType); + + return ArrowFunction::new() + ->addArguments('value', 'context', 'info') + ->setExpression(Literal::new($expression)); + } + + return $resolveType; + } +} diff --git a/src/Generator/ConfigBuilder/TypesBuilder.php b/src/Generator/ConfigBuilder/TypesBuilder.php new file mode 100644 index 000000000..16d68263f --- /dev/null +++ b/src/Generator/ConfigBuilder/TypesBuilder.php @@ -0,0 +1,23 @@ +types) && !empty($typeConfig->types)) { + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $items = array_map(static fn ($type) => "{$gqlServices}->getType('$type')", $typeConfig->types); + $builder->addItem('types', ArrowFunction::new(Collection::numeric($items, true))); + } + } +} diff --git a/src/Generator/ConfigBuilder/ValidationBuilder.php b/src/Generator/ConfigBuilder/ValidationBuilder.php new file mode 100644 index 000000000..ebddc18d1 --- /dev/null +++ b/src/Generator/ConfigBuilder/ValidationBuilder.php @@ -0,0 +1,28 @@ +validationRulesBuilder = $validationRulesBuilder; + } + + public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void + { + // only by input-object types (for class level validation) + if (isset($typeConfig->validation)) { + $builder->addItem('validation', $this->validationRulesBuilder->build($typeConfig->validation, $phpFile)); + } + } +} diff --git a/src/Generator/ConfigBuilder/ValuesBuilder.php b/src/Generator/ConfigBuilder/ValuesBuilder.php new file mode 100644 index 000000000..30e47fe7e --- /dev/null +++ b/src/Generator/ConfigBuilder/ValuesBuilder.php @@ -0,0 +1,20 @@ +values)) { + $builder->addItem('values', Collection::assoc($typeConfig->values)); + } + } +} diff --git a/src/Generator/Model/AbstractConfig.php b/src/Generator/Model/AbstractConfig.php new file mode 100644 index 000000000..584c534d0 --- /dev/null +++ b/src/Generator/Model/AbstractConfig.php @@ -0,0 +1,59 @@ + + */ +abstract class AbstractConfig extends \ArrayObject +{ + /** + * @param string $name + * + * @return mixed|null + */ + public function __get($name) + { + return $this->offsetGet($name); + } + + /** + * @param string $name + */ + public function __isset($name): bool + { + return $this->offsetExists($name) && null !== $this->offsetGet($name); + } + + /** + * @param string|int $name + * @param mixed|null $value + */ + public function __set($name, $value): void + { + $this->offsetSet($name, $value); + } + + public function offsetGet($key) + { + if (!$this->offsetExists($key)) { + throw new \OutOfBoundsException(sprintf('Index "%s" is undefined', $key)); + } + + return parent::offsetGet($key); + } + + public function offsetSet($key, $value): void + { + throw new \LogicException('Setting of values is forbidden'); + } + + public function offsetUnset($key): void + { + throw new \LogicException('Unsetting of values is forbidden'); + } +} diff --git a/src/Generator/Model/ArgumentConfig.php b/src/Generator/Model/ArgumentConfig.php new file mode 100644 index 000000000..9b7958c2c --- /dev/null +++ b/src/Generator/Model/ArgumentConfig.php @@ -0,0 +1,31 @@ + $config + */ + public function __construct(array $config, string $name) + { + parent::__construct($config); + + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Generator/Model/Collection.php b/src/Generator/Model/Collection.php new file mode 100644 index 000000000..df5209f4f --- /dev/null +++ b/src/Generator/Model/Collection.php @@ -0,0 +1,19 @@ + $config + */ + public function __construct(array $config, string $name) + { + parent::__construct($config); + + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Generator/Model/TypeConfig.php b/src/Generator/Model/TypeConfig.php new file mode 100644 index 000000000..87d540b5c --- /dev/null +++ b/src/Generator/Model/TypeConfig.php @@ -0,0 +1,49 @@ + $config + */ + public function __construct(array $config, string $type) + { + parent::__construct($config); + + $this->type = $type; + } + + public function isCustomScalar(): bool + { + return TypeEnum::CUSTOM_SCALAR === $this->type; + } + + public function isInputObject(): bool + { + return TypeEnum::INPUT_OBJECT === $this->type; + } +} diff --git a/src/Generator/Model/TypeGeneratorOptions.php b/src/Generator/Model/TypeGeneratorOptions.php new file mode 100644 index 000000000..d237eda4f --- /dev/null +++ b/src/Generator/Model/TypeGeneratorOptions.php @@ -0,0 +1,54 @@ +namespace = $namespace; + $this->cacheDir = $cacheDir; + $this->useClassMap = $useClassMap; + $this->cacheBaseDir = $cacheBaseDir; + + if (null === $cacheDirMask) { + // Apply permission 0777 for default cache dir otherwise apply 0775. + $cacheDirMask = null === $cacheDir ? 0777 : 0775; + } + + $this->cacheDirMask = $cacheDirMask; + } +} diff --git a/src/Generator/Model/ValidationConfig.php b/src/Generator/Model/ValidationConfig.php new file mode 100644 index 000000000..12259218e --- /dev/null +++ b/src/Generator/Model/ValidationConfig.php @@ -0,0 +1,14 @@ +expressionConverter = $expressionConverter; + } + + /** + * Builds a resolver closure that contains the compiled result of user-defined + * expression and optionally the validation logic. + * + * Render example (no expression language): + * + * function ($value, $args, $context, $info) use ($services) { + * return "Hello, World!"; + * } + * + * Render example (with expression language): + * + * function ($value, $args, $context, $info) use ($services) { + * return $services->mutation("my_resolver", $args); + * } + * + * Render example (with validation): + * + * function ($value, $args, $context, $info) use ($services) { + * $validator = $services->createInputValidator(...func_get_args()); + * return $services->mutation("create_post", $validator]); + * } + * + * Render example (with validation, but errors are injected into the user-defined resolver): + * {@link https://github.com/overblog/GraphQLBundle/blob/master/docs/validation/index.md#injecting-errors} + * + * function ($value, $args, $context, $info) use ($services) { + * $errors = new ResolveErrors(); + * $validator = $services->createInputValidator(...func_get_args()); + * + * $errors->setValidationErrors($validator->validate(null, false)) + * + * return $services->mutation("create_post", $errors); + * } + * + * @param string|mixed $resolve + * + * @throws GeneratorException + * + * @return GeneratorInterface|string + */ + public function build(TypeConfig $typeConfig, $resolve, ?string $currentField = null, ?array $groups = null) + { + if (is_callable($resolve) && is_array($resolve)) { + return Collection::numeric($resolve); + } + + // TODO: before creating an input validator, check if any validation rules are defined + if (EL::isStringWithTrigger($resolve)) { + $closure = Closure::new() + ->addArguments('value', 'args', 'context', 'info') + ->bindVar(TypeGenerator::GRAPHQL_SERVICES); + + $injectValidator = EL::expressionContainsVar('validator', $resolve); + + if ($this->configContainsValidation($typeConfig, $currentField)) { + $injectErrors = EL::expressionContainsVar('errors', $resolve); + + if ($injectErrors) { + $closure->append('$errors = ', Instance::new(ResolveErrors::class)); + } + + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $closure->append('$validator = ', "{$gqlServices}->createInputValidator(...func_get_args())"); + + // If auto-validation on or errors are injected + if (!$injectValidator || $injectErrors) { + if (!empty($groups)) { + $validationGroups = Collection::numeric($groups); + } else { + $validationGroups = 'null'; + } + + $closure->emptyLine(); + + if ($injectErrors) { + $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))'); + } else { + $closure->append('$validator->validate(', $validationGroups, ')'); + } + + $closure->emptyLine(); + } + } elseif ($injectValidator) { + throw new GeneratorException('Unable to inject an instance of the InputValidator. No validation constraints provided. Please remove the "validator" argument from the list of dependencies of your resolver or provide validation configs.'); + } + + $closure->append('return ', $this->expressionConverter->convert($resolve)); + + return $closure; + } + + return ArrowFunction::new($resolve); + } + + /** + * Checks if given config contains any validation rules. + */ + protected function configContainsValidation(TypeConfig $typeConfig, ?string $currentField): bool + { + // FIXME this strange solution used to save current strange behavior :) It MUST BE refactored!!! + $currentField ??= array_key_last($typeConfig->fields); + $fieldConfig = $typeConfig->fields[$currentField]; + + if (!empty($fieldConfig['validation'])) { + return true; + } + + foreach ($fieldConfig['args'] ?? [] as $argConfig) { + if (!empty($argConfig['validation'])) { + return true; + } + } + + return false; + } +} diff --git a/src/Generator/TypeBaseClassProvider/CustomScalarTypeBaseClassProvider.php b/src/Generator/TypeBaseClassProvider/CustomScalarTypeBaseClassProvider.php new file mode 100644 index 000000000..caab1c76a --- /dev/null +++ b/src/Generator/TypeBaseClassProvider/CustomScalarTypeBaseClassProvider.php @@ -0,0 +1,21 @@ + + */ + public function getBaseClass(): string; +} diff --git a/src/Generator/TypeBaseClassProvider/UnionTypeBaseClassProvider.php b/src/Generator/TypeBaseClassProvider/UnionTypeBaseClassProvider.php new file mode 100644 index 000000000..90cc61384 --- /dev/null +++ b/src/Generator/TypeBaseClassProvider/UnionTypeBaseClassProvider.php @@ -0,0 +1,21 @@ + ObjectType::class, - 'input-object' => InputObjectType::class, - 'interface' => InterfaceType::class, - 'union' => UnionType::class, - 'enum' => EnumType::class, - 'custom-scalar' => CustomScalarType::class, - ]; + protected AwareTypeBaseClassProvider $baseClassProvider; + protected ConfigBuilder $configBuilder; protected ExpressionConverter $expressionConverter; - protected PhpFile $file; protected string $namespace; - protected array $config; - protected string $type; - protected string $currentField; - protected string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; - public function __construct(ExpressionConverter $expressionConverter, string $namespace) - { + public function __construct( + AwareTypeBaseClassProvider $baseClassProvider, + ConfigBuilder $configBuilder, + ExpressionConverter $expressionConverter, + string $namespace + ) { + $this->baseClassProvider = $baseClassProvider; + $this->configBuilder = $configBuilder; $this->expressionConverter = $expressionConverter; $this->namespace = $namespace; @@ -111,15 +74,12 @@ public function __construct(ExpressionConverter $expressionConverter, string $na */ public function build(array $config, string $type): PhpFile { - // This values should be accessible from every method - $this->config = $config; - $this->type = $type; - - $this->file = PhpFile::new()->setNamespace($this->namespace); + $typeConfig = new TypeConfig($config, $type); + $file = PhpFile::new()->setNamespace($this->namespace); - $class = $this->file->createClass($config['class_name']) + $class = $file->createClass($config['class_name']) ->setFinal() - ->setExtends(static::EXTENDS[$type]) + ->setExtends($this->baseClassProvider->getFQCN($type)) ->addImplements(GeneratedTypeInterface::class, AliasedInterface::class) ->addConst('NAME', $config['name']) ->setDocBlock(static::DOCBLOCK_TEXT); @@ -129,7 +89,7 @@ public function build(array $config, string $type): PhpFile $class->createConstructor() ->addArgument('configProcessor', ConfigProcessor::class) ->addArgument(TypeGenerator::GRAPHQL_SERVICES, GraphQLServices::class) - ->append('$config = ', $this->buildConfig($config)) + ->append('$config = ', $this->configBuilder->build($typeConfig, $file)) ->emptyLine() ->append('parent::__construct($configProcessor->process($config))'); @@ -139,855 +99,6 @@ public function build(array $config, string $type): PhpFile ->setDocBlock('{@inheritdoc}') ->append('return [self::NAME]'); - return $this->file; - } - - /** - * Converts a native GraphQL type string into the `webonyx/graphql-php` - * type literal. References to user-defined types are converted into - * TypeResovler method call and wrapped into a closure. - * - * Render examples: - * - * - "String" -> Type::string() - * - "String!" -> Type::nonNull(Type::string()) - * - "[String!] -> Type::listOf(Type::nonNull(Type::string())) - * - "[Post]" -> Type::listOf($services->getType('Post')) - * - * @return GeneratorInterface|string - */ - protected function buildType(string $typeDefinition) - { - $typeNode = Parser::parseType($typeDefinition); - - $isReference = false; - $type = $this->wrapTypeRecursive($typeNode, $isReference); - - if ($isReference) { - // References to other types should be wrapped in a closure - // for performance reasons - return ArrowFunction::new($type); - } - - return $type; - } - - /** - * Used by {@see buildType}. - * - * @param mixed $typeNode - * - * @return Literal|string - */ - protected function wrapTypeRecursive($typeNode, bool &$isReference) - { - switch ($typeNode->kind) { - case NodeKind::NON_NULL_TYPE: - $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference); - $type = Literal::new("Type::nonNull($innerType)"); - $this->file->addUse(Type::class); - break; - case NodeKind::LIST_TYPE: - $innerType = $this->wrapTypeRecursive($typeNode->type, $isReference); - $type = Literal::new("Type::listOf($innerType)"); - $this->file->addUse(Type::class); - break; - default: // NodeKind::NAMED_TYPE - if (in_array($typeNode->name->value, static::BUILT_IN_TYPES)) { - $name = strtolower($typeNode->name->value); - $type = Literal::new("Type::$name()"); - $this->file->addUse(Type::class); - } else { - $name = $typeNode->name->value; - $type = "$this->gqlServices->getType('$name')"; - $isReference = true; - } - break; - } - - return $type; - } - - /** - * Builds a config array compatible with webonyx/graphql-php type system. The content - * of the array depends on the GraphQL type that is currently being generated. - * - * Render example (object): - * - * [ - * 'name' => self::NAME, - * 'description' => 'Root query type', - * 'fields' => fn() => [ - * 'posts' => {@see buildField}, - * 'users' => {@see buildField}, - * ... - * ], - * 'interfaces' => fn() => [ - * $services->getType('PostInterface'), - * ... - * ], - * 'resolveField' => {@see buildResolveField}, - * ] - * - * Render example (input-object): - * - * [ - * 'name' => self::NAME, - * 'description' => 'Some description.', - * 'validation' => {@see buildValidationRules} - * 'fields' => fn() => [ - * {@see buildField}, - * ... - * ], - * ] - * - * Render example (interface) - * - * [ - * 'name' => self::NAME, - * 'description' => 'Some description.', - * 'fields' => fn() => [ - * {@see buildField}, - * ... - * ], - * 'resolveType' => {@see buildResolveType}, - * ] - * - * Render example (union): - * - * [ - * 'name' => self::NAME, - * 'description' => 'Some description.', - * 'types' => fn() => [ - * $services->getType('Photo'), - * ... - * ], - * 'resolveType' => {@see buildResolveType}, - * ] - * - * Render example (custom-scalar): - * - * [ - * 'name' => self::NAME, - * 'description' => 'Some description' - * 'serialize' => {@see buildScalarCallback}, - * 'parseValue' => {@see buildScalarCallback}, - * 'parseLiteral' => {@see buildScalarCallback}, - * ] - * - * Render example (enum): - * - * [ - * 'name' => self::NAME, - * 'values' => [ - * 'PUBLISHED' => ['value' => 1], - * 'DRAFT' => ['value' => 2], - * 'STANDBY' => [ - * 'value' => 3, - * 'description' => 'Waiting for validation', - * ], - * ... - * ], - * ] - * - * @throws GeneratorException - */ - protected function buildConfig(array $config): Collection - { - // Convert to an object for a better readability - $c = (object) $config; - - $configLoader = Collection::assoc(); - $configLoader->addItem('name', new Literal('self::NAME')); - - if (isset($c->description)) { - $configLoader->addItem('description', $c->description); - } - - // only by input-object types (for class level validation) - if (isset($c->validation)) { - $configLoader->addItem('validation', $this->buildValidationRules($c->validation)); - } - - // only by object, input-object and interface types - if (!empty($c->fields)) { - $configLoader->addItem('fields', ArrowFunction::new( - Collection::map($c->fields, [$this, 'buildField']) - )); - } - - if (!empty($c->interfaces)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->interfaces); - $configLoader->addItem('interfaces', ArrowFunction::new(Collection::numeric($items, true))); - } - - if (!empty($c->types)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->types); - $configLoader->addItem('types', ArrowFunction::new(Collection::numeric($items, true))); - } - - if (isset($c->resolveType)) { - $configLoader->addItem('resolveType', $this->buildResolveType($c->resolveType)); - } - - if (isset($c->isTypeOf)) { - $configLoader->addItem('isTypeOf', $this->buildIsTypeOf($c->isTypeOf)); - } - - if (isset($c->resolveField)) { - $configLoader->addItem('resolveField', $this->buildResolve($c->resolveField)); - } - - // only by enum types - if (isset($c->values)) { - $configLoader->addItem('values', Collection::assoc($c->values)); - } - - // only by custom-scalar types - if ('custom-scalar' === $this->type) { - if (isset($c->scalarType)) { - $configLoader->addItem('scalarType', $c->scalarType); - } - - if (isset($c->serialize)) { - $configLoader->addItem('serialize', $this->buildScalarCallback($c->serialize, 'serialize')); - } - - if (isset($c->parseValue)) { - $configLoader->addItem('parseValue', $this->buildScalarCallback($c->parseValue, 'parseValue')); - } - - if (isset($c->parseLiteral)) { - $configLoader->addItem('parseLiteral', $this->buildScalarCallback($c->parseLiteral, 'parseLiteral')); - } - } - - return $configLoader; - } - - /** - * Builds an arrow function that calls a static method. - * - * Render example: - * - * fn() => MyClassName::myMethodName(...\func_get_args()) - * - * @param callable $callback - a callable string or a callable array - * - * @throws GeneratorException - * - * @return ArrowFunction - */ - protected function buildScalarCallback($callback, string $fieldName) - { - if (!is_callable($callback)) { - throw new GeneratorException("Value of '$fieldName' is not callable."); - } - - $closure = new ArrowFunction(); - - if (!is_string($callback)) { - [$class, $method] = $callback; - } else { - [$class, $method] = explode('::', $callback); - } - - $className = Utils::resolveQualifier($class); - - if ($className === $this->config['class_name']) { - // Create an alias if name of serializer is same as type name - $className = 'Base'.$className; - $this->file->addUse($class, $className); - } else { - $this->file->addUse($class); - } - - $closure->setExpression(Literal::new("$className::$method(...\\func_get_args())")); - - return $closure; - } - - /** - * Builds a resolver closure that contains the compiled result of user-defined - * expression and optionally the validation logic. - * - * Render example (no expression language): - * - * function ($value, $args, $context, $info) use ($services) { - * return "Hello, World!"; - * } - * - * Render example (with expression language): - * - * function ($value, $args, $context, $info) use ($services) { - * return $services->mutation("my_resolver", $args); - * } - * - * Render example (with validation): - * - * function ($value, $args, $context, $info) use ($services) { - * $validator = $services->createInputValidator(...func_get_args()); - * return $services->mutation("create_post", $validator]); - * } - * - * Render example (with validation, but errors are injected into the user-defined resolver): - * {@link https://github.com/overblog/GraphQLBundle/blob/master/docs/validation/index.md#injecting-errors} - * - * function ($value, $args, $context, $info) use ($services) { - * $errors = new ResolveErrors(); - * $validator = $services->createInputValidator(...func_get_args()); - * - * $errors->setValidationErrors($validator->validate(null, false)) - * - * return $services->mutation("create_post", $errors); - * } - * - * @param mixed $resolve - * - * @throws GeneratorException - * - * @return GeneratorInterface|string - */ - protected function buildResolve($resolve, ?array $groups = null) - { - if (is_callable($resolve) && is_array($resolve)) { - return Collection::numeric($resolve); - } - - // TODO: before creating an input validator, check if any validation rules are defined - if (EL::isStringWithTrigger($resolve)) { - $closure = Closure::new() - ->addArguments('value', 'args', 'context', 'info') - ->bindVar(TypeGenerator::GRAPHQL_SERVICES); - - $injectValidator = EL::expressionContainsVar('validator', $resolve); - - if ($this->configContainsValidation()) { - $injectErrors = EL::expressionContainsVar('errors', $resolve); - - if ($injectErrors) { - $closure->append('$errors = ', Instance::new(ResolveErrors::class)); - } - - $closure->append('$validator = ', "$this->gqlServices->createInputValidator(...func_get_args())"); - - // If auto-validation on or errors are injected - if (!$injectValidator || $injectErrors) { - if (!empty($groups)) { - $validationGroups = Collection::numeric($groups); - } else { - $validationGroups = 'null'; - } - - $closure->emptyLine(); - - if ($injectErrors) { - $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))'); - } else { - $closure->append('$validator->validate(', $validationGroups, ')'); - } - - $closure->emptyLine(); - } - } elseif ($injectValidator) { - throw new GeneratorException('Unable to inject an instance of the InputValidator. No validation constraints provided. Please remove the "validator" argument from the list of dependencies of your resolver or provide validation configs.'); - } - - $closure->append('return ', $this->expressionConverter->convert($resolve)); - - return $closure; - } - - return ArrowFunction::new($resolve); - } - - /** - * Checks if given config contains any validation rules. - */ - private function configContainsValidation(): bool - { - $fieldConfig = $this->config['fields'][$this->currentField]; - - if (!empty($fieldConfig['validation'])) { - return true; - } - - foreach ($fieldConfig['args'] ?? [] as $argConfig) { - if (!empty($argConfig['validation'])) { - return true; - } - } - - return false; - } - - /** - * Render example: - * - * [ - * 'link' => {@see normalizeLink} - * 'cascade' => [ - * 'groups' => ['my_group'], - * ], - * 'constraints' => {@see buildConstraints} - * ] - * - * If only constraints provided, uses {@see buildConstraints} directly. - * - * @param array{ - * constraints: array, - * link: string, - * cascade: array - * } $config - * - * @throws GeneratorException - */ - protected function buildValidationRules(array $config): GeneratorInterface - { - // Convert to object for better readability - $c = (object) $config; - - $array = Collection::assoc(); - - if (!empty($c->link)) { - if (false === strpos($c->link, '::')) { - // e.g. App\Entity\Droid - $array->addItem('link', $c->link); - } else { - // e.g. App\Entity\Droid::$id - $array->addItem('link', Collection::numeric($this->normalizeLink($c->link))); - } - } - - if (isset($c->cascade)) { - // If there are only constarainst, use short syntax - if (empty($c->cascade['groups'])) { - $this->file->addUse(InputValidator::class); - - return Literal::new('InputValidator::CASCADE'); - } - $array->addItem('cascade', $c->cascade['groups']); - } - - if (!empty($c->constraints)) { - // If there are only constarainst, use short syntax - if (0 === $array->count()) { - return $this->buildConstraints($c->constraints); - } - $array->addItem('constraints', $this->buildConstraints($c->constraints)); - } - - return $array; - } - - /** - * Builds a closure or a numeric multiline array with Symfony Constraint - * instances. The array is used by {@see InputValidator} during requests. - * - * Render example (array): - * - * [ - * new NotNull(), - * new Length([ - * 'min' => 5, - * 'max' => 10 - * ]), - * ... - * ] - * - * Render example (in a closure): - * - * fn() => [ - * new NotNull(), - * new Length([ - * 'min' => 5, - * 'max' => 10 - * ]), - * ... - * ] - * - * @throws GeneratorException - * - * @return ArrowFunction|Collection - */ - protected function buildConstraints(array $constraints = [], bool $inClosure = true) - { - $result = Collection::numeric()->setMultiline(); - - foreach ($constraints as $wrapper) { - $name = key($wrapper); - $args = reset($wrapper); - - if (false !== strpos($name, '\\')) { - // Custom constraint - $fqcn = ltrim($name, '\\'); - $instance = Instance::new("@\\$fqcn"); - } else { - // Symfony constraint - $fqcn = static::CONSTRAINTS_NAMESPACE."\\$name"; - $this->file->addUse(static::CONSTRAINTS_NAMESPACE.' as SymfonyConstraints'); - $instance = Instance::new("@SymfonyConstraints\\$name"); - } - - if (!class_exists($fqcn)) { - throw new GeneratorException("Constraint class '$fqcn' doesn't exist."); - } - - if (is_array($args)) { - if (isset($args[0]) && is_array($args[0])) { - // Nested instance - $instance->addArgument($this->buildConstraints($args, false)); - } elseif (isset($args['constraints'][0]) && is_array($args['constraints'][0])) { - // Nested instance with "constraints" key (full syntax) - $options = [ - 'constraints' => $this->buildConstraints($args['constraints'], false), - ]; - - // Check for additional options - foreach ($args as $key => $option) { - if ('constraints' === $key) { - continue; - } - $options[$key] = $option; - } - - $instance->addArgument($options); - } else { - // Numeric or Assoc array? - $instance->addArgument(isset($args[0]) ? $args : Collection::assoc($args)); - } - } elseif (null !== $args) { - $instance->addArgument($args); - } - - $result->push($instance); - } - - if ($inClosure) { - return ArrowFunction::new($result); - } - - return $result; // @phpstan-ignore-line - } - - /** - * Render example: - * - * [ - * 'type' => {@see buildType}, - * 'description' => 'Some description.', - * 'deprecationReason' => 'This field will be removed soon.', - * 'args' => fn() => [ - * {@see buildArg}, - * {@see buildArg}, - * ... - * ], - * 'resolve' => {@see buildResolve}, - * 'complexity' => {@see buildComplexity}, - * ] - * - * @param array{ - * type: string, - * resolve?: string, - * description?: string, - * args?: array, - * complexity?: string, - * deprecatedReason?: string, - * validation?: array, - * } $fieldConfig - * - * @internal - * - * @throws GeneratorException - * - * @return GeneratorInterface|Collection|string - */ - public function buildField(array $fieldConfig, string $fieldname) - { - $this->currentField = $fieldname; - - // Convert to object for better readability - $c = (object) $fieldConfig; - - // TODO(any): modify `InputValidator` and `TypeDecoratorListener` to support it before re-enabling this - // see https://github.com/overblog/GraphQLBundle/issues/973 - // If there is only 'type', use shorthand - /*if (1 === count($fieldConfig) && isset($c->type)) { - return $this->buildType($c->type); - }*/ - - $field = Collection::assoc() - ->addItem('type', $this->buildType($c->type)); - - // only for object types - if (isset($c->resolve)) { - if (isset($c->validation)) { - $field->addItem('validation', $this->buildValidationRules($c->validation)); - } - $field->addItem('resolve', $this->buildResolve($c->resolve, $fieldConfig['validationGroups'] ?? null)); - } - - if (isset($c->deprecationReason)) { - $field->addItem('deprecationReason', $c->deprecationReason); - } - - if (isset($c->description)) { - $field->addItem('description', $c->description); - } - - if (!empty($c->args)) { - $field->addItem('args', Collection::map($c->args, [$this, 'buildArg'], false)); - } - - if (isset($c->complexity)) { - $field->addItem('complexity', $this->buildComplexity($c->complexity)); - } - - if (isset($c->public)) { - $field->addItem('public', $this->buildPublic($c->public)); - } - - if (isset($c->access)) { - $field->addItem('access', $this->buildAccess($c->access)); - } - - if (!empty($c->access) && is_string($c->access) && EL::expressionContainsVar('object', $c->access)) { - $field->addItem('useStrictAccess', false); - } - - if ('input-object' === $this->type) { - if (property_exists($c, 'defaultValue')) { - $field->addItem('defaultValue', $c->defaultValue); - } - - if (isset($c->validation)) { - $field->addItem('validation', $this->buildValidationRules($c->validation)); - } - } - - return $field; - } - - /** - * Render example: - * - * [ - * 'name' => 'username', - * 'type' => {@see buildType}, - * 'description' => 'Some fancy description.', - * 'defaultValue' => 'admin', - * ] - * - * - * @param array{ - * type: string, - * description?: string, - * defaultValue?: string - * } $argConfig - * - * @internal - * - * @throws GeneratorException - */ - public function buildArg(array $argConfig, string $argName): Collection - { - // Convert to object for better readability - $c = (object) $argConfig; - - $arg = Collection::assoc() - ->addItem('name', $argName) - ->addItem('type', $this->buildType($c->type)); - - if (isset($c->description)) { - $arg->addIfNotEmpty('description', $c->description); - } - - if (property_exists($c, 'defaultValue')) { - $arg->addItem('defaultValue', $c->defaultValue); - } - - if (!empty($c->validation)) { - if (in_array($c->type, self::BUILT_IN_TYPES) && isset($c->validation['cascade'])) { - throw new GeneratorException('Cascade validation cannot be applied to built-in types.'); - } - - $arg->addIfNotEmpty('validation', $this->buildValidationRules($c->validation)); - } - - return $arg; - } - - /** - * Builds a closure or an arrow function, depending on whether the `args` param is provided. - * - * Render example (closure): - * - * function ($value, $arguments) use ($services) { - * $args = $services->get('argumentFactory')->create($arguments); - * return ($args['age'] + 5); - * } - * - * Render example (arrow function): - * - * fn($childrenComplexity) => ($childrenComplexity + 20); - * - * @param mixed $complexity - * - * @return Closure|mixed - */ - protected function buildComplexity($complexity) - { - if (EL::isStringWithTrigger($complexity)) { - $expression = $this->expressionConverter->convert($complexity); - - if (EL::expressionContainsVar('args', $complexity)) { - return Closure::new() - ->addArgument('childrenComplexity') - ->addArgument('arguments', '', []) - ->bindVar(TypeGenerator::GRAPHQL_SERVICES) - ->append('$args = ', "$this->gqlServices->get('argumentFactory')->create(\$arguments)") - ->append('return ', $expression) - ; - } - - $arrow = ArrowFunction::new(is_string($expression) ? new Literal($expression) : $expression); - - if (EL::expressionContainsVar('childrenComplexity', $complexity)) { - $arrow->addArgument('childrenComplexity'); - } - - return $arrow; - } - - return new ArrowFunction(0); - } - - /** - * Builds an arrow function from a string with an expression prefix, - * otherwise just returns the provided value back untouched. - * - * Render example (if expression): - * - * fn($fieldName, $typeName = self::NAME) => ($fieldName == "name") - * - * @param mixed $public - * - * @return ArrowFunction|mixed - */ - protected function buildPublic($public) - { - if (EL::isStringWithTrigger($public)) { - $expression = $this->expressionConverter->convert($public); - $arrow = ArrowFunction::new(Literal::new($expression)); - - if (EL::expressionContainsVar('fieldName', $public)) { - $arrow->addArgument('fieldName'); - } - - if (EL::expressionContainsVar('typeName', $public)) { - $arrow->addArgument('fieldName'); - $arrow->addArgument('typeName', '', new Literal('self::NAME')); - } - - return $arrow; - } - - return $public; - } - - /** - * Builds an arrow function from a string with an expression prefix, - * otherwise just returns the provided value back untouched. - * - * Render example (if expression): - * - * fn($value, $args, $context, $info, $object) => $services->get('private_service')->hasAccess() - * - * @param mixed $access - * - * @return ArrowFunction|mixed - */ - protected function buildAccess($access) - { - if (EL::isStringWithTrigger($access)) { - $expression = $this->expressionConverter->convert($access); - - return ArrowFunction::new() - ->addArguments('value', 'args', 'context', 'info', 'object') - ->setExpression(Literal::new($expression)); - } - - return $access; - } - - /** - * Builds an arrow function from a string with an expression prefix, - * otherwise just returns the provided value back untouched. - * - * Render example: - * - * fn($value, $context, $info) => $services->getType($value) - * - * @param mixed $resolveType - * - * @return mixed|ArrowFunction - */ - protected function buildResolveType($resolveType) - { - if (EL::isStringWithTrigger($resolveType)) { - $expression = $this->expressionConverter->convert($resolveType); - - return ArrowFunction::new() - ->addArguments('value', 'context', 'info') - ->setExpression(Literal::new($expression)); - } - - return $resolveType; - } - - /** - * Builds an arrow function from a string with an expression prefix, - * otherwise just returns the provided value back untouched. - * - * Render example: - * - * fn($className) => (($className = "App\\ClassName") && $value instanceof $className) - * - * @param mixed $isTypeOf - */ - private function buildIsTypeOf($isTypeOf): ArrowFunction - { - if (EL::isStringWithTrigger($isTypeOf)) { - $expression = $this->expressionConverter->convert($isTypeOf); - - return ArrowFunction::new(Literal::new($expression), 'bool') - ->setStatic() - ->addArguments('value', 'context') - ->addArgument('info', ResolveInfo::class); - } - - return ArrowFunction::new($isTypeOf); - } - - /** - * Creates and array from a formatted string. - * - * Examples: - * - * "App\Entity\User::$firstName" -> ['App\Entity\User', 'firstName', 'property'] - * "App\Entity\User::firstName()" -> ['App\Entity\User', 'firstName', 'getter'] - * "App\Entity\User::firstName" -> ['App\Entity\User', 'firstName', 'member'] - */ - protected function normalizeLink(string $link): array - { - [$fqcn, $classMember] = explode('::', $link); - - if ('$' === $classMember[0]) { - return [$fqcn, ltrim($classMember, '$'), 'property']; - } elseif (')' === substr($classMember, -1)) { - return [$fqcn, rtrim($classMember, '()'), 'getter']; - } else { - return [$fqcn, $classMember, 'member']; - } + return $file; } } diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 9d8b5e4f0..9ae7da696 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -7,6 +7,7 @@ use Composer\Autoload\ClassLoader; use Overblog\GraphQLBundle\Config\Processor; use Overblog\GraphQLBundle\Event\SchemaCompiledEvent; +use Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions; use Symfony\Component\Filesystem\Filesystem; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function array_merge; @@ -25,6 +26,7 @@ class TypeGenerator public const MODE_WRITE = 4; public const MODE_OVERRIDE = 8; public const GRAPHQL_SERVICES = 'services'; + public const GRAPHQL_SERVICES_EXPR = '$' . self::GRAPHQL_SERVICES; private static bool $classMapLoaded = false; private array $typeConfigs; diff --git a/src/Generator/TypeGeneratorOptions.php b/src/Generator/TypeGeneratorOptions.php index 840593608..e0c34b4d1 100644 --- a/src/Generator/TypeGeneratorOptions.php +++ b/src/Generator/TypeGeneratorOptions.php @@ -4,51 +4,14 @@ namespace Overblog\GraphQLBundle\Generator; -class TypeGeneratorOptions -{ - /** - * PSR-4 namespace for generated GraphQL classes. - */ - public string $namespace; - - /** - * Relative path to a directory for generated GraphQL classes. - * Equals `null` unless explicitly set by user. - */ - public ?string $cacheDir; - - /** - * Permission bitmask for the directory of generated classes. - */ - public int $cacheDirMask; +use Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions as BaseTypeGeneratorOptions; - /** - * Whether a class map should be generated. - */ - public bool $useClassMap = true; +@trigger_error(sprintf('Since overblog/graphql-bundle 0.14.4: Class \Overblog\GraphQLBundle\Generator\TypeGeneratorOptions is deprecated. Use %s instead of it.', BaseTypeGeneratorOptions::class), \E_USER_DEPRECATED); - /** - * Base directory for generated classes. - */ - public ?string $cacheBaseDir; - - public function __construct( - string $namespace, - ?string $cacheDir, - bool $useClassMap = true, - ?string $cacheBaseDir = null, - ?int $cacheDirMask = null - ) { - $this->namespace = $namespace; - $this->cacheDir = $cacheDir; - $this->useClassMap = $useClassMap; - $this->cacheBaseDir = $cacheBaseDir; - - if (null === $cacheDirMask) { - // Apply permission 0777 for default cache dir otherwise apply 0775. - $cacheDirMask = null === $cacheDir ? 0777 : 0775; - } +/** + * @deprecated Use {@see \Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions } + */ +class TypeGeneratorOptions extends BaseTypeGeneratorOptions +{ - $this->cacheDirMask = $cacheDirMask; - } } diff --git a/src/Generator/ValidationRulesBuilder.php b/src/Generator/ValidationRulesBuilder.php new file mode 100644 index 000000000..0c8ab0a14 --- /dev/null +++ b/src/Generator/ValidationRulesBuilder.php @@ -0,0 +1,191 @@ + {@see normalizeLink} + * 'cascade' => [ + * 'groups' => ['my_group'], + * ], + * 'constraints' => {@see buildConstraints} + * ] + * + * If only constraints provided, uses {@see buildConstraints} directly. + * + * @param array{ + * constraints: array, + * link: string, + * cascade: array + * } $config + * + * @throws GeneratorException + */ + public function build(array $config, PhpFile $phpFile): GeneratorInterface + { + // Convert to object for better readability + $validationConfig = new ValidationConfig($config); + + $array = Collection::assoc(); + + if (!empty($validationConfig->link)) { + if (false === strpos($validationConfig->link, '::')) { + // e.g. App\Entity\Droid + $array->addItem('link', $validationConfig->link); + } else { + // e.g. App\Entity\Droid::$id + $array->addItem('link', Collection::numeric($this->normalizeLink($validationConfig->link))); + } + } + + if (isset($validationConfig->cascade)) { + // If there are only constarainst, use short syntax + if (empty($validationConfig->cascade['groups'])) { + $phpFile->addUse(InputValidator::class); + + return Literal::new('InputValidator::CASCADE'); + } + $array->addItem('cascade', $validationConfig->cascade['groups']); + } + + if (!empty($validationConfig->constraints)) { + // If there are only constarainst, use short syntax + if (0 === $array->count()) { + return $this->buildConstraints($phpFile, $validationConfig->constraints); + } + $array->addItem('constraints', $this->buildConstraints($phpFile, $validationConfig->constraints)); + } + + return $array; + } + + /** + * Builds a closure or a numeric multiline array with Symfony Constraint + * instances. The array is used by {@see InputValidator} during requests. + * + * Render example (array): + * + * [ + * new NotNull(), + * new Length([ + * 'min' => 5, + * 'max' => 10 + * ]), + * ... + * ] + * + * Render example (in a closure): + * + * fn() => [ + * new NotNull(), + * new Length([ + * 'min' => 5, + * 'max' => 10 + * ]), + * ... + * ] + * + * @throws GeneratorException + * + * @return ArrowFunction|Collection + */ + protected function buildConstraints(PhpFile $phpFile, array $constraints = [], bool $inClosure = true) + { + $result = Collection::numeric()->setMultiline(); + + foreach ($constraints as $wrapper) { + $name = key($wrapper); + $args = reset($wrapper); + + if (false !== strpos($name, '\\')) { + // Custom constraint + $fqcn = ltrim($name, '\\'); + $instance = Instance::new("@\\$fqcn"); + } else { + // Symfony constraint + $fqcn = static::CONSTRAINTS_NAMESPACE."\\$name"; + $phpFile->addUse(static::CONSTRAINTS_NAMESPACE.' as SymfonyConstraints'); + $instance = Instance::new("@SymfonyConstraints\\$name"); + } + + if (!class_exists($fqcn)) { + throw new GeneratorException("Constraint class '$fqcn' doesn't exist."); + } + + if (is_array($args)) { + if (isset($args[0]) && is_array($args[0])) { + // Nested instance + $instance->addArgument($this->buildConstraints($phpFile, $args, false)); + } elseif (isset($args['constraints'][0]) && is_array($args['constraints'][0])) { + // Nested instance with "constraints" key (full syntax) + $options = [ + 'constraints' => $this->buildConstraints($phpFile, $args['constraints'], false), + ]; + + // Check for additional options + foreach ($args as $key => $option) { + if ('constraints' === $key) { + continue; + } + $options[$key] = $option; + } + + $instance->addArgument($options); + } else { + // Numeric or Assoc array? + $instance->addArgument(isset($args[0]) ? $args : Collection::assoc($args)); + } + } elseif (null !== $args) { + $instance->addArgument($args); + } + + $result->push($instance); + } + + if ($inClosure) { + return ArrowFunction::new($result); + } + + return $result; // @phpstan-ignore-line + } + + /** + * Creates and array from a formatted string. + * + * Examples: + * + * "App\Entity\User::$firstName" -> ['App\Entity\User', 'firstName', 'property'] + * "App\Entity\User::firstName()" -> ['App\Entity\User', 'firstName', 'getter'] + * "App\Entity\User::firstName" -> ['App\Entity\User', 'firstName', 'member'] + */ + protected function normalizeLink(string $link): array + { + [$fqcn, $classMember] = explode('::', $link); + + if ('$' === $classMember[0]) { + return [$fqcn, ltrim($classMember, '$'), 'property']; + } + if (')' === substr($classMember, -1)) { + return [$fqcn, rtrim($classMember, '()'), 'getter']; + } + return [$fqcn, $classMember, 'member']; + } +} diff --git a/src/Relay/Connection/ConnectionDefinition.php b/src/Relay/Connection/ConnectionDefinition.php index 98e2ef585..e1c44565c 100644 --- a/src/Relay/Connection/ConnectionDefinition.php +++ b/src/Relay/Connection/ConnectionDefinition.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Relay\Connection; use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; +use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_merge; use function is_array; use function is_string; @@ -33,7 +34,7 @@ public function toMappingDefinition(array $config): array return [ $edgeAlias => [ - 'type' => 'object', + 'type' => TypeEnum::OBJECT, 'config' => [ 'name' => $edgeName, 'description' => 'An edge in a connection.', @@ -55,7 +56,7 @@ public function toMappingDefinition(array $config): array ], ], $connectionAlias => [ - 'type' => 'object', + 'type' => TypeEnum::OBJECT, 'config' => [ 'name' => $connectionName, 'description' => 'A connection to a list of items.', diff --git a/src/Relay/Mutation/InputDefinition.php b/src/Relay/Mutation/InputDefinition.php index 058b25a63..0ec9958e7 100644 --- a/src/Relay/Mutation/InputDefinition.php +++ b/src/Relay/Mutation/InputDefinition.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Relay\Mutation; use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; +use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_merge; use function is_array; use function preg_replace; @@ -21,7 +22,7 @@ public function toMappingDefinition(array $config): array return [ $alias => [ - 'type' => 'input-object', + 'type' => TypeEnum::INPUT_OBJECT, 'config' => [ 'name' => $name, 'fields' => array_merge( diff --git a/src/Relay/Mutation/PayloadDefinition.php b/src/Relay/Mutation/PayloadDefinition.php index 604578f1a..31ddb13d9 100644 --- a/src/Relay/Mutation/PayloadDefinition.php +++ b/src/Relay/Mutation/PayloadDefinition.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Relay\Mutation; use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; +use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_merge; use function is_array; use function preg_replace; @@ -20,7 +21,7 @@ public function toMappingDefinition(array $config): array return [ $alias => [ - 'type' => 'object', + 'type' => TypeEnum::OBJECT, 'config' => [ 'name' => $name, 'fields' => array_merge( diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 251820a79..74b4b2fbb 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -2,6 +2,15 @@ parameters: overblog_graphql_types.config: [] services: + _defaults: + autowire: true + + _instanceof: + Overblog\GraphQLBundle\Generator\ConfigBuilder\ConfigBuilderInterface: + tags: [ 'overblog_graphql.config_builder' ] + Overblog\GraphQLBundle\Generator\TypeBaseClassProvider\TypeBaseClassProviderInterface: + tags: [ 'overblog_graphql.type_base_class.provider' ] + Overblog\GraphQLBundle\Executor\Executor: ~ Overblog\GraphQLBundle\Request\Parser: ~ Overblog\GraphQLBundle\Request\BatchParser: ~ @@ -28,10 +37,7 @@ services: - '@Overblog\GraphQLBundle\Resolver\TypeResolver' - false - Overblog\GraphQLBundle\Definition\Builder\TypeFactory: - arguments: - - '@Overblog\GraphQLBundle\Definition\ConfigProcessor' - - '@Overblog\GraphQLBundle\Definition\GraphQLServices' + Overblog\GraphQLBundle\Definition\Builder\TypeFactory: ~ Overblog\GraphQLBundle\Resolver\TypeResolver: calls: @@ -60,13 +66,27 @@ services: arguments: - '@?overblog_graphql.cache_expression_language_parser' + Overblog\GraphQLBundle\Generator\: + resource: '../../Generator/*' + exclude: '../../Generator/{Collection.php,Exception,Model,TypeGeneratorOptions.php}' + + Overblog\GraphQLBundle\Generator\AwareTypeBaseClassProvider: + arguments: + $providers: !tagged_iterator 'overblog_graphql.type_base_class.provider' + + Overblog\GraphQLBundle\Generator\ConfigBuilder: + arguments: + $builders: !tagged_iterator 'overblog_graphql.config_builder' + + Overblog\GraphQLBundle\Generator\TypeBuilder: + arguments: + $namespace: '%overblog_graphql.class_namespace%' + Overblog\GraphQLBundle\Generator\TypeGenerator: arguments: - - '%overblog_graphql_types.config%' - - '@Overblog\GraphQLBundle\Generator\TypeBuilder' - - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' - - !service - class: Overblog\GraphQLBundle\Generator\TypeGeneratorOptions + $typeConfigs: '%overblog_graphql_types.config%' + $options: !service + class: Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions arguments: - '%overblog_graphql.class_namespace%' - '%overblog_graphql.cache_dir%' @@ -83,11 +103,8 @@ services: Overblog\GraphQLBundle\Controller\GraphController: public: true arguments: - - '@Overblog\GraphQLBundle\Request\BatchParser' - - '@Overblog\GraphQLBundle\Request\Executor' - - '@Overblog\GraphQLBundle\Request\Parser' - - "%overblog_graphql.handle_cors%" - - "%overblog_graphql.batching_method%" + $shouldHandleCORS: "%overblog_graphql.handle_cors%" + $graphQLBatchingMethod: "%overblog_graphql.batching_method%" Overblog\GraphQLBundle\Definition\ConfigProcessor: arguments: @@ -105,15 +122,6 @@ services: tags: - { name: overblog_graphql.service, alias: security, public: false } - Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter: - arguments: - - '@Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage' - - Overblog\GraphQLBundle\Generator\TypeBuilder: - arguments: - - '@Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter' - - '%overblog_graphql.class_namespace%' - Overblog\GraphQLBundle\Validator\InputValidatorFactory: arguments: - '@?validator.validator_factory' diff --git a/src/Validator/Mapping/ObjectMetadata.php b/src/Validator/Mapping/ObjectMetadata.php index a9e4038f6..f1c4fc45f 100644 --- a/src/Validator/Mapping/ObjectMetadata.php +++ b/src/Validator/Mapping/ObjectMetadata.php @@ -17,11 +17,9 @@ public function __construct(ValidationNode $object) } /** - * @param string $property - * * @return $this|ObjectMetadata */ - public function addPropertyConstraint($property, Constraint $constraint): self + public function addPropertyConstraint(string $property, Constraint $constraint): self { if (!isset($this->properties[$property])) { $this->properties[$property] = new PropertyMetadata($property); diff --git a/tests/Config/Parser/MetadataParser/TypeGuesser/DocBlockTypeGuesserTest.php b/tests/Config/Parser/MetadataParser/TypeGuesser/DocBlockTypeGuesserTest.php index 4003657e5..a07b77933 100644 --- a/tests/Config/Parser/MetadataParser/TypeGuesser/DocBlockTypeGuesserTest.php +++ b/tests/Config/Parser/MetadataParser/TypeGuesser/DocBlockTypeGuesserTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace Overblog\GraphQLBundle\Tests\Config\Parser; +namespace Overblog\GraphQLBundle\Tests\Config\Parser\MetadataParser\TypeGuesser; use Exception; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\ClassesTypesMap; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DocBlockTypeGuesser; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException; +use Overblog\GraphQLBundle\Tests\Config\Parser\TestCase; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; @@ -83,7 +84,7 @@ public function guessErrorDataProvider(): iterable foreach ($this->reflectors as $reflectorClass => $tag) { yield ['int|float', $reflectorClass, 'Tag @'.$tag.' found, but composite types are only allowed with null']; yield ['array', $reflectorClass, 'Tag @'.$tag.' found, but composite types in array or iterable are only allowed with null']; - yield ['UnknownClass', $reflectorClass, 'Tag @'.$tag.' found, but target object "Overblog\GraphQLBundle\Tests\Config\Parser\UnknownClass" is not a GraphQL Type class']; + yield ['UnknownClass', $reflectorClass, 'Tag @'.$tag.' found, but target object "Overblog\GraphQLBundle\Tests\Config\Parser\MetadataParser\TypeGuesser\UnknownClass" is not a GraphQL Type class']; yield ['object', $reflectorClass, 'Tag @'.$tag.' found, but type "object" is too generic']; yield ['mixed[]', $reflectorClass, 'Tag @'.$tag.' found, but the array values cannot be mixed type']; yield ['array', $reflectorClass, 'Tag @'.$tag.' found, but the array values cannot be mixed type']; diff --git a/tests/Config/Parser/MetadataParser/TypeGuesser/DoctrineTypeGuesserTest.php b/tests/Config/Parser/MetadataParser/TypeGuesser/DoctrineTypeGuesserTest.php index 5a892c87b..d34f5872f 100644 --- a/tests/Config/Parser/MetadataParser/TypeGuesser/DoctrineTypeGuesserTest.php +++ b/tests/Config/Parser/MetadataParser/TypeGuesser/DoctrineTypeGuesserTest.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace Overblog\GraphQLBundle\Tests\Config\Parser; +namespace Overblog\GraphQLBundle\Tests\Config\Parser\MetadataParser\TypeGuesser; use Doctrine\ORM\Mapping\Column; use Exception; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\ClassesTypesMap; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\DoctrineTypeGuesser; use Overblog\GraphQLBundle\Config\Parser\MetadataParser\TypeGuesser\TypeGuessingException; +use Overblog\GraphQLBundle\Tests\Config\Parser\TestCase; use ReflectionClass; class DoctrineTypeGuesserTest extends TestCase diff --git a/tests/Config/Parser/TestCase.php b/tests/Config/Parser/TestCase.php index 4cacf124c..7255d0537 100644 --- a/tests/Config/Parser/TestCase.php +++ b/tests/Config/Parser/TestCase.php @@ -12,7 +12,7 @@ abstract class TestCase extends WebTestCase { - /** @var ContainerBuilder|MockObject */ + /** @var ContainerBuilder & MockObject */ protected $containerBuilder; public function setUp(): void diff --git a/tests/DependencyInjection/Compiler/GraphQLServicesPassTest.php b/tests/DependencyInjection/Compiler/GraphQLServicesPassTest.php index d2bb5ab56..800acdb48 100644 --- a/tests/DependencyInjection/Compiler/GraphQLServicesPassTest.php +++ b/tests/DependencyInjection/Compiler/GraphQLServicesPassTest.php @@ -20,7 +20,7 @@ class GraphQLServicesPassTest extends TestCase */ public function testInvalidAlias($invalidAlias): void { - /** @var ContainerBuilder|MockObject $container */ + /** @var ContainerBuilder & MockObject $container */ $container = $this->getMockBuilder(ContainerBuilder::class) ->onlyMethods(['findTaggedServiceIds', 'findDefinition']) ->getMock(); diff --git a/tests/EventListener/TypeDecoratorListenerTest.php b/tests/EventListener/TypeDecoratorListenerTest.php index ee68200e6..6b5db9c9e 100644 --- a/tests/EventListener/TypeDecoratorListenerTest.php +++ b/tests/EventListener/TypeDecoratorListenerTest.php @@ -285,9 +285,9 @@ private function decorate(array $types, array $map): void } /** - * @return \PHPUnit\Framework\MockObject\MockObject|ResolverMap + * @return \PHPUnit\Framework\MockObject\MockObject & ResolverMap */ - private function createResolverMapMock(array $map = []) + private function createResolverMapMock(array $map = []): ResolverMap { $resolverMap = $this->getMockBuilder(ResolverMap::class)->setMethods(['map'])->getMock(); $resolverMap->expects($this->any())->method('map')->willReturn($map); diff --git a/tests/ExpressionLanguage/TestCase.php b/tests/ExpressionLanguage/TestCase.php index 723a5a23d..9adfb362a 100644 --- a/tests/ExpressionLanguage/TestCase.php +++ b/tests/ExpressionLanguage/TestCase.php @@ -112,7 +112,7 @@ protected function createGraphQLServices(array $services = []): GraphQLServices $locateableServices = [ 'typeResolver' => fn () => $this->createMock(TypeResolver::class), 'queryResolver' => fn () => $this->createMock(TypeResolver::class), - 'mutationResolver' => fn () => $$this->createMock(MutationResolver::class), + 'mutationResolver' => fn () => $this->createMock(MutationResolver::class), ]; foreach ($services as $id => $service) { diff --git a/tests/Functional/TypeShorthand/TypeShorthandTest.php b/tests/Functional/TypeShorthand/TypeShorthandTest.php index 057d73aa5..486b2cf68 100644 --- a/tests/Functional/TypeShorthand/TypeShorthandTest.php +++ b/tests/Functional/TypeShorthand/TypeShorthandTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Overblog\GraphQLBundle\Tests\Functional\AutoConfigure; +namespace Overblog\GraphQLBundle\Tests\Functional\TypeShorthand; use Doctrine\Common\Annotations\Reader; use Overblog\GraphQLBundle\Tests\Functional\TestCase; @@ -26,6 +26,6 @@ public function testQuery(): void $query = 'query { user(auth: {username: "bar", password: "baz"}) {username, address {street, zipcode}} }'; $expectedData = ['user' => ['username' => 'bar', 'address' => ['street' => 'bar foo street', 'zipcode' => '12345']]]; - $this->assertGraphQL($query, $expectedData); + static::assertGraphQL($query, $expectedData); } } diff --git a/tests/Generator/TypeGeneratorTest.php b/tests/Generator/TypeGeneratorTest.php index 61baaa258..a0e0bceb1 100644 --- a/tests/Generator/TypeGeneratorTest.php +++ b/tests/Generator/TypeGeneratorTest.php @@ -6,9 +6,9 @@ use Generator; use Overblog\GraphQLBundle\Event\SchemaCompiledEvent; +use Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions; use Overblog\GraphQLBundle\Generator\TypeBuilder; use Overblog\GraphQLBundle\Generator\TypeGenerator; -use Overblog\GraphQLBundle\Generator\TypeGeneratorOptions; use PHPUnit\Framework\TestCase; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;