From 1e0c08493bee36753686f61a21f5f1d9f96c3f06 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 11:23:15 +0300 Subject: [PATCH 01/34] Ignore IDE configs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 638ad6a42..9730d573c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea phpunit.xml /build /vendor From df90ef08a42f2e4caa5d0616320e2f258d65b88d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 11:25:56 +0300 Subject: [PATCH 02/34] Define default configs by constant instead of static property --- src/DependencyInjection/Compiler/ConfigParserPass.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 19178e0de..4e031d349 100644 --- a/src/DependencyInjection/Compiler/ConfigParserPass.php +++ b/src/DependencyInjection/Compiler/ConfigParserPass.php @@ -52,7 +52,7 @@ class ConfigParserPass implements CompilerPassInterface 'attribute' => AttributeParser::class, ]; - private static array $defaultDefaultConfig = [ + private const DEFAULT_CONFIG = [ 'definitions' => [ 'mappings' => [ 'auto_discover' => [ @@ -65,6 +65,12 @@ class ConfigParserPass implements CompilerPassInterface ], ]; + /** + * @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 = []; @@ -170,7 +176,7 @@ 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); + $config = array_replace_recursive(self::DEFAULT_CONFIG, $config); $mappingConfig = $config['definitions']['mappings']; $typesMappings = $mappingConfig['types']; From 3d62d903e96829d5fad4ed26de8ec02bb8e18e20 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 11:37:52 +0300 Subject: [PATCH 03/34] Update phpDoc --- src/DependencyInjection/Compiler/ConfigParserPass.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 4e031d349..8e1f90722 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; @@ -43,7 +44,7 @@ class ConfigParserPass implements CompilerPassInterface ]; /** - * @var array> + * @var array> */ public const PARSERS = [ 'yaml' => YamlParser::class, From 255bf3afefb10d71b8e9db2a78a04edb13b4c5d7 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 11:48:27 +0300 Subject: [PATCH 04/34] Make parsers configurable --- .../Compiler/ConfigParserPass.php | 50 +++++++++++++------ src/DependencyInjection/Configuration.php | 28 +++++++++++ 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 8e1f90722..fb15d2aec 100644 --- a/src/DependencyInjection/Compiler/ConfigParserPass.php +++ b/src/DependencyInjection/Compiler/ConfigParserPass.php @@ -36,21 +36,34 @@ 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', ]; /** + * @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 const DEFAULT_CONFIG = [ @@ -64,6 +77,7 @@ class ConfigParserPass implements CompilerPassInterface 'types' => [], ], ], + 'parsers' => self::PARSERS, ]; /** @@ -93,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 = []; @@ -103,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); @@ -122,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); } @@ -152,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); } @@ -176,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::DEFAULT_CONFIG, $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'); From 73794470d32d88cf5f6ac10a2557d824c3a62503 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 15:44:38 +0300 Subject: [PATCH 05/34] Add documentation about parsers configuration --- README.md | 1 + docs/tune_configuration.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/tune_configuration.md 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/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' + # ... +``` From 556e1fb07d56f07bdaba383d18834887a6bd6282 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 21:51:10 +0300 Subject: [PATCH 06/34] Allow plugin phpstan/extension-installer --- composer.json | 3 +++ 1 file changed, 3 insertions(+) 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 }, From ea13b32e9bcc1c269ccb90b7af75b83b8e86dd09 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 16:12:58 +0300 Subject: [PATCH 07/34] Make GraphQLParser.php easy overridable. --- src/Config/Parser/GraphQLParser.php | 43 +++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/Config/Parser/GraphQLParser.php b/src/Config/Parser/GraphQLParser.php index bf9414151..b8588ad53 100644 --- a/src/Config/Parser/GraphQLParser.php +++ b/src/Config/Parser/GraphQLParser.php @@ -8,21 +8,22 @@ use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\EnumTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\NodeKind; 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\NodeInterface; 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; @@ -57,22 +58,40 @@ public static function parse(SplFileInfo $file, ContainerBuilder $container, arr 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[$typeDef->name->value] = $config; } return $typesConfig; } + /** + * @return class-string + */ + protected static function getNodeClass(DefinitionNode $typeDef): string + { + if (isset($typeDef->kind) && array_key_exists($typeDef->kind, self::DEFINITION_TYPE_MAPPING)) { + /** + * @var class-string $class + */ + return sprintf('\\%s\\GraphQL\\ASTConverter\\%sNode', __NAMESPACE__, ucfirst(self::DEFINITION_TYPE_MAPPING[$typeDef->kind])); + } + + self::throwUnsupportedDefinitionNode($typeDef); + } + + /** + * @return array + */ + protected static function prepareConfig(DefinitionNode $typeDef): array + { + $nodeClass = static::getNodeClass($typeDef); + + return call_user_func([$nodeClass, 'toConfig'], $typeDef); + } + private static function throwUnsupportedDefinitionNode(DefinitionNode $typeDef): void { $path = explode('\\', get_class($typeDef)); From c5eb2ca48c8a82fbfcdf8f2cac1e8b6d1c40bc2d Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 25 Jul 2022 19:01:44 +0300 Subject: [PATCH 08/34] Make ObjectNode.php config parsing better overridable. --- .../GraphQL/ASTConverter/ObjectNode.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index cb3bfcc91..50d8e661b 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -5,12 +5,26 @@ namespace Overblog\GraphQLBundle\Config\Parser\GraphQL\ASTConverter; use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; class ObjectNode implements NodeInterface { protected const TYPENAME = 'object'; public static function toConfig(Node $node): array + { + return [ + 'type' => static::TYPENAME, + 'config' => static::parseConfig($node), + ]; + } + + /** + * @param ObjectTypeDefinitionNode $node + * + * @return array + */ + protected static function parseConfig(Node $node): array { $config = DescriptionNode::toConfig($node) + [ 'fields' => FieldsNode::toConfig($node), @@ -24,9 +38,6 @@ public static function toConfig(Node $node): array $config['interfaces'] = $interfaces; } - return [ - 'type' => static::TYPENAME, - 'config' => $config, - ]; + return $config; } } From 03770484d6a67b6813e8e19769392e9f9397b134 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 26 Jul 2022 11:54:28 +0300 Subject: [PATCH 09/34] Make ObjectTypeDefinition.php overridable --- src/Config/ObjectTypeDefinition.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Config/ObjectTypeDefinition.php b/src/Config/ObjectTypeDefinition.php index 8444b21ea..e47ff6113 100644 --- a/src/Config/ObjectTypeDefinition.php +++ b/src/Config/ObjectTypeDefinition.php @@ -10,9 +10,11 @@ class ObjectTypeDefinition extends TypeWithOutputFieldsDefinition { + protected const CONFIG_NAME = '_object_config'; + 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 +22,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()) From 547f00cd1aab2c91f028067adc56af1341848358 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 26 Jul 2022 12:59:54 +0300 Subject: [PATCH 10/34] Use base type enum --- .../GraphQL/ASTConverter/CustomScalarNode.php | 3 ++- .../Parser/GraphQL/ASTConverter/EnumNode.php | 3 ++- .../GraphQL/ASTConverter/InputObjectNode.php | 4 +++- .../GraphQL/ASTConverter/InterfaceNode.php | 4 +++- .../GraphQL/ASTConverter/ObjectNode.php | 3 ++- .../Parser/GraphQL/ASTConverter/UnionNode.php | 3 ++- .../Parser/MetadataParser/MetadataParser.php | 11 +++++----- src/Config/Processor/InheritanceProcessor.php | 7 ++++--- .../TypesConfiguration.php | 13 ++++++------ src/Enum/TypeEnum.php | 20 +++++++++++++++++++ src/Generator/TypeBuilder.php | 17 ++++++++-------- src/Relay/Connection/ConnectionDefinition.php | 5 +++-- src/Relay/Mutation/InputDefinition.php | 3 ++- src/Relay/Mutation/PayloadDefinition.php | 3 ++- 14 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 src/Enum/TypeEnum.php 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/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/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/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index 50d8e661b..f4acb7de6 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -6,10 +6,11 @@ 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; public static function toConfig(Node $node): array { 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/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/Processor/InheritanceProcessor.php b/src/Config/Processor/InheritanceProcessor.php index 634162e87..d0fa36a08 100644 --- a/src/Config/Processor/InheritanceProcessor.php +++ b/src/Config/Processor/InheritanceProcessor.php @@ -6,6 +6,7 @@ use Exception; use InvalidArgumentException; +use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_column; use function array_filter; use function array_flip; @@ -80,8 +81,8 @@ private static function processConfigsInherits(array $configs): array } $allowedTypes = [$config['type']]; - if ('object' === $config['type']) { - $allowedTypes[] = 'interface'; + if (TypeEnum::OBJECT === $config['type']) { + $allowedTypes[] = TypeEnum::INTERFACE; } $flattenInherits = self::flattenInherits($name, $configs, $allowedTypes); if (empty($flattenInherits)) { @@ -106,7 +107,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']); } diff --git a/src/DependencyInjection/TypesConfiguration.php b/src/DependencyInjection/TypesConfiguration.php index 329d8c7e1..73b2ebf45 100644 --- a/src/DependencyInjection/TypesConfiguration.php +++ b/src/DependencyInjection/TypesConfiguration.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\Config; use Overblog\GraphQLBundle\Config\Processor\InheritanceProcessor; +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; @@ -22,12 +23,12 @@ class TypesConfiguration implements ConfigurationInterface { 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, ]; public function getConfigTreeBuilder(): TreeBuilder 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 @@ + ObjectType::class, - 'input-object' => InputObjectType::class, - 'interface' => InterfaceType::class, - 'union' => UnionType::class, - 'enum' => EnumType::class, - 'custom-scalar' => CustomScalarType::class, + TypeEnum::OBJECT => ObjectType::class, + TypeEnum::INPUT_OBJECT => InputObjectType::class, + TypeEnum::INTERFACE => InterfaceType::class, + TypeEnum::UNION => UnionType::class, + TypeEnum::ENUM => EnumType::class, + TypeEnum::CUSTOM_SCALAR => CustomScalarType::class, ]; protected ExpressionConverter $expressionConverter; @@ -344,7 +345,7 @@ protected function buildConfig(array $config): Collection } // only by custom-scalar types - if ('custom-scalar' === $this->type) { + if (TypeEnum::CUSTOM_SCALAR === $this->type) { if (isset($c->scalarType)) { $configLoader->addItem('scalarType', $c->scalarType); } @@ -755,7 +756,7 @@ public function buildField(array $fieldConfig, string $fieldname) $field->addItem('useStrictAccess', false); } - if ('input-object' === $this->type) { + if (TypeEnum::INPUT_OBJECT === $this->type) { if (property_exists($c, 'defaultValue')) { $field->addItem('defaultValue', $c->defaultValue); } 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( From 51ca4613ca7c330bfdcaee333d790ede8b2bbdcf Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 26 Jul 2022 13:21:03 +0300 Subject: [PATCH 11/34] Configure services autowiring --- src/Resources/config/services.yaml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 251820a79..92946c1a7 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -2,6 +2,9 @@ parameters: overblog_graphql_types.config: [] services: + _defaults: + autowire: true + Overblog\GraphQLBundle\Executor\Executor: ~ Overblog\GraphQLBundle\Request\Parser: ~ Overblog\GraphQLBundle\Request\BatchParser: ~ @@ -28,10 +31,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: @@ -62,10 +62,8 @@ services: Overblog\GraphQLBundle\Generator\TypeGenerator: arguments: - - '%overblog_graphql_types.config%' - - '@Overblog\GraphQLBundle\Generator\TypeBuilder' - - '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' - - !service + $typeConfigs: '%overblog_graphql_types.config%' + $options: !service class: Overblog\GraphQLBundle\Generator\TypeGeneratorOptions arguments: - '%overblog_graphql.class_namespace%' @@ -83,11 +81,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,14 +100,11 @@ services: tags: - { name: overblog_graphql.service, alias: security, public: false } - Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter: - arguments: - - '@Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage' + Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter: ~ Overblog\GraphQLBundle\Generator\TypeBuilder: arguments: - - '@Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter' - - '%overblog_graphql.class_namespace%' + $namespace: '%overblog_graphql.class_namespace%' Overblog\GraphQLBundle\Validator\InputValidatorFactory: arguments: From 624fa15eb5ce9610473ba6e6c2c90403246e8729 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 26 Jul 2022 13:49:10 +0300 Subject: [PATCH 12/34] Make TypeBuilder extendable --- src/Generator/AwareTypeBaseClassProvider.php | 41 +++++++++++++++++++ .../CustomScalarTypeBaseClassProvider.php | 21 ++++++++++ .../EnumTypeBaseClassProvider.php | 21 ++++++++++ .../InputObjectTypeBaseClassProvider.php | 21 ++++++++++ .../InterfaceTypeBaseClassProvider.php | 21 ++++++++++ .../ObjectTypeBaseClassProvider.php | 21 ++++++++++ .../TypeBaseClassProviderInterface.php | 17 ++++++++ .../UnionTypeBaseClassProvider.php | 21 ++++++++++ src/Generator/TypeBuilder.php | 22 ++-------- src/Resources/config/services.yaml | 11 +++++ 10 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 src/Generator/AwareTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/CustomScalarTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/EnumTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/InputObjectTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/InterfaceTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/ObjectTypeBaseClassProvider.php create mode 100644 src/Generator/TypeBaseClassProvider/TypeBaseClassProviderInterface.php create mode 100644 src/Generator/TypeBaseClassProvider/UnionTypeBaseClassProvider.php 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/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, - TypeEnum::INPUT_OBJECT => InputObjectType::class, - TypeEnum::INTERFACE => InterfaceType::class, - TypeEnum::UNION => UnionType::class, - TypeEnum::ENUM => EnumType::class, - TypeEnum::CUSTOM_SCALAR => CustomScalarType::class, - ]; - + protected AwareTypeBaseClassProvider $baseClassProvider; protected ExpressionConverter $expressionConverter; protected PhpFile $file; protected string $namespace; @@ -83,8 +68,9 @@ class TypeBuilder protected string $currentField; protected string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; - public function __construct(ExpressionConverter $expressionConverter, string $namespace) + public function __construct(AwareTypeBaseClassProvider $baseClassProvider, ExpressionConverter $expressionConverter, string $namespace) { + $this->baseClassProvider = $baseClassProvider; $this->expressionConverter = $expressionConverter; $this->namespace = $namespace; @@ -120,7 +106,7 @@ public function build(array $config, string $type): PhpFile $class = $this->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); diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 92946c1a7..82071a987 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -5,6 +5,10 @@ services: _defaults: autowire: true + _instanceof: + Overblog\GraphQLBundle\Generator\TypeBaseClassProvider\TypeBaseClassProviderInterface: + tags: [ 'overblog_graphql.type_base_class.provider' ] + Overblog\GraphQLBundle\Executor\Executor: ~ Overblog\GraphQLBundle\Request\Parser: ~ Overblog\GraphQLBundle\Request\BatchParser: ~ @@ -106,6 +110,13 @@ services: arguments: $namespace: '%overblog_graphql.class_namespace%' + Overblog\GraphQLBundle\Generator\AwareTypeBaseClassProvider: + arguments: + $providers: !tagged_iterator 'overblog_graphql.type_base_class.provider' + + Overblog\GraphQLBundle\Generator\TypeBaseClassProvider\: + resource: '../../Generator/TypeBaseClassProvider/*' + Overblog\GraphQLBundle\Validator\InputValidatorFactory: arguments: - '@?validator.validator_factory' From 38b706e26e7c4038456f54d2a80dca05f1f13996 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Tue, 26 Jul 2022 14:46:24 +0300 Subject: [PATCH 13/34] Make TypesConfiguration and InheritanceProcessor extendable (dirty quick solution) --- src/Config/CustomScalarTypeDefinition.php | 9 ++- src/Config/EnumTypeDefinition.php | 9 ++- src/Config/InputObjectTypeDefinition.php | 9 ++- src/Config/InterfaceTypeDefinition.php | 9 ++- src/Config/ObjectTypeDefinition.php | 7 ++- .../GraphQL/ASTConverter/NodeInterface.php | 3 + src/Config/PermittedInheritTypeProvider.php | 34 +++++++++++ src/Config/Processor/InheritanceProcessor.php | 35 +++++++++-- src/Config/TypeDefinition.php | 2 + src/Config/UnionTypeDefinition.php | 9 ++- .../TypesConfiguration.php | 59 +++++++++++++++---- 11 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 src/Config/PermittedInheritTypeProvider.php 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 e47ff6113..836dfc3af 100644 --- a/src/Config/ObjectTypeDefinition.php +++ b/src/Config/ObjectTypeDefinition.php @@ -10,7 +10,12 @@ class ObjectTypeDefinition extends TypeWithOutputFieldsDefinition { - protected const CONFIG_NAME = '_object_config'; + public const CONFIG_NAME = '_object_config'; + + public static function getName(): string + { + return static::CONFIG_NAME; + } public function getDefinition(): ArrayNodeDefinition { 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/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 d0fa36a08..05505c3c0 100644 --- a/src/Config/Processor/InheritanceProcessor.php +++ b/src/Config/Processor/InheritanceProcessor.php @@ -6,6 +6,7 @@ use Exception; use InvalidArgumentException; +use Overblog\GraphQLBundle\Config\PermittedInheritTypeProvider; use Overblog\GraphQLBundle\Enum\TypeEnum; use function array_column; use function array_filter; @@ -29,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); @@ -80,10 +99,7 @@ private static function processConfigsInherits(array $configs): array continue; } - $allowedTypes = [$config['type']]; - if (TypeEnum::OBJECT === $config['type']) { - $allowedTypes[] = TypeEnum::INTERFACE; - } + $allowedTypes = self::getAllowedTypes($config['type']); $flattenInherits = self::flattenInherits($name, $configs, $allowedTypes); if (empty($flattenInherits)) { continue; @@ -198,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/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/DependencyInjection/TypesConfiguration.php b/src/DependencyInjection/TypesConfiguration.php index 73b2ebf45..09ef2d8d7 100644 --- a/src/DependencyInjection/TypesConfiguration.php +++ b/src/DependencyInjection/TypesConfiguration.php @@ -6,6 +6,7 @@ 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; @@ -22,6 +23,23 @@ 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 = [ TypeEnum::OBJECT, TypeEnum::ENUM, @@ -31,6 +49,22 @@ class TypesConfiguration implements ConfigurationInterface 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'); @@ -43,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) { @@ -81,8 +117,9 @@ public function getConfigTreeBuilder(): TreeBuilder return $v; }) ->end() - ->cannotBeOverwritten() - ->children() + ->cannotBeOverwritten(); + $prototypeChildren = $prototype->children(); + $prototypeChildren ->scalarNode('class_name') ->isRequired() ->validate() @@ -95,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 From 1a3665ee56485b884d1991aa268ea0866adb7454 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 27 Jul 2022 16:14:49 +0300 Subject: [PATCH 14/34] Split TypeBuilder for more flexibility & extensibility --- src/ExpressionLanguage/ExpressionFunction.php | 3 + src/Generator/ConfigBuilder.php | 117 +++ .../ConfigBuilder/ConfigBuilderInterface.php | 14 + .../CustomScalarTypeFieldsBuilder.php | 78 ++ .../ConfigBuilder/DescriptionBuilder.php | 19 + src/Generator/ConfigBuilder/FieldsBuilder.php | 362 +++++++ .../ConfigBuilder/InterfacesBuilder.php | 27 + .../ConfigBuilder/IsTypeOfBuilder.php | 55 ++ src/Generator/ConfigBuilder/NameBuilder.php | 18 + .../ConfigBuilder/ResolveFieldBuilder.php | 27 + .../ConfigBuilder/ResolveTypeBuilder.php | 55 ++ src/Generator/ConfigBuilder/TypesBuilder.php | 27 + .../ConfigBuilder/ValidationBuilder.php | 28 + src/Generator/ConfigBuilder/ValuesBuilder.php | 20 + src/Generator/Model/AbstractConfig.php | 59 ++ src/Generator/Model/ArgumentConfig.php | 31 + src/Generator/Model/FieldConfig.php | 39 + src/Generator/Model/TypeConfig.php | 49 + src/Generator/Model/ValidationConfig.php | 14 + src/Generator/ResolveInstructionBuilder.php | 146 +++ src/Generator/TypeBuilder.php | 912 +----------------- src/Generator/ValidationRulesBuilder.php | 190 ++++ src/Resources/config/services.yaml | 12 + 23 files changed, 1408 insertions(+), 894 deletions(-) create mode 100644 src/Generator/ConfigBuilder.php create mode 100644 src/Generator/ConfigBuilder/ConfigBuilderInterface.php create mode 100644 src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php create mode 100644 src/Generator/ConfigBuilder/DescriptionBuilder.php create mode 100644 src/Generator/ConfigBuilder/FieldsBuilder.php create mode 100644 src/Generator/ConfigBuilder/InterfacesBuilder.php create mode 100644 src/Generator/ConfigBuilder/IsTypeOfBuilder.php create mode 100644 src/Generator/ConfigBuilder/NameBuilder.php create mode 100644 src/Generator/ConfigBuilder/ResolveFieldBuilder.php create mode 100644 src/Generator/ConfigBuilder/ResolveTypeBuilder.php create mode 100644 src/Generator/ConfigBuilder/TypesBuilder.php create mode 100644 src/Generator/ConfigBuilder/ValidationBuilder.php create mode 100644 src/Generator/ConfigBuilder/ValuesBuilder.php create mode 100644 src/Generator/Model/AbstractConfig.php create mode 100644 src/Generator/Model/ArgumentConfig.php create mode 100644 src/Generator/Model/FieldConfig.php create mode 100644 src/Generator/Model/TypeConfig.php create mode 100644 src/Generator/Model/ValidationConfig.php create mode 100644 src/Generator/ResolveInstructionBuilder.php create mode 100644 src/Generator/ValidationRulesBuilder.php diff --git a/src/ExpressionLanguage/ExpressionFunction.php b/src/ExpressionLanguage/ExpressionFunction.php index 06b9610ff..092e98770 100644 --- a/src/ExpressionLanguage/ExpressionFunction.php +++ b/src/ExpressionLanguage/ExpressionFunction.php @@ -10,6 +10,9 @@ class ExpressionFunction extends BaseExpressionFunction { + /** + * TODO: use single source for all usages (create a provider). + */ protected string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; public function __construct(string $name, callable $compiler, ?callable $evaluator = null) diff --git a/src/Generator/ConfigBuilder.php b/src/Generator/ConfigBuilder.php new file mode 100644 index 000000000..48df0bf68 --- /dev/null +++ b/src/Generator/ConfigBuilder.php @@ -0,0 +1,117 @@ + + */ + 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..625ae5e31 --- /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_string($callback)) { + [$class, $method] = $callback; + } else { + [$class, $method] = explode('::', $callback); + } + + $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..83d571c01 --- /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..da35cba5c --- /dev/null +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -0,0 +1,362 @@ +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}, + * ] + * + * + * @return GeneratorInterface|Collection|string + * + * @throws GeneratorException + * + * @internal + */ + protected function buildField(FieldConfig $fieldConfig, TypeConfig $typeConfig, PhpFile $phpFile): MurtucovCollection + { + // 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 (array_key_exists('defaultValue', $fieldConfig)) { + $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 (array_key_exists('defaultValue', $argConfig)) { + $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)) { + 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 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; + $type = "$this->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..2819994c7 --- /dev/null +++ b/src/Generator/ConfigBuilder/InterfacesBuilder.php @@ -0,0 +1,27 @@ +interfaces) && !empty($typeConfig->interfaces)) { + $items = array_map(fn ($type) => "$this->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..c3de109b4 --- /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..4f2422c3d --- /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..d88dbf153 --- /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..a79a135a2 --- /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..14949c301 --- /dev/null +++ b/src/Generator/ConfigBuilder/TypesBuilder.php @@ -0,0 +1,27 @@ +types) && !empty($typeConfig->types)) { + $items = array_map(fn ($type) => "$this->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..d83f4dd51 --- /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..2292996d8 --- /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/FieldConfig.php b/src/Generator/Model/FieldConfig.php new file mode 100644 index 000000000..21ca9d49f --- /dev/null +++ b/src/Generator/Model/FieldConfig.php @@ -0,0 +1,39 @@ + $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/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)); + } + + $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. + */ + 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/TypeBuilder.php b/src/Generator/TypeBuilder.php index f9f324798..ef9049f4a 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -4,43 +4,16 @@ namespace Overblog\GraphQLBundle\Generator; -use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\Parser; -use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Type\Definition\Type; -use Murtukov\PHPCodeGenerator\ArrowFunction; -use Murtukov\PHPCodeGenerator\Closure; use Murtukov\PHPCodeGenerator\Config; use Murtukov\PHPCodeGenerator\ConverterInterface; -use Murtukov\PHPCodeGenerator\GeneratorInterface; -use Murtukov\PHPCodeGenerator\Instance; -use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; -use Murtukov\PHPCodeGenerator\Utils; use Overblog\GraphQLBundle\Definition\ConfigProcessor; use Overblog\GraphQLBundle\Definition\GraphQLServices; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; use Overblog\GraphQLBundle\Definition\Type\GeneratedTypeInterface; -use Overblog\GraphQLBundle\Enum\TypeEnum; -use Overblog\GraphQLBundle\Error\ResolveErrors; -use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; -use Overblog\GraphQLBundle\Validator\InputValidator; -use function array_map; -use function class_exists; -use function explode; -use function in_array; -use function is_array; -use function is_callable; -use function is_string; -use function key; -use function ltrim; -use function reset; -use function rtrim; -use function strpos; -use function strtolower; -use function substr; +use Overblog\GraphQLBundle\Generator\Model\TypeConfig; /** * Service that exposes a single method `build` called for each GraphQL @@ -55,22 +28,25 @@ */ class TypeBuilder { - protected const CONSTRAINTS_NAMESPACE = 'Symfony\Component\Validator\Constraints'; + /** + * @deprecated Use {@see ValidationRulesBuilder::CONSTRAINTS_NAMESPACE } + */ + public const CONSTRAINTS_NAMESPACE = ValidationRulesBuilder::CONSTRAINTS_NAMESPACE; protected const DOCBLOCK_TEXT = 'THIS FILE WAS GENERATED AND SHOULD NOT BE EDITED MANUALLY.'; - protected const BUILT_IN_TYPES = [Type::STRING, Type::INT, Type::FLOAT, Type::BOOLEAN, Type::ID]; 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(AwareTypeBaseClassProvider $baseClassProvider, 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; @@ -98,13 +74,10 @@ public function __construct(AwareTypeBaseClassProvider $baseClassProvider, Expre */ public function build(array $config, string $type): PhpFile { - // This values should be accessible from every method - $this->config = $config; - $this->type = $type; + $typeConfig = new TypeConfig($config, $type); + $file = PhpFile::new()->setNamespace($this->namespace); - $this->file = PhpFile::new()->setNamespace($this->namespace); - - $class = $this->file->createClass($config['class_name']) + $class = $file->createClass($config['class_name']) ->setFinal() ->setExtends($this->baseClassProvider->getFQCN($type)) ->addImplements(GeneratedTypeInterface::class, AliasedInterface::class) @@ -116,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))'); @@ -126,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 (TypeEnum::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 (TypeEnum::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/ValidationRulesBuilder.php b/src/Generator/ValidationRulesBuilder.php new file mode 100644 index 000000000..151dd4e28 --- /dev/null +++ b/src/Generator/ValidationRulesBuilder.php @@ -0,0 +1,190 @@ + {@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/Resources/config/services.yaml b/src/Resources/config/services.yaml index 82071a987..1b9c685e1 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -6,6 +6,8 @@ services: 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' ] @@ -104,6 +106,13 @@ services: tags: - { name: overblog_graphql.service, alias: security, public: false } + Overblog\GraphQLBundle\Generator\ConfigBuilder\: + resource: '../../Generator/ConfigBuilder/*' + + Overblog\GraphQLBundle\Generator\ConfigBuilder: + arguments: + $builders: !tagged_iterator 'overblog_graphql.config_builder' + Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter: ~ Overblog\GraphQLBundle\Generator\TypeBuilder: @@ -117,6 +126,9 @@ services: Overblog\GraphQLBundle\Generator\TypeBaseClassProvider\: resource: '../../Generator/TypeBaseClassProvider/*' + Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder: ~ + Overblog\GraphQLBundle\Generator\ValidationRulesBuilder: ~ + Overblog\GraphQLBundle\Validator\InputValidatorFactory: arguments: - '@?validator.validator_factory' From 9b13b9b14f3e06ed96ef9da505adb66c555ae673 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 27 Jul 2022 16:36:14 +0300 Subject: [PATCH 15/34] Use single source for all consumers of "GQL services" expression part --- src/ExpressionLanguage/ExpressionFunction.php | 5 +---- src/Generator/ConfigBuilder/FieldsBuilder.php | 12 +++++------- src/Generator/ConfigBuilder/InterfacesBuilder.php | 8 ++------ src/Generator/ConfigBuilder/TypesBuilder.php | 8 ++------ src/Generator/ResolveInstructionBuilder.php | 8 ++------ src/Generator/TypeGenerator.php | 1 + 6 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/ExpressionLanguage/ExpressionFunction.php b/src/ExpressionLanguage/ExpressionFunction.php index 092e98770..2c79af7d7 100644 --- a/src/ExpressionLanguage/ExpressionFunction.php +++ b/src/ExpressionLanguage/ExpressionFunction.php @@ -10,10 +10,7 @@ class ExpressionFunction extends BaseExpressionFunction { - /** - * TODO: use single source for all usages (create a provider). - */ - 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/ConfigBuilder/FieldsBuilder.php b/src/Generator/ConfigBuilder/FieldsBuilder.php index da35cba5c..7f9e70dd4 100644 --- a/src/Generator/ConfigBuilder/FieldsBuilder.php +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -33,11 +33,6 @@ class FieldsBuilder implements ConfigBuilderInterface protected ResolveInstructionBuilder $resolveInstructionBuilder; protected ValidationRulesBuilder $validationRulesBuilder; - /** - * TODO: use single source for all usages (create a provider). - */ - protected string $gqlServices = '$' . TypeGenerator::GRAPHQL_SERVICES; - public function __construct( ExpressionConverter $expressionConverter, ResolveInstructionBuilder $resolveInstructionBuilder, @@ -241,11 +236,13 @@ protected function buildComplexity($complexity): GeneratorInterface $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 = ', "$this->gqlServices->get('argumentFactory')->create(\$arguments)") + ->append('$args = ', "{$gqlServices}->get('argumentFactory')->create(\$arguments)") ->append('return ', $expression); } @@ -351,7 +348,8 @@ protected function wrapTypeRecursive($typeNode, bool &$isReference, PhpFile $php $phpFile->addUse(Type::class); } else { $name = $typeNode->name->value; - $type = "$this->gqlServices->getType('$name')"; + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $type = "{$gqlServices}->getType('$name')"; $isReference = true; } break; diff --git a/src/Generator/ConfigBuilder/InterfacesBuilder.php b/src/Generator/ConfigBuilder/InterfacesBuilder.php index 2819994c7..d61cec63c 100644 --- a/src/Generator/ConfigBuilder/InterfacesBuilder.php +++ b/src/Generator/ConfigBuilder/InterfacesBuilder.php @@ -12,15 +12,11 @@ class InterfacesBuilder implements ConfigBuilderInterface { - /** - * TODO: use single source for all usages (create a provider). - */ - protected string $gqlServices = '$' . TypeGenerator::GRAPHQL_SERVICES; - public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void { if (isset($typeConfig->interfaces) && !empty($typeConfig->interfaces)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $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/TypesBuilder.php b/src/Generator/ConfigBuilder/TypesBuilder.php index 14949c301..ab97f75ac 100644 --- a/src/Generator/ConfigBuilder/TypesBuilder.php +++ b/src/Generator/ConfigBuilder/TypesBuilder.php @@ -12,15 +12,11 @@ class TypesBuilder implements ConfigBuilderInterface { - /** - * TODO: use single source for all usages (create a provider). - */ - protected string $gqlServices = '$' . TypeGenerator::GRAPHQL_SERVICES; - public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpFile): void { if (isset($typeConfig->types) && !empty($typeConfig->types)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $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/ResolveInstructionBuilder.php b/src/Generator/ResolveInstructionBuilder.php index 40c8961cb..95cafbd33 100644 --- a/src/Generator/ResolveInstructionBuilder.php +++ b/src/Generator/ResolveInstructionBuilder.php @@ -18,11 +18,6 @@ class ResolveInstructionBuilder { protected ExpressionConverter $expressionConverter; - /** - * TODO: use single source for all usages (create a provider). - */ - protected string $gqlServices = '$' . TypeGenerator::GRAPHQL_SERVICES; - public function __construct(ExpressionConverter $expressionConverter) { $this->expressionConverter = $expressionConverter; @@ -90,7 +85,8 @@ public function build(TypeConfig $typeConfig, $resolve, ?string $currentField = $closure->append('$errors = ', Instance::new(ResolveErrors::class)); } - $closure->append('$validator = ', "$this->gqlServices->createInputValidator(...func_get_args())"); + $gqlServices = TypeGenerator::GRAPHQL_SERVICES_EXPR; + $closure->append('$validator = ', "{$gqlServices}->createInputValidator(...func_get_args())"); // If auto-validation on or errors are injected if (!$injectValidator || $injectErrors) { diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 9d8b5e4f0..464aef85e 100644 --- a/src/Generator/TypeGenerator.php +++ b/src/Generator/TypeGenerator.php @@ -25,6 +25,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; From 7da17db43a3d2b61c6edbdd016ac59a19a01249a Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Wed, 27 Jul 2022 16:43:43 +0300 Subject: [PATCH 16/34] Simplify generators services definition --- src/Resources/config/services.yaml | 39 ++++++++++++------------------ 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 1b9c685e1..6bcb5a7ca 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -66,6 +66,22 @@ 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: $typeConfigs: '%overblog_graphql_types.config%' @@ -106,29 +122,6 @@ services: tags: - { name: overblog_graphql.service, alias: security, public: false } - Overblog\GraphQLBundle\Generator\ConfigBuilder\: - resource: '../../Generator/ConfigBuilder/*' - - Overblog\GraphQLBundle\Generator\ConfigBuilder: - arguments: - $builders: !tagged_iterator 'overblog_graphql.config_builder' - - Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter: ~ - - Overblog\GraphQLBundle\Generator\TypeBuilder: - arguments: - $namespace: '%overblog_graphql.class_namespace%' - - Overblog\GraphQLBundle\Generator\AwareTypeBaseClassProvider: - arguments: - $providers: !tagged_iterator 'overblog_graphql.type_base_class.provider' - - Overblog\GraphQLBundle\Generator\TypeBaseClassProvider\: - resource: '../../Generator/TypeBaseClassProvider/*' - - Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder: ~ - Overblog\GraphQLBundle\Generator\ValidationRulesBuilder: ~ - Overblog\GraphQLBundle\Validator\InputValidatorFactory: arguments: - '@?validator.validator_factory' From b531be40aa5b109f87abe2680e7b6a3d1d82c807 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 09:12:05 +0300 Subject: [PATCH 17/34] Fix misprint in tests --- tests/ExpressionLanguage/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 5c70a146fa75d9b6326b7164ccbafad70299625a Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 09:17:14 +0300 Subject: [PATCH 18/34] Fix parsing of callback of custom scalar type --- .../ConfigBuilder/CustomScalarTypeFieldsBuilder.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php b/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php index 49f69ab08..258d02696 100644 --- a/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php +++ b/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php @@ -55,10 +55,12 @@ protected function buildScalarCallback($callback, string $fieldName, TypeConfig $closure = new ArrowFunction(); - if (!is_string($callback)) { + if (\is_array($callback)) { [$class, $method] = $callback; - } else { + } elseif(\is_string($callback)) { [$class, $method] = explode('::', $callback); + } else { + throw new GeneratorException(sprintf('Invalid type of "%s" value passed.', $fieldName)); } $className = Utils::resolveQualifier($class); From e81e44d3c3ad1a7c676d6fee447cb7394142adce Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 09:23:11 +0300 Subject: [PATCH 19/34] Move TypeGeneratorOptions to the Model namespace For further simplifying of dependency injection --- UPGRADE.md | 2 +- src/Generator/{ => Model}/TypeGeneratorOptions.php | 2 +- src/Generator/TypeGenerator.php | 1 + src/Resources/config/services.yaml | 2 +- tests/Generator/TypeGeneratorTest.php | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) rename src/Generator/{ => Model}/TypeGeneratorOptions.php (96%) 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/src/Generator/TypeGeneratorOptions.php b/src/Generator/Model/TypeGeneratorOptions.php similarity index 96% rename from src/Generator/TypeGeneratorOptions.php rename to src/Generator/Model/TypeGeneratorOptions.php index 840593608..d237eda4f 100644 --- a/src/Generator/TypeGeneratorOptions.php +++ b/src/Generator/Model/TypeGeneratorOptions.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Overblog\GraphQLBundle\Generator; +namespace Overblog\GraphQLBundle\Generator\Model; class TypeGeneratorOptions { diff --git a/src/Generator/TypeGenerator.php b/src/Generator/TypeGenerator.php index 464aef85e..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; diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 6bcb5a7ca..74b4b2fbb 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -86,7 +86,7 @@ services: arguments: $typeConfigs: '%overblog_graphql_types.config%' $options: !service - class: Overblog\GraphQLBundle\Generator\TypeGeneratorOptions + class: Overblog\GraphQLBundle\Generator\Model\TypeGeneratorOptions arguments: - '%overblog_graphql.class_namespace%' - '%overblog_graphql.cache_dir%' 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; From e1dc5a10cb6799e44183c3d413828ce1827b5c5b Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 09:27:23 +0300 Subject: [PATCH 20/34] Move generator's Collection to the Model namespace For further simplifying of dependency injection --- src/Generator/ConfigBuilder.php | 1 + src/Generator/ConfigBuilder/ConfigBuilderInterface.php | 2 +- src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php | 2 +- src/Generator/ConfigBuilder/DescriptionBuilder.php | 2 +- src/Generator/ConfigBuilder/FieldsBuilder.php | 2 +- src/Generator/ConfigBuilder/InterfacesBuilder.php | 2 +- src/Generator/ConfigBuilder/IsTypeOfBuilder.php | 2 +- src/Generator/ConfigBuilder/NameBuilder.php | 2 +- src/Generator/ConfigBuilder/ResolveFieldBuilder.php | 2 +- src/Generator/ConfigBuilder/ResolveTypeBuilder.php | 2 +- src/Generator/ConfigBuilder/TypesBuilder.php | 2 +- src/Generator/ConfigBuilder/ValidationBuilder.php | 2 +- src/Generator/ConfigBuilder/ValuesBuilder.php | 2 +- src/Generator/{ => Model}/Collection.php | 2 +- src/Generator/ResolveInstructionBuilder.php | 1 + src/Generator/ValidationRulesBuilder.php | 1 + 16 files changed, 16 insertions(+), 13 deletions(-) rename src/Generator/{ => Model}/Collection.php (89%) diff --git a/src/Generator/ConfigBuilder.php b/src/Generator/ConfigBuilder.php index 48df0bf68..dd13fee86 100644 --- a/src/Generator/ConfigBuilder.php +++ b/src/Generator/ConfigBuilder.php @@ -6,6 +6,7 @@ use Murtukov\PHPCodeGenerator\PhpFile; use Overblog\GraphQLBundle\Generator\ConfigBuilder\ConfigBuilderInterface; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class ConfigBuilder diff --git a/src/Generator/ConfigBuilder/ConfigBuilderInterface.php b/src/Generator/ConfigBuilder/ConfigBuilderInterface.php index 625ae5e31..d85f44fe5 100644 --- a/src/Generator/ConfigBuilder/ConfigBuilderInterface.php +++ b/src/Generator/ConfigBuilder/ConfigBuilderInterface.php @@ -5,7 +5,7 @@ namespace Overblog\GraphQLBundle\Generator\ConfigBuilder; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; interface ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php b/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php index 258d02696..2859ed97a 100644 --- a/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php +++ b/src/Generator/ConfigBuilder/CustomScalarTypeFieldsBuilder.php @@ -8,8 +8,8 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Murtukov\PHPCodeGenerator\Utils; -use Overblog\GraphQLBundle\Generator\Collection; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class CustomScalarTypeFieldsBuilder implements ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/DescriptionBuilder.php b/src/Generator/ConfigBuilder/DescriptionBuilder.php index 83d571c01..1b3fe318b 100644 --- a/src/Generator/ConfigBuilder/DescriptionBuilder.php +++ b/src/Generator/ConfigBuilder/DescriptionBuilder.php @@ -5,7 +5,7 @@ namespace Overblog\GraphQLBundle\Generator\ConfigBuilder; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class DescriptionBuilder implements ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/FieldsBuilder.php b/src/Generator/ConfigBuilder/FieldsBuilder.php index 7f9e70dd4..5a51ad714 100644 --- a/src/Generator/ConfigBuilder/FieldsBuilder.php +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -15,10 +15,10 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; -use Overblog\GraphQLBundle\Generator\Collection; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; use Overblog\GraphQLBundle\Generator\Model\ArgumentConfig; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\FieldConfig; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; use Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder; diff --git a/src/Generator/ConfigBuilder/InterfacesBuilder.php b/src/Generator/ConfigBuilder/InterfacesBuilder.php index d61cec63c..ac6a5607d 100644 --- a/src/Generator/ConfigBuilder/InterfacesBuilder.php +++ b/src/Generator/ConfigBuilder/InterfacesBuilder.php @@ -6,7 +6,7 @@ use Murtukov\PHPCodeGenerator\ArrowFunction; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; use Overblog\GraphQLBundle\Generator\TypeGenerator; diff --git a/src/Generator/ConfigBuilder/IsTypeOfBuilder.php b/src/Generator/ConfigBuilder/IsTypeOfBuilder.php index c3de109b4..d923df7a8 100644 --- a/src/Generator/ConfigBuilder/IsTypeOfBuilder.php +++ b/src/Generator/ConfigBuilder/IsTypeOfBuilder.php @@ -9,8 +9,8 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; -use Overblog\GraphQLBundle\Generator\Collection; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class IsTypeOfBuilder implements ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/NameBuilder.php b/src/Generator/ConfigBuilder/NameBuilder.php index 4f2422c3d..d738a2a97 100644 --- a/src/Generator/ConfigBuilder/NameBuilder.php +++ b/src/Generator/ConfigBuilder/NameBuilder.php @@ -6,7 +6,7 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class NameBuilder implements ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/ResolveFieldBuilder.php b/src/Generator/ConfigBuilder/ResolveFieldBuilder.php index d88dbf153..edcd6aae5 100644 --- a/src/Generator/ConfigBuilder/ResolveFieldBuilder.php +++ b/src/Generator/ConfigBuilder/ResolveFieldBuilder.php @@ -5,7 +5,7 @@ namespace Overblog\GraphQLBundle\Generator\ConfigBuilder; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; use Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder; diff --git a/src/Generator/ConfigBuilder/ResolveTypeBuilder.php b/src/Generator/ConfigBuilder/ResolveTypeBuilder.php index a79a135a2..2f6c33ed3 100644 --- a/src/Generator/ConfigBuilder/ResolveTypeBuilder.php +++ b/src/Generator/ConfigBuilder/ResolveTypeBuilder.php @@ -8,8 +8,8 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; -use Overblog\GraphQLBundle\Generator\Collection; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class ResolveTypeBuilder implements ConfigBuilderInterface diff --git a/src/Generator/ConfigBuilder/TypesBuilder.php b/src/Generator/ConfigBuilder/TypesBuilder.php index ab97f75ac..16d68263f 100644 --- a/src/Generator/ConfigBuilder/TypesBuilder.php +++ b/src/Generator/ConfigBuilder/TypesBuilder.php @@ -6,7 +6,7 @@ use Murtukov\PHPCodeGenerator\ArrowFunction; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; use Overblog\GraphQLBundle\Generator\TypeGenerator; diff --git a/src/Generator/ConfigBuilder/ValidationBuilder.php b/src/Generator/ConfigBuilder/ValidationBuilder.php index d83f4dd51..ebddc18d1 100644 --- a/src/Generator/ConfigBuilder/ValidationBuilder.php +++ b/src/Generator/ConfigBuilder/ValidationBuilder.php @@ -5,7 +5,7 @@ namespace Overblog\GraphQLBundle\Generator\ConfigBuilder; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; use Overblog\GraphQLBundle\Generator\ValidationRulesBuilder; diff --git a/src/Generator/ConfigBuilder/ValuesBuilder.php b/src/Generator/ConfigBuilder/ValuesBuilder.php index 2292996d8..30e47fe7e 100644 --- a/src/Generator/ConfigBuilder/ValuesBuilder.php +++ b/src/Generator/ConfigBuilder/ValuesBuilder.php @@ -5,7 +5,7 @@ namespace Overblog\GraphQLBundle\Generator\ConfigBuilder; use Murtukov\PHPCodeGenerator\PhpFile; -use Overblog\GraphQLBundle\Generator\Collection; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class ValuesBuilder implements ConfigBuilderInterface diff --git a/src/Generator/Collection.php b/src/Generator/Model/Collection.php similarity index 89% rename from src/Generator/Collection.php rename to src/Generator/Model/Collection.php index 9b861be3b..df5209f4f 100644 --- a/src/Generator/Collection.php +++ b/src/Generator/Model/Collection.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Overblog\GraphQLBundle\Generator; +namespace Overblog\GraphQLBundle\Generator\Model; use Murtukov\PHPCodeGenerator\Collection as BaseCollection; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; diff --git a/src/Generator/ResolveInstructionBuilder.php b/src/Generator/ResolveInstructionBuilder.php index 95cafbd33..4145ff877 100644 --- a/src/Generator/ResolveInstructionBuilder.php +++ b/src/Generator/ResolveInstructionBuilder.php @@ -12,6 +12,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\TypeConfig; class ResolveInstructionBuilder diff --git a/src/Generator/ValidationRulesBuilder.php b/src/Generator/ValidationRulesBuilder.php index 151dd4e28..0c8ab0a14 100644 --- a/src/Generator/ValidationRulesBuilder.php +++ b/src/Generator/ValidationRulesBuilder.php @@ -10,6 +10,7 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; +use Overblog\GraphQLBundle\Generator\Model\Collection; use Overblog\GraphQLBundle\Generator\Model\ValidationConfig; use Overblog\GraphQLBundle\Validator\InputValidator; From f00368dce5a05c975e64b1a0bca9a067ed3c929c Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 09:37:56 +0300 Subject: [PATCH 21/34] Add stubs instead of moved models for backward compatibility --- src/Generator/Collection.php | 17 +++++++++++++++++ src/Generator/TypeGeneratorOptions.php | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Generator/Collection.php create mode 100644 src/Generator/TypeGeneratorOptions.php diff --git a/src/Generator/Collection.php b/src/Generator/Collection.php new file mode 100644 index 000000000..deb4236d0 --- /dev/null +++ b/src/Generator/Collection.php @@ -0,0 +1,17 @@ + Date: Thu, 28 Jul 2022 17:44:34 +0300 Subject: [PATCH 22/34] Add phpDoc --- src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index f4acb7de6..7eac83013 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -12,6 +12,11 @@ class ObjectNode implements NodeInterface { protected const TYPENAME = TypeEnum::OBJECT; + /** + * @param ObjectTypeDefinitionNode $node + * + * @return array + */ public static function toConfig(Node $node): array { return [ From 23e557014664c93bf9ee6c9be97af602ca662cfb Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 28 Jul 2022 17:48:12 +0300 Subject: [PATCH 23/34] Describe returned data type --- src/Definition/Resolver/AliasedInterface.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Definition/Resolver/AliasedInterface.php b/src/Definition/Resolver/AliasedInterface.php index feed83ba0..9ff2695a8 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; } From 98ea0b9d285b75c1c7093c470f692dc380dfc959 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 29 Jul 2022 11:39:37 +0300 Subject: [PATCH 24/34] Simplify extending of fields definition --- .../Parser/GraphQL/ASTConverter/FieldsNode.php | 17 +++++++++++++++-- .../Parser/GraphQL/ASTConverter/ObjectNode.php | 14 +++++++++++--- src/Config/TypeWithOutputFieldsDefinition.php | 8 ++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) 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/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index 7eac83013..ce156fb6f 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -32,9 +32,7 @@ public static function toConfig(Node $node): array */ protected static function parseConfig(Node $node): array { - $config = DescriptionNode::toConfig($node) + [ - 'fields' => FieldsNode::toConfig($node), - ]; + $config = DescriptionNode::toConfig($node) + static::parseFields($node); if (!empty($node->interfaces)) { $interfaces = []; @@ -46,4 +44,14 @@ protected static function parseConfig(Node $node): array return $config; } + + /** + * @return array{fields: array } + */ + protected static function parseFields(Node $node): array + { + return [ + 'fields' => FieldsNode::toConfig($node), + ]; + } } 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 + { + + } } From a69993b6fd26ac44ba31ea4faf5eba7af37849e4 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 1 Aug 2022 14:37:12 +0300 Subject: [PATCH 25/34] Extract parsing of interfaces --- .../GraphQL/ASTConverter/ObjectNode.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php index ce156fb6f..1e010ef5c 100644 --- a/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php +++ b/src/Config/Parser/GraphQL/ASTConverter/ObjectNode.php @@ -33,14 +33,7 @@ public static function toConfig(Node $node): array protected static function parseConfig(Node $node): array { $config = DescriptionNode::toConfig($node) + static::parseFields($node); - - if (!empty($node->interfaces)) { - $interfaces = []; - foreach ($node->interfaces as $interface) { - $interfaces[] = TypeNode::astTypeNodeToString($interface); - } - $config['interfaces'] = $interfaces; - } + $config += static::parseInterfaces($node); return $config; } @@ -54,4 +47,21 @@ protected static function parseFields(Node $node): array 'fields' => FieldsNode::toConfig($node), ]; } + + /** + * @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); + } + $config['interfaces'] = $interfaces; + } + + return $config; + } } From beaba147967817085d0f03bf95294be9a3846d4f Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 11:26:35 +0300 Subject: [PATCH 26/34] Fix code style --- src/Config/Parser/GraphQLParser.php | 58 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/Config/Parser/GraphQLParser.php b/src/Config/Parser/GraphQLParser.php index b8588ad53..2df688630 100644 --- a/src/Config/Parser/GraphQLParser.php +++ b/src/Config/Parser/GraphQLParser.php @@ -14,30 +14,34 @@ 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_pop; -use function call_user_func; use function explode; use function file_get_contents; -use function get_class; 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 @@ -67,19 +71,28 @@ public static function parse(SplFileInfo $file, ContainerBuilder $container, arr return $typesConfig; } + protected static function createUnsupportedDefinitionNodeException(DefinitionNode $typeDef): 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, self::DEFINITION_TYPE_MAPPING)) { - /** - * @var class-string $class - */ - return sprintf('\\%s\\GraphQL\\ASTConverter\\%sNode', __NAMESPACE__, ucfirst(self::DEFINITION_TYPE_MAPPING[$typeDef->kind])); + if (isset($typeDef->kind) && \array_key_exists($typeDef->kind, static::DEFINITION_TYPE_MAPPING)) { + return static::DEFINITION_TYPE_MAPPING[$typeDef->kind]; } - self::throwUnsupportedDefinitionNode($typeDef); + throw static::createUnsupportedDefinitionNodeException($typeDef); } /** @@ -89,17 +102,6 @@ protected static function prepareConfig(DefinitionNode $typeDef): array { $nodeClass = static::getNodeClass($typeDef); - return call_user_func([$nodeClass, 'toConfig'], $typeDef); - } - - private static function throwUnsupportedDefinitionNode(DefinitionNode $typeDef): void - { - $path = explode('\\', get_class($typeDef)); - throw new InvalidArgumentException( - sprintf( - '%s definition is not supported right now.', - preg_replace('@DefinitionNode$@', '', array_pop($path)) - ) - ); + return \call_user_func([$nodeClass, 'toConfig'], $typeDef); } } From 0f0a096a4232d66b9226a2494ddf90ba45478205 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 13:10:22 +0300 Subject: [PATCH 27/34] Add phpDoc describing data types --- src/Config/Parser/ParserInterface.php | 5 +++++ src/Definition/Builder/TypeFactory.php | 3 +++ 2 files changed, 8 insertions(+) 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/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); From e748140e29d1cdcd03a5e90d4421818b23845882 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 18:11:24 +0300 Subject: [PATCH 28/34] Make code better extendable --- src/Config/Parser/GraphQLParser.php | 53 ++++++++++++---- src/Definition/Builder/SchemaBuilder.php | 79 +++++++++++++++++++----- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/Config/Parser/GraphQLParser.php b/src/Config/Parser/GraphQLParser.php index 2df688630..b3a389e13 100644 --- a/src/Config/Parser/GraphQLParser.php +++ b/src/Config/Parser/GraphQLParser.php @@ -6,10 +6,12 @@ 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; @@ -47,25 +49,24 @@ class GraphQLParser implements ParserInterface 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|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|UnionTypeDefinitionNode|InputObjectTypeDefinitionNode|ScalarTypeDefinitionNode $typeDef */ $config = static::prepareConfig($typeDef); - $typesConfig[$typeDef->name->value] = $config; + $typesConfig[static::getTypeDefinitionName($typeDef, $nameGeneratorBag)] = $config; } return $typesConfig; @@ -95,6 +96,18 @@ protected static function getNodeClass(DefinitionNode $typeDef): string 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 */ @@ -104,4 +117,20 @@ protected static function prepareConfig(DefinitionNode $typeDef): array 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/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index ec278e13c..e97d3303c 100644 --- a/src/Definition/Builder/SchemaBuilder.php +++ b/src/Definition/Builder/SchemaBuilder.php @@ -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): Closure + { + 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): Closure + { + 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; } } From a6c2a1817f7fd249819ad08320c3118efbf89b39 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 20:23:01 +0300 Subject: [PATCH 29/34] Fix tests namespaces --- .../MetadataParser/TypeGuesser/DocBlockTypeGuesserTest.php | 5 +++-- .../MetadataParser/TypeGuesser/DoctrineTypeGuesserTest.php | 3 ++- tests/Functional/TypeShorthand/TypeShorthandTest.php | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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/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); } } From 47fd0270d9ae12fdc6033f9a3fac73803f3a51f8 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 4 Aug 2022 20:51:18 +0300 Subject: [PATCH 30/34] Work around of parsing of "description" field of type extension nodes --- .../Parser/GraphQL/ASTConverter/DescriptionNode.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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; } } From 515f86f294700d048263f19d0939f2392eec4448 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 5 Aug 2022 15:44:34 +0300 Subject: [PATCH 31/34] Fix PHPStan errors --- phpstan-baseline.neon | 268 ++---------------- src/Definition/Resolver/AliasedInterface.php | 2 +- src/Error/UserErrors.php | 16 +- src/Generator/ConfigBuilder/FieldsBuilder.php | 12 +- src/Validator/Mapping/ObjectMetadata.php | 4 +- tests/Config/Parser/TestCase.php | 2 +- .../Compiler/GraphQLServicesPassTest.php | 2 +- .../TypeDecoratorListenerTest.php | 4 +- 8 files changed, 46 insertions(+), 264 deletions(-) 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/Definition/Resolver/AliasedInterface.php b/src/Definition/Resolver/AliasedInterface.php index 9ff2695a8..e6cde401d 100644 --- a/src/Definition/Resolver/AliasedInterface.php +++ b/src/Definition/Resolver/AliasedInterface.php @@ -12,7 +12,7 @@ interface AliasedInterface * For instance: * array('myMethod' => 'myAlias') * - * @return array + * @return array */ public static function getAliases(): array; } diff --git a/src/Error/UserErrors.php b/src/Error/UserErrors.php index edc8fb9ec..e007a105d 100644 --- a/src/Error/UserErrors.php +++ b/src/Error/UserErrors.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Error; use Exception; +use GraphQL\Error\UserError as WebonyxUserError; use InvalidArgumentException; use RuntimeException; use function is_object; @@ -13,9 +14,12 @@ class UserErrors extends RuntimeException { - /** @var UserError[] */ + /** @var WebonyxUserError[] */ private array $errors = []; + /** + * @param array $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/Generator/ConfigBuilder/FieldsBuilder.php b/src/Generator/ConfigBuilder/FieldsBuilder.php index 5a51ad714..a31c12988 100644 --- a/src/Generator/ConfigBuilder/FieldsBuilder.php +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -10,7 +10,6 @@ use GraphQL\Type\Definition\Type; use Murtukov\PHPCodeGenerator\ArrowFunction; use Murtukov\PHPCodeGenerator\Closure; -use Murtukov\PHPCodeGenerator\Collection as MurtucovCollection; use Murtukov\PHPCodeGenerator\GeneratorInterface; use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; @@ -24,6 +23,9 @@ use Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder; use Overblog\GraphQLBundle\Generator\TypeGenerator; use Overblog\GraphQLBundle\Generator\ValidationRulesBuilder; +use function array_key_exists; +use function in_array; +use function is_string; class FieldsBuilder implements ConfigBuilderInterface { @@ -71,15 +73,13 @@ public function build(TypeConfig $typeConfig, Collection $builder, PhpFile $phpF * 'resolve' => {@see \Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder::build()}, * 'complexity' => {@see buildComplexity}, * ] - * - * - * @return GeneratorInterface|Collection|string + * . * * @throws GeneratorException * * @internal */ - protected function buildField(FieldConfig $fieldConfig, TypeConfig $typeConfig, PhpFile $phpFile): MurtucovCollection + 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 @@ -179,7 +179,7 @@ protected function buildAccess($access) * 'description' => 'Some fancy description.', * 'defaultValue' => 'admin', * ] - * + * . * * @throws GeneratorException * 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/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); From cf99c9b737ba16b7a5c168670faff4d501377ff8 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Fri, 5 Aug 2022 20:32:21 +0300 Subject: [PATCH 32/34] Fix extendability of schema builder --- src/Definition/Builder/SchemaBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index e97d3303c..da912e454 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) { From b19d6de046d34eae602d5569b5975899b1f67755 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 8 Aug 2022 14:07:16 +0300 Subject: [PATCH 33/34] Change return type from \Closure to callable --- src/Definition/Builder/SchemaBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index da912e454..14f11e5b4 100644 --- a/src/Definition/Builder/SchemaBuilder.php +++ b/src/Definition/Builder/SchemaBuilder.php @@ -92,7 +92,7 @@ protected function buildSchemaArguments( ]; } - protected function createTypeLoaderClosure(string $schemaName): Closure + protected function createTypeLoaderClosure(string $schemaName): callable { return function ($name) use ($schemaName): ?Type { $this->typeResolver->setCurrentSchemaName($schemaName); @@ -104,7 +104,7 @@ protected function createTypeLoaderClosure(string $schemaName): Closure /** * @param string[] $types */ - protected function createTypesClosure(string $schemaName, array $types): Closure + protected function createTypesClosure(string $schemaName, array $types): callable { return function () use ($types, $schemaName): array { $this->typeResolver->setCurrentSchemaName($schemaName); From 3b251834b431028cee51f92e6007f95ed8b32298 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Mon, 8 Aug 2022 14:48:54 +0300 Subject: [PATCH 34/34] Fix using of array_key_exists with \ArrayObject --- src/Generator/ConfigBuilder/FieldsBuilder.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Generator/ConfigBuilder/FieldsBuilder.php b/src/Generator/ConfigBuilder/FieldsBuilder.php index a31c12988..46300f450 100644 --- a/src/Generator/ConfigBuilder/FieldsBuilder.php +++ b/src/Generator/ConfigBuilder/FieldsBuilder.php @@ -23,7 +23,6 @@ use Overblog\GraphQLBundle\Generator\ResolveInstructionBuilder; use Overblog\GraphQLBundle\Generator\TypeGenerator; use Overblog\GraphQLBundle\Generator\ValidationRulesBuilder; -use function array_key_exists; use function in_array; use function is_string; @@ -132,7 +131,7 @@ protected function buildField(FieldConfig $fieldConfig, TypeConfig $typeConfig, } if ($typeConfig->isInputObject()) { - if (array_key_exists('defaultValue', $fieldConfig)) { + if ($fieldConfig->offsetExists('defaultValue')) { $field->addItem('defaultValue', $fieldConfig->defaultValue); } @@ -197,7 +196,7 @@ protected function buildArg(ArgumentConfig $argConfig, PhpFile $phpFile): Collec $arg->addIfNotEmpty('description', $argConfig->description); } - if (array_key_exists('defaultValue', $argConfig)) { + if ($argConfig->offsetExists('defaultValue')) { $arg->addItem('defaultValue', $argConfig->defaultValue); }