From 9319b1d51d8d7dbb3ca6a3c259f223c3cac018e2 Mon Sep 17 00:00:00 2001 From: CocoJr Date: Wed, 20 Jun 2018 10:48:17 +0200 Subject: [PATCH 1/2] Create annotation parser --- Annotation/AbstractGraphQLControl.php | 23 + Annotation/AbstractGraphQLRelation.php | 30 ++ Annotation/GraphQLAccessControl.php | 17 + Annotation/GraphQLAlias.php | 24 + Annotation/GraphQLAnnotation.php | 19 + Annotation/GraphQLColumn.php | 31 ++ Annotation/GraphQLDescription.php | 24 + Annotation/GraphQLInputArgs.php | 37 ++ Annotation/GraphQLMutation.php | 27 + Annotation/GraphQLPublicControl.php | 17 + Annotation/GraphQLQuery.php | 32 ++ Annotation/GraphQLRelayMutation.php | 26 + Annotation/GraphQLResolver.php | 21 + Annotation/GraphQLToMany.php | 18 + Annotation/GraphQLToOne.php | 18 + Annotation/GraphQLType.php | 24 + Config/Parser/AnnotationParser.php | 498 ++++++++++++++++++ Config/Parser/YamlParser.php | 1 + Config/Parser/debug.yml.log | 204 +++++++ .../OverblogGraphQLTypesExtension.php | 9 +- Resources/doc/definitions/mutation.md | 8 +- Resources/doc/definitions/resolver.md | 2 +- Resources/doc/definitions/schema.md | 41 ++ Resources/doc/definitions/type-system/enum.md | 30 ++ .../doc/definitions/type-system/index.md | 2 +- .../definitions/type-system/input-object.md | 18 + .../doc/definitions/type-system/interface.md | 39 ++ .../doc/definitions/type-system/lists.md | 18 +- .../doc/definitions/type-system/non-null.md | 16 + .../doc/definitions/type-system/scalars.md | 52 ++ Resources/doc/index.md | 4 + .../doc/security/fields-access-control.md | 54 ++ .../doc/security/fields-public-control.md | 29 + 33 files changed, 1408 insertions(+), 5 deletions(-) create mode 100644 Annotation/AbstractGraphQLControl.php create mode 100644 Annotation/AbstractGraphQLRelation.php create mode 100644 Annotation/GraphQLAccessControl.php create mode 100644 Annotation/GraphQLAlias.php create mode 100644 Annotation/GraphQLAnnotation.php create mode 100644 Annotation/GraphQLColumn.php create mode 100644 Annotation/GraphQLDescription.php create mode 100644 Annotation/GraphQLInputArgs.php create mode 100644 Annotation/GraphQLMutation.php create mode 100644 Annotation/GraphQLPublicControl.php create mode 100644 Annotation/GraphQLQuery.php create mode 100644 Annotation/GraphQLRelayMutation.php create mode 100644 Annotation/GraphQLResolver.php create mode 100644 Annotation/GraphQLToMany.php create mode 100644 Annotation/GraphQLToOne.php create mode 100644 Annotation/GraphQLType.php create mode 100644 Config/Parser/AnnotationParser.php create mode 100644 Config/Parser/debug.yml.log diff --git a/Annotation/AbstractGraphQLControl.php b/Annotation/AbstractGraphQLControl.php new file mode 100644 index 000000000..6cc7c1fe2 --- /dev/null +++ b/Annotation/AbstractGraphQLControl.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql control + * + * @Annotation + * @Target("PROPERTY") + */ +abstract class AbstractGraphQLControl +{ + /** + * Access control access name + * + * @var string + */ + public $method; +} \ No newline at end of file diff --git a/Annotation/AbstractGraphQLRelation.php b/Annotation/AbstractGraphQLRelation.php new file mode 100644 index 000000000..0254b6b13 --- /dev/null +++ b/Annotation/AbstractGraphQLRelation.php @@ -0,0 +1,30 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type relation + * + * @Annotation + * @Target("PROPERTY") + */ +abstract class AbstractGraphQLRelation +{ + /** + * Type + * + * @var string + */ + public $target; + + /** + * Is nullable? + * + * @var bool + */ + public $nullable; +} \ No newline at end of file diff --git a/Annotation/GraphQLAccessControl.php b/Annotation/GraphQLAccessControl.php new file mode 100644 index 000000000..a7e845f8b --- /dev/null +++ b/Annotation/GraphQLAccessControl.php @@ -0,0 +1,17 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql access control + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLAccessControl extends AbstractGraphQLControl +{ +} \ No newline at end of file diff --git a/Annotation/GraphQLAlias.php b/Annotation/GraphQLAlias.php new file mode 100644 index 000000000..04ec46d4c --- /dev/null +++ b/Annotation/GraphQLAlias.php @@ -0,0 +1,24 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("CLASS") + */ +final class GraphQLAlias +{ + /** + * Type + * + * @var string + */ + public $name; +} \ No newline at end of file diff --git a/Annotation/GraphQLAnnotation.php b/Annotation/GraphQLAnnotation.php new file mode 100644 index 000000000..3f8fd0c5c --- /dev/null +++ b/Annotation/GraphQLAnnotation.php @@ -0,0 +1,19 @@ + + * @copyright 2018 Thibault Colette + */ + +require_once __DIR__.'/GraphQLAccessControl.php'; +require_once __DIR__.'/GraphQLAlias.php'; +require_once __DIR__.'/GraphQLColumn.php'; +require_once __DIR__.'/GraphQLDescription.php'; +require_once __DIR__.'/GraphQLInputArgs.php'; +require_once __DIR__.'/GraphQLMutation.php'; +require_once __DIR__.'/GraphQLPublicControl.php'; +require_once __DIR__.'/GraphQLQuery.php'; +require_once __DIR__.'/GraphQLRelayMutation.php'; +require_once __DIR__.'/GraphQLResolver.php'; +require_once __DIR__.'/GraphQLToMany.php'; +require_once __DIR__.'/GraphQLToOne.php'; +require_once __DIR__.'/GraphQLType.php'; diff --git a/Annotation/GraphQLColumn.php b/Annotation/GraphQLColumn.php new file mode 100644 index 000000000..6b2f6c425 --- /dev/null +++ b/Annotation/GraphQLColumn.php @@ -0,0 +1,31 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLColumn +{ + /** + * Type + * + * @var string + */ + public $type; + + /** + * Is nullable? + * + * @var bool + */ + public $nullable; +} \ No newline at end of file diff --git a/Annotation/GraphQLDescription.php b/Annotation/GraphQLDescription.php new file mode 100644 index 000000000..4cf859ccf --- /dev/null +++ b/Annotation/GraphQLDescription.php @@ -0,0 +1,24 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target({"CLASS", "PROPERTY"}) + */ +final class GraphQLDescription +{ + /** + * Type + * + * @var string + */ + public $description; +} \ No newline at end of file diff --git a/Annotation/GraphQLInputArgs.php b/Annotation/GraphQLInputArgs.php new file mode 100644 index 000000000..8b9df4b76 --- /dev/null +++ b/Annotation/GraphQLInputArgs.php @@ -0,0 +1,37 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +class GraphQLInputArgs +{ + /** + * @var string + */ + public $name; + + /** + * @var string + */ + public $target; + + /** + * @var string + */ + public $type; + + /** + * @var string + */ + public $description; +} \ No newline at end of file diff --git a/Annotation/GraphQLMutation.php b/Annotation/GraphQLMutation.php new file mode 100644 index 000000000..404bd771b --- /dev/null +++ b/Annotation/GraphQLMutation.php @@ -0,0 +1,27 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +class GraphQLMutation +{ + /** + * @var string + */ + public $method; + + /** + * @var array + */ + public $args; +} \ No newline at end of file diff --git a/Annotation/GraphQLPublicControl.php b/Annotation/GraphQLPublicControl.php new file mode 100644 index 000000000..0d1acacfc --- /dev/null +++ b/Annotation/GraphQLPublicControl.php @@ -0,0 +1,17 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql access control + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLPublicControl extends AbstractGraphQLControl +{ +} \ No newline at end of file diff --git a/Annotation/GraphQLQuery.php b/Annotation/GraphQLQuery.php new file mode 100644 index 000000000..cadb95b84 --- /dev/null +++ b/Annotation/GraphQLQuery.php @@ -0,0 +1,32 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +class GraphQLQuery +{ + /** + * @var string + */ + public $method; + + /** + * @var array + */ + public $args; + + /** + * @var string + */ + public $type; +} \ No newline at end of file diff --git a/Annotation/GraphQLRelayMutation.php b/Annotation/GraphQLRelayMutation.php new file mode 100644 index 000000000..1eaa4a03f --- /dev/null +++ b/Annotation/GraphQLRelayMutation.php @@ -0,0 +1,26 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql access control + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLRelayMutation extends GraphQLMutation +{ + /** + * @var string The input graphql related type + */ + public $input; + + /** + * @var string The payload graphql related type + */ + public $payload; +} \ No newline at end of file diff --git a/Annotation/GraphQLResolver.php b/Annotation/GraphQLResolver.php new file mode 100644 index 000000000..a41121205 --- /dev/null +++ b/Annotation/GraphQLResolver.php @@ -0,0 +1,21 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql access control + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLResolver +{ + /** + * @var string + */ + public $resolve; +} \ No newline at end of file diff --git a/Annotation/GraphQLToMany.php b/Annotation/GraphQLToMany.php new file mode 100644 index 000000000..8d822f8f5 --- /dev/null +++ b/Annotation/GraphQLToMany.php @@ -0,0 +1,18 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLToMany extends AbstractGraphQLRelation +{ +} \ No newline at end of file diff --git a/Annotation/GraphQLToOne.php b/Annotation/GraphQLToOne.php new file mode 100644 index 000000000..0c76beb90 --- /dev/null +++ b/Annotation/GraphQLToOne.php @@ -0,0 +1,18 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type. + * Use it if you don't use Doctrine ORM + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLToOne extends AbstractGraphQLRelation +{ +} \ No newline at end of file diff --git a/Annotation/GraphQLType.php b/Annotation/GraphQLType.php new file mode 100644 index 000000000..0edfeda38 --- /dev/null +++ b/Annotation/GraphQLType.php @@ -0,0 +1,24 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("CLASS") + */ +final class GraphQLType +{ + /** + * Type + * + * @var string + */ + public $type; +} \ No newline at end of file diff --git a/Config/Parser/AnnotationParser.php b/Config/Parser/AnnotationParser.php new file mode 100644 index 000000000..da1752926 --- /dev/null +++ b/Config/Parser/AnnotationParser.php @@ -0,0 +1,498 @@ +addResource(new FileResource($file->getRealPath())); + try { + $fileContent = file_get_contents($file->getRealPath()); + + $entityName = substr($file->getFilename(), 0, -4); + if (preg_match('#namespace (.+);#', $fileContent, $namespace)) { + $className = $namespace[1] . '\\' . $entityName; + } else { + $className = $entityName; + } + + $reflexionEntity = new \ReflectionClass($className); + + $annotations = $reader->getClassAnnotations($reflexionEntity); + $annotations = self::parseAnnotation($annotations); + + $alias = self::getGraphQLAlias($annotations) ?: $entityName; + $type = self::getGraphQLType($annotations); + + switch ($type) { + case 'enum': + return self::formatEnumType($alias, $entityName, $reflexionEntity->getProperties()); + case 'custom-scalar': + return self::formatCustomScalarType($alias, $type, $className); + default: + return self::formatScalarType($alias, $type, $entityName, $reflexionEntity->getProperties()); + } + } catch (\InvalidArgumentException $e) { + throw new InvalidArgumentException(sprintf('Unable to parse file "%s".', $file), $e->getCode(), $e); + } + } + + /** + * Get the graphQL alias + * + * @param $annotation + * + * @return string|null + */ + protected static function getGraphQLAlias($annotation) + { + if (array_key_exists('GraphQLAlias', $annotation) && !empty($annotation['GraphQLAlias']['name'])) { + return $annotation['GraphQLAlias']['name']; + } + + return null; + } + + /** + * Get the graphQL type + * + * @param $annotation + * + * @return string + */ + protected static function getGraphQLType($annotation) + { + if (array_key_exists('GraphQLType', $annotation) && !empty($annotation['GraphQLType']['type'])) { + return $annotation['GraphQLType']['type']; + } + + return 'object'; + } + + /** + * Format enum type + * + * @param string $alias + * @param string $entityName + * @param \ReflectionProperty[] $properties + * + * @return array + */ + protected static function formatEnumType($alias, $entityName, $properties) + { + $reader = self::getAnnotationReader(); + + $typesConfig = [ + $alias => [ + 'type' => 'enum', + 'config' => [ + 'description' => $entityName . ' type', + ], + ] + ]; + + $values = []; + /** @var \ReflectionProperty $property */ + foreach ($properties as $property) { + $propertyName = $property->getName(); + + $propertyAnnotation = $reader->getPropertyAnnotations($property); + $propertyAnnotation = self::parseAnnotation($propertyAnnotation); + + $values[$propertyName] = [ + 'value' => $propertyAnnotation, + ]; + + if (array_key_exists('GraphQLDescription', $propertyAnnotation) && !empty($test['GraphQLDescription']['description'])) { + $values[$propertyName]['description'] = $test['GraphQLDescription']['description']; + } + } + + $typesConfig[$alias]['config']['values'] = $values; + + return $typesConfig; + } + + /** + * Format custom scalar type + * + * @param string $alias + * @param string $type + * @param string $className + * + * @return array + */ + protected static function formatCustomScalarType($alias, $type, $className) + { + $config = [ + 'serialize' => [$className, 'serialize'], + 'parseValue' => [$className, 'parseValue'], + 'parseLiteral' => [$className, 'parseLiteral'], + ]; + + return [ + $alias => [ + 'type' => $type, + 'config' => $config, + ] + ]; + } + + /** + * Format scalar type + * + * @param string $alias + * @param string $type + * @param string $entityName + * @param \ReflectionProperty[] $properties + * + * @return array + */ + protected static function formatScalarType($alias, $type, $entityName, $properties) + { + $reader = self::getAnnotationReader(); + + $typesConfig = [ + $alias => [ + 'type' => $type, + 'config' => [ + 'description' => $entityName . ' type', + 'fields' => [], + ], + ] + ]; + + foreach ($properties as $property) { + $propertyName = $property->getName(); + $propertyAnnotation = $reader->getPropertyAnnotations($property); + $propertyAnnotation = self::parseAnnotation($propertyAnnotation); + + if (!$graphQlType = self::getGraphQLFieldType($propertyName, $propertyAnnotation)) { + continue; + } + + if ($graphQlAccessControl = self::getGraphQLAccessControl($propertyAnnotation)) { + $graphQlType['access'] = $graphQlAccessControl; + } + + if ($graphQlPublicControl = self::getGraphQLPublicControl($propertyAnnotation)) { + $graphQlType['public'] = $graphQlPublicControl; + } + + $typesConfig[$alias]['config']['fields'][$propertyName] = $graphQlType; + } + + return empty($typesConfig[$alias]['config']['fields']) + ? [] + : $typesConfig; + } + + /** + * Return the graphQL type for the named field + * + * @param string $name + * @param array $annotation + * + * @return array|null + */ + protected static function getGraphQLFieldType($name, $annotation) + { + if (!$type = self::getGraphQLScalarFieldType($name, $annotation)) { + if (!$type = self::getGraphQLQueryField($annotation)) { + if (!$type = self::getGraphQLMutationField($annotation)) { + return null; + } + } + } + + return $type; + } + + /** + * Return the common field type, like ID, Int, String, and other user-created type + * + * @param string $name + * @param array $annotation + * + * @return array|null + */ + protected static function getGraphQLScalarFieldType($name, $annotation) + { + // Get the current type, depending on current annotation + $type = $graphQLType = null; + $nullable = $isMultiple = false; + if (array_key_exists('GraphQLColumn', $annotation) && array_key_exists('type', $annotation['GraphQLColumn'])) { + $annotation = $annotation['GraphQLColumn']; + $type = $annotation['type']; + } elseif (array_key_exists('GraphQLToMany', $annotation) && array_key_exists('target', $annotation['GraphQLToMany'])) { + $annotation = $annotation['GraphQLToMany']; + $type = $annotation['target']; + $isMultiple = $nullable = true; + } elseif (array_key_exists('GraphQLToOne', $annotation) && array_key_exists('target', $annotation['GraphQLToOne'])) { + $annotation = $annotation['GraphQLToOne']; + $type = $annotation['target']; + $nullable = true; + } elseif (array_key_exists('OneToMany', $annotation) && array_key_exists('targetEntity', $annotation['OneToMany'])) { + $annotation = $annotation['OneToMany']; + $type = $annotation['targetEntity']; + $isMultiple = $nullable = true; + } elseif (array_key_exists('OneToOne', $annotation) && array_key_exists('targetEntity', $annotation['OneToOne'])) { + $annotation = $annotation['OneToOne']; + $type = $annotation['targetEntity']; + $nullable = true; + } elseif (array_key_exists('ManyToMany', $annotation) && array_key_exists('targetEntity', $annotation['ManyToMany'])) { + $annotation = $annotation['ManyToMany']; + $type = $annotation['targetEntity']; + $isMultiple = $nullable = true; + } elseif (array_key_exists('ManyToOne', $annotation) && array_key_exists('targetEntity', $annotation['ManyToOne'])) { + $annotation = $annotation['ManyToOne']; + $type = $annotation['targetEntity']; + $nullable = true; + } elseif (array_key_exists('Column', $annotation) && array_key_exists('type', $annotation['Column'])) { + $annotation = $annotation['Column']; + $type = $annotation['type']; + } + + if (!$type) { + return null; + } + + if (array_key_exists('nullable', $annotation)) { + $nullable = $annotation['nullable'] == 'true' + ? true + : false; + } + + $type = explode('\\', $type); + $type = $type[count($type)-1]; + + // Get the graphQL type representation + // Specific case for ID and relation + if ($name === 'id' && $type === 'integer') { + $graphQLType = 'ID'; + } else { + // Make the relation between doctrine Column type and graphQL type + switch ($type) { + case 'integer'; + $graphQLType = 'Int'; + break; + case 'string': + case 'text': + $graphQLType = 'String'; + break; + case 'bool': + case 'boolean': + $graphQLType = 'Boolean'; + break; + case 'float': + $graphQLType = 'Float'; + break; + default: + // No maching: considering is custom-scalar graphQL type + $graphQLType = $type; + } + } + + if ($isMultiple) { + $graphQLType = '['.$graphQLType.']'; + } + + if (!$nullable) { + $graphQLType .= '!'; + } + + return ['type' => $graphQLType]; + } + + /** + * Get the graphql query formatted field + * + * @param array $annotation + * + * @return array|null + */ + protected static function getGraphQLQueryField($annotation) + { + if (!array_key_exists('GraphQLQuery', $annotation)) { + return null; + } + + $annotationQuery = $annotation['GraphQLQuery']; + + $ret = [ + 'type' => $annotationQuery['type'], + ]; + + $method = $annotationQuery['method']; + $args = $queryArgs = []; + if (array_key_exists('GraphQLInputArgs', $annotation)) { + $annotationArgs = $annotation['GraphQLInputArgs']; + if (!array_key_exists(0, $annotationArgs)) { + $annotationArgs = [$annotationArgs]; + } + + foreach ($annotationArgs as $arg) { + $args[$arg['name']] = [ + 'type' => $arg['type'], + ]; + + if (!empty($arg['description'])) { + $args[$arg['name']]['description'] = $arg['description']; + } + + $queryArgs[] = $arg['target']; + } + + $ret['args'] = $args; + } + + if (!empty($queryArgs)) { + $query = "'".$method."', [".implode(', ', $queryArgs)."]"; + } else { + $query = "'".$method."'"; + } + + $ret['resolve'] = "@=resolver(".$query.")"; + + return $ret; + } + + /** + * Get the formatted graphQL mutation field + * + * @param array $annotation + * + * @return array + */ + protected static function getGraphQLMutationField($annotation) + { + if (!array_key_exists('GraphQLMutation', $annotation)) { + return self::getGraphQLRelayMutationField($annotation); + } + + // @TODO + } + + /** + * Get the formatted graphQL relay mutation field + * + * @param array $annotation + * + * @return array|null + */ + protected static function getGraphQLRelayMutationField($annotation) + { + if (!array_key_exists('GraphQLRelayMutation', $annotation)) { + return null; + } + + $annotation = $annotation['GraphQLRelayMutation']; + if (array_key_exists('args', $annotation)) { + $mutate = "'".$annotation['method']."', [".implode(', ', $annotation['args'])."]"; + } else { + $mutate = "'".$annotation['method']."'"; + } + + return [ + "builder" => "Relay::Mutation", + "builderConfig" => [ + "inputType" => $annotation['input'], + "payloadType" => $annotation['payload'], + "mutateAndGetPayload" => "@=mutation(".$mutate.")", + ], + ]; + } + + /** + * Get graphql access control annotation + * + * @param $annotation + * + * @return null|string + */ + protected static function getGraphQLAccessControl($annotation) + { + if (array_key_exists('GraphQLAccessControl', $annotation) && array_key_exists('method', $annotation['GraphQLAccessControl'])) { + return '@='.$annotation['GraphQLAccessControl']['method']; + } + + return null; + } + + /** + * Get graphql public control + * + * @param $annotation + * + * @return null|string + */ + protected static function getGraphQLPublicControl($annotation) + { + if (array_key_exists('GraphQLPublicControl', $annotation) && array_key_exists('method', $annotation['GraphQLPublicControl'])) { + return '@='.$annotation['GraphQLPublicControl']['method']; + } + + return null; + } + + /** + * Parse annotation + * + * @param mixed $annotation + * + * @return array + */ + protected static function parseAnnotation($annotations) + { + $returnAnnotation = []; + foreach ($annotations as $index => $annotation) { + if (!is_array($annotation)) { + $index = explode('\\', get_class($annotation)); + $index = $index[count($index) - 1]; + } + + $returnAnnotation[$index] = []; + + foreach ($annotation as $indexAnnotation => $value) { + if (is_string($value) && strpos($value, '\\')) { + $value = explode('\\', $value); + $value = $value[count($value) - 1]; + } + + $returnAnnotation[$index][$indexAnnotation] = $value; + } + } + + return $returnAnnotation; + } +} diff --git a/Config/Parser/YamlParser.php b/Config/Parser/YamlParser.php index 7b4f73dcd..44bc3fb9c 100644 --- a/Config/Parser/YamlParser.php +++ b/Config/Parser/YamlParser.php @@ -25,6 +25,7 @@ public static function parse(\SplFileInfo $file, ContainerBuilder $container) try { $typesConfig = self::$yamlParser->parse(file_get_contents($file->getPathname())); +// var_dump($typesConfig); } catch (ParseException $e) { throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $file), 0, $e); } diff --git a/Config/Parser/debug.yml.log b/Config/Parser/debug.yml.log new file mode 100644 index 000000000..9b3e6c9bf --- /dev/null +++ b/Config/Parser/debug.yml.log @@ -0,0 +1,204 @@ +array(1) { + ["Query"]=> + array(2) { + ["type"]=> + string(6) "object" + ["config"]=> + array(2) { + ["description"]=> + string(10) "Root Query" + ["fields"]=> + array(10) { + ["user_me"]=> + array(2) { + ["type"]=> + string(4) "User" + ["resolve"]=> + string(30) "@=resolver('user_resolver_me')" + } + ["user_logout"]=> + array(2) { + ["type"]=> + string(4) "User" + ["resolve"]=> + string(34) "@=resolver('user_resolver_logout')" + } + ["user"]=> + array(3) { + ["type"]=> + string(5) "User!" + ["args"]=> + array(1) { + ["id"]=> + array(2) { + ["type"]=> + string(3) "Int" + ["description"]=> + string(18) "The id of the User" + } + } + ["resolve"]=> + string(45) "@=resolver('user_resolver_get', [args['id']])" + } + ["users"]=> + array(2) { + ["type"]=> + string(8) "[User!]!" + ["resolve"]=> + string(35) "@=resolver('user_resolver_get_all')" + } + ["projects_user_logged"]=> + array(2) { + ["type"]=> + string(10) "[Project]!" + ["resolve"]=> + string(34) "@=resolver('projects_user_logged')" + } + ["project_user"]=> + array(3) { + ["type"]=> + string(7) "Project" + ["args"]=> + array(1) { + ["id"]=> + array(2) { + ["type"]=> + string(6) "String" + ["description"]=> + string(21) "The id of the Project" + } + } + ["resolve"]=> + string(40) "@=resolver('project_user', [args['id']])" + } + ["crawls_user_project"]=> + array(3) { + ["type"]=> + string(12) "[CrawlTask]!" + ["args"]=> + array(1) { + ["id"]=> + array(2) { + ["type"]=> + string(6) "String" + ["description"]=> + string(21) "The id of the project" + } + } + ["resolve"]=> + string(47) "@=resolver('crawls_user_project', [args['id']])" + } + ["crawl_user_project"]=> + array(3) { + ["type"]=> + string(12) "[CrawlTask]!" + ["args"]=> + array(1) { + ["id"]=> + array(2) { + ["type"]=> + string(6) "String" + ["description"]=> + string(19) "The id of the crawl" + } + } + ["resolve"]=> + string(46) "@=resolver('crawl_user_project', [args['id']])" + } + ["crawl_summary"]=> + array(3) { + ["type"]=> + string(16) "CrawlTaskSummary" + ["args"]=> + array(1) { + ["id"]=> + array(2) { + ["type"]=> + string(6) "String" + ["description"]=> + string(24) "The id of the crawl task" + } + } + ["resolve"]=> + string(41) "@=resolver('crawl_summary', [args['id']])" + } + ["locations"]=> + array(2) { + ["type"]=> + string(12) "[Location!]!" + ["resolve"]=> + string(39) "@=resolver('location_resolver_get_all')" + } + } + } + } +} +array(1) { + ["Mutation"]=> + array(2) { + ["type"]=> + string(6) "object" + ["config"]=> + array(1) { + ["fields"]=> + array(4) { + ["userLogin"]=> + array(2) { + ["builder"]=> + string(15) "Relay::Mutation" + ["builderConfig"]=> + array(3) { + ["inputType"]=> + string(14) "UserLoginInput" + ["payloadType"]=> + string(16) "AuthTokenPayload" + ["mutateAndGetPayload"]=> + string(73) "@=mutation('user_mutation_login', [value['username'], value['password']])" + } + } + ["userRegister"]=> + array(2) { + ["builder"]=> + string(15) "Relay::Mutation" + ["builderConfig"]=> + array(3) { + ["inputType"]=> + string(17) "UserRegisterInput" + ["payloadType"]=> + string(11) "UserPayload" + ["mutateAndGetPayload"]=> + string(140) "@=mutation('user_mutation_create', [value['username'], value['email'], value['password'], value['passwordConfirm'], value['termsAccepted']])" + } + } + ["projectCreate"]=> + array(2) { + ["builder"]=> + string(15) "Relay::Mutation" + ["builderConfig"]=> + array(3) { + ["inputType"]=> + string(18) "ProjectCreateInput" + ["payloadType"]=> + string(14) "ProjectPayload" + ["mutateAndGetPayload"]=> + string(90) "@=mutation('project_mutation_create', [value['name'], value['domain'], value['location']])" + } + } + ["projectDelete"]=> + array(2) { + ["builder"]=> + string(15) "Relay::Mutation" + ["builderConfig"]=> + array(3) { + ["inputType"]=> + string(18) "ProjectDeleteInput" + ["payloadType"]=> + string(13) "StatusPayload" + ["mutateAndGetPayload"]=> + string(52) "@=mutation('project_mutation_delete', [value['id']])" + } + } + } + } + } +} diff --git a/DependencyInjection/OverblogGraphQLTypesExtension.php b/DependencyInjection/OverblogGraphQLTypesExtension.php index 350e69532..b1a78a0ff 100644 --- a/DependencyInjection/OverblogGraphQLTypesExtension.php +++ b/DependencyInjection/OverblogGraphQLTypesExtension.php @@ -2,6 +2,7 @@ namespace Overblog\GraphQLBundle\DependencyInjection; +use Overblog\GraphQLBundle\Config\Parser\AnnotationParser; use Overblog\GraphQLBundle\Config\Parser\GraphQLParser; use Overblog\GraphQLBundle\Config\Parser\XmlParser; use Overblog\GraphQLBundle\Config\Parser\YamlParser; @@ -15,12 +16,18 @@ class OverblogGraphQLTypesExtension extends Extension { - const SUPPORTED_TYPES_EXTENSIONS = ['yaml' => '{yaml,yml}', 'xml' => 'xml', 'graphql' => '{graphql,graphqls}']; + const SUPPORTED_TYPES_EXTENSIONS = [ + 'yaml' => '{yaml,yml}', + 'xml' => 'xml', + 'graphql' => '{graphql,graphqls}', + 'annotation' => 'php', + ]; const PARSERS = [ 'yaml' => YamlParser::class, 'xml' => XmlParser::class, 'graphql' => GraphQLParser::class, + 'annotation' => AnnotationParser::class, ]; private static $defaultDefaultConfig = [ diff --git a/Resources/doc/definitions/mutation.md b/Resources/doc/definitions/mutation.md index 802dc2842..606f63d6c 100644 --- a/Resources/doc/definitions/mutation.md +++ b/Resources/doc/definitions/mutation.md @@ -1,6 +1,6 @@ # Mutation -Here an example without using relay: +Here an example without using relay in yaml: ```yaml Mutation: @@ -34,4 +34,10 @@ IntroduceShipInput: type: "String!" ``` +The same example with annotation: @TODO + +```php + +``` + Here the same example [using relay mutation](relay/mutation.md). diff --git a/Resources/doc/definitions/resolver.md b/Resources/doc/definitions/resolver.md index 98b2e4680..4cee369cb 100644 --- a/Resources/doc/definitions/resolver.md +++ b/Resources/doc/definitions/resolver.md @@ -19,7 +19,7 @@ Auto map classes method are accessible by: * for callable classes you can use the service id (example: `AppBunble\GraphQL\InvokeResolver` for a resolver implementing the `__invoke` method) you can also alias a type by implementing `Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface` which returns a map of method/alias. The service created will autowire the `__construct` and `Symfony\Component\DependencyInjection\ContainerAwareInterface::setContainer` methods. **Note:** -* When using service id as FQCN in yaml definition, backslashes must be correctly escaped, here an example: +* When using service id as FQCN in yaml or annotation definition, backslashes must be correctly escaped, here an example: `'@=resolver("App\\GraphQL\\Resolver\\Greetings", [args["name"]])'`. * You can also see the more straight forward way using [resolver map](resolver-map.md). diff --git a/Resources/doc/definitions/schema.md b/Resources/doc/definitions/schema.md index 9c5c68c3a..07a84afcf 100644 --- a/Resources/doc/definitions/schema.md +++ b/Resources/doc/definitions/schema.md @@ -63,6 +63,47 @@ Query: resolve: "@=resolver('character_droid', [args])" ``` +Or using annotation: + +```php +format('Y-m-d H:i:s'); + } + + /** + * @param mixed $value + * + * @return mixed + */ + public static function parseValue($value) + { + return new \DateTime($value); + } + + /** + * @param Node $valueNode + * + * @return string + */ + public static function parseLiteral($valueNode) + { + return new \DateTime($valueNode->value); + } +} +``` \ No newline at end of file diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 8a1bcab34..78757b5c5 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -131,3 +131,7 @@ Finish by dumping the new autoloader. ```bash composer dump-autoload ``` + +## Example + +You can found complete example using React Apollo and OverblogGraphQLBundle with symfony flex and annotation type here: [https://github.com/CocoJr/sf4-react-graphql-example]() diff --git a/Resources/doc/security/fields-access-control.md b/Resources/doc/security/fields-access-control.md index 7949edbfb..3b9c69e1e 100644 --- a/Resources/doc/security/fields-access-control.md +++ b/Resources/doc/security/fields-access-control.md @@ -37,6 +37,60 @@ Human: interfaces: [Character] ``` +Or using annotation: + +```php + + * @copyright 2018 Thibault Colette + */ + +namespace App\Entity\GraphQLType; + +use Overblog\GraphQLBundle\Annotation as GQL; + +/** + * Class FormErrorType + * + * @GQL\GraphQLDescription(description="A humanoid creature in the Star Wars universe.") + */ +class Human implements Character +{ + /** + * @GQL\GraphQLColumn(type="string") + * @GQL\GraphQLDescription(description="The id of the character.") + */ + public $id; + + /** + * @GQL\GraphQLColumn(type="string") + * @GQL\GraphQLDescription(description="The name of the character.") + * @GQL\GraphQLAccessControl(method="isAuthenticated()") + */ + public $name; + + /** + * @GQL\GraphQLToMany(target="Character") + * @GQL\GraphQLDescription(description="The friends of the character.") + */ + public $friends; + + /** + * @GQL\GraphQLToMany(target="Episode") + * @GQL\GraphQLDescription(description="Which movies they appear in.") + */ + public $appearsIn; + + /** + * @GQL\GraphQLColumn(type="string") + * @GQL\GraphQLDescription(description="The home planet of the human, or null if unknown.") + */ + public $homePlanet; +} +``` + + Performance ----------- Checking access on each field can be a performance issue and may be dealt with using: diff --git a/Resources/doc/security/fields-public-control.md b/Resources/doc/security/fields-public-control.md index 2ec84b1b3..141bcf683 100644 --- a/Resources/doc/security/fields-public-control.md +++ b/Resources/doc/security/fields-public-control.md @@ -20,6 +20,35 @@ AnObject: ``` +Or using annotation: + +```php + Date: Wed, 11 Jul 2018 01:46:31 +0200 Subject: [PATCH 2/2] Support Relay Pagination --- Annotation/GraphQLConnectionFields.php | 29 ++++++++ Annotation/GraphQLEdgeFields.php | 29 ++++++++ Annotation/GraphQLNode.php | 29 ++++++++ Annotation/GraphQLQuery.php | 10 +++ Config/Parser/AnnotationParser.php | 92 ++++++++++++++++++++++---- 5 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 Annotation/GraphQLConnectionFields.php create mode 100644 Annotation/GraphQLEdgeFields.php create mode 100644 Annotation/GraphQLNode.php diff --git a/Annotation/GraphQLConnectionFields.php b/Annotation/GraphQLConnectionFields.php new file mode 100644 index 000000000..c407a9b05 --- /dev/null +++ b/Annotation/GraphQLConnectionFields.php @@ -0,0 +1,29 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLConnectionFields +{ + /** + * Type + * + * @var string + */ + public $type; + + /** + * @var string + */ + public $resolve; +} \ No newline at end of file diff --git a/Annotation/GraphQLEdgeFields.php b/Annotation/GraphQLEdgeFields.php new file mode 100644 index 000000000..c61f38f4e --- /dev/null +++ b/Annotation/GraphQLEdgeFields.php @@ -0,0 +1,29 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("PROPERTY") + */ +final class GraphQLEdgeFields +{ + /** + * Type + * + * @var string + */ + public $type; + + /** + * @var string + */ + public $resolve; +} \ No newline at end of file diff --git a/Annotation/GraphQLNode.php b/Annotation/GraphQLNode.php new file mode 100644 index 000000000..1edee26ec --- /dev/null +++ b/Annotation/GraphQLNode.php @@ -0,0 +1,29 @@ + + * @copyright 2018 Thibault Colette + */ + +namespace Overblog\GraphQLBundle\Annotation; + +/** + * Annotation for graphql type + * Use it if you don't use Doctrine ORM annotation + * + * @Annotation + * @Target("CLASS") + */ +final class GraphQLNode +{ + /** + * Type + * + * @var string + */ + public $type; + + /** + * @var string + */ + public $resolve; +} \ No newline at end of file diff --git a/Annotation/GraphQLQuery.php b/Annotation/GraphQLQuery.php index cadb95b84..3ebb5abc4 100644 --- a/Annotation/GraphQLQuery.php +++ b/Annotation/GraphQLQuery.php @@ -29,4 +29,14 @@ class GraphQLQuery * @var string */ public $type; + + /** + * @var array + */ + public $input; + + /** + * @var string + */ + public $argsBuilder; } \ No newline at end of file diff --git a/Config/Parser/AnnotationParser.php b/Config/Parser/AnnotationParser.php index da1752926..fbc1cddf0 100644 --- a/Config/Parser/AnnotationParser.php +++ b/Config/Parser/AnnotationParser.php @@ -6,6 +6,7 @@ use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Zend\Code\Reflection\PropertyReflection; class AnnotationParser implements ParserInterface { @@ -55,6 +56,8 @@ public static function parse(\SplFileInfo $file, ContainerBuilder $container) $type = self::getGraphQLType($annotations); switch ($type) { + case 'relay-connection': + return self::formatRelay($type, $alias, $annotations, $reflexionEntity->getProperties()); case 'enum': return self::formatEnumType($alias, $entityName, $reflexionEntity->getProperties()); case 'custom-scalar': @@ -99,6 +102,63 @@ protected static function getGraphQLType($annotation) return 'object'; } + /** + * @param string $type + * @param string $alias + * @param array $classAnnotations + * @param PropertyReflection[] $properties + * + * @return array + * + * @throws \Exception + */ + protected static function formatRelay($type, $alias, $classAnnotations, $properties) + { + $reader = self::getAnnotationReader(); + + $typesConfig = [ + $alias => [ + 'type' => $type, + 'config' => [], + ] + ]; + + if (!empty($classAnnotations['GraphQLNode'])) { + $typesConfig[$alias]['config']['nodeType'] = $classAnnotations['GraphQLNode']['type']; + $typesConfig[$alias]['config']['resolveNode'] = $classAnnotations['GraphQLNode']['resolve']; + } + + foreach ($properties as $property) { + $propertyName = $property->getName(); + $propertyAnnotation = $reader->getPropertyAnnotations($property); + $propertyAnnotation = self::parseAnnotation($propertyAnnotation); + + if (!empty($propertyAnnotation['GraphQLEdgeFields'])) { + if (empty($typesConfig[$alias]['config']['edgeFields'])) { + $typesConfig[$alias]['config']['edgeFields'] = []; + } + + $typesConfig[$alias]['config']['edgeFields'][$propertyName] = [ + 'type' => $propertyAnnotation['GraphQLEdgeFields']['type'], + 'resolve' => $propertyAnnotation['GraphQLEdgeFields']['resolve'], + ]; + } elseif (!empty($propertyAnnotation['GraphQLConnectionFields'])) { + if (empty($typesConfig[$alias]['config']['connectionFields'])) { + $typesConfig[$alias]['config']['connectionFields'] = []; + } + + $typesConfig[$alias]['config']['connectionFields'][$propertyName] = [ + 'type' => $propertyAnnotation['GraphQLConnectionFields']['type'], + 'resolve' => $propertyAnnotation['GraphQLConnectionFields']['resolve'], + ]; + } + } + + return empty($typesConfig[$alias]['config']) + ? [] + : $typesConfig; + } + /** * Format enum type * @@ -355,25 +415,29 @@ protected static function getGraphQLQueryField($annotation) $method = $annotationQuery['method']; $args = $queryArgs = []; - if (array_key_exists('GraphQLInputArgs', $annotation)) { - $annotationArgs = $annotation['GraphQLInputArgs']; + if (!empty($annotationQuery['input'])) { + $annotationArgs = $annotationQuery['input']; if (!array_key_exists(0, $annotationArgs)) { $annotationArgs = [$annotationArgs]; } foreach ($annotationArgs as $arg) { - $args[$arg['name']] = [ - 'type' => $arg['type'], - ]; - - if (!empty($arg['description'])) { - $args[$arg['name']]['description'] = $arg['description']; + if (!empty($arg['name'])) { + $args[$arg['name']] = [ + 'type' => $arg['type'], + ]; + + if (!empty($arg['description'])) { + $args[$arg['name']]['description'] = $arg['description']; + } } $queryArgs[] = $arg['target']; } - $ret['args'] = $args; + if (!empty($args)) { + $ret['args'] = $args; + } } if (!empty($queryArgs)) { @@ -384,6 +448,11 @@ protected static function getGraphQLQueryField($annotation) $ret['resolve'] = "@=resolver(".$query.")"; + if (!empty($annotationQuery['argsBuilder'])) { + $ret['argsBuilder'] = $annotationQuery['argsBuilder']; + + } + return $ret; } @@ -484,11 +553,6 @@ protected static function parseAnnotation($annotations) $returnAnnotation[$index] = []; foreach ($annotation as $indexAnnotation => $value) { - if (is_string($value) && strpos($value, '\\')) { - $value = explode('\\', $value); - $value = $value[count($value) - 1]; - } - $returnAnnotation[$index][$indexAnnotation] = $value; } }